03线程池

线程池

ThreadLocal

  • 用于为每个线程提供独立的变量副本,实现线程隔离,避免共享变量的线程安全问题。它在高并发场景下广泛应用
  • 实现原理
    • 每个 Thread 对象内部维护一个 ThreadLocalMap,本质是一个定制化的哈希表。
    • ThreadLocalMap 的键(Key)是 ThreadLocal 实例,值(Value)是线程的变量副本。ThreadLocal 实例是 弱引用(WeakReference),使用弱引用是为了当 ThreadLocal 实例被垃圾回收(GC)时,对应的键会自动被清除,避免内存泄漏
    • 一个线程中可以有多个ThreadLocal实例
    • 哈希冲突解决:开放寻址法(线性探测)。
  • threadlocal是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据.每个线程都维护着一个ThreadLocalMap
  • 底层是数组结构,寻址使用的斐波那契散列法,哈希冲突时使用拉链法解决
    • 维护着一个ThreadLocalMap对象,ThreadLocalMap是当前线程Thread一个叫threadLocals的变量中获取的。并未实现Map接口,而且他的Entry是继承WeakReference(弱引用)的,哈希冲突靠开放寻址的线性探测来实现

add操作

  • threadLocalAdd流程

内存泄漏

  • 强引用链:线程(如线程池线程) → ThreadLocalMap → Entry → Value。
  • 如果threadLocal定义的是线程变量,当线程运行完成,由于是线程池,线程会一直存活,会一直保持对threadLocal和value的引用,由于key采用弱引用,在下一次GC时,threadLocal对象会被回收,需要等到下一次对这个线程的threadLocal操作,才会断开对value的引用,那么这段时间中,value是一直不能被GC掉的,从而导致内存泄漏。试想一下,如果key是强引用,这种情况key和value就永远都不会被GC掉了
  • 如果threadLocal定义的是静态变量,那threadLocal对象会一直存活,在上面的场景下,value会一直被引用,导致value不会被GC,产生内存泄漏。

最佳实践

  • 尽量使用 try-finally 清理资源
    确保 remove() 在 finally 块中被调用。
  • 避免存储大对象
    ThreadLocal 的数据会伴随线程生命周期,大对象易导致内存溢出。
  • 谨慎使用全局静态 ThreadLocal
    静态变量可能导致 ClassLoader 内存泄漏(尤其在 Web 应用重启时)。
  • 优先使用 withInitial
    Java 8+ 的工厂方法更简洁且支持延迟初始化。

InheritableThreadLocal

  • 实现原理
    • 使用 Thread 的另一个 ThreadLocalMap(即 inheritableThreadLocals
    • 当父线程创建子线程时,若父线程的 inheritableThreadLocals 不为空,则将其内容复制到子线程的 inheritableThreadLocals 中。
    • 仅在子线程创建时复制一次,后续父线程对变量的修改不影响已创建的子线程。
  • 特性 ThreadLocal InheritableThreadLocal
    数据传递范围 仅限当前线程 父线程 → 子线程(单向传递)
    实现机制 通过 Thread.threadLocals 存储数据 通过 Thread.inheritableThreadLocals 存储数据
    子线程能否继承数据 是(仅在子线程创建时复制一次)
    典型应用场景 线程隔离的上下文(如数据库连接、Session) 父子线程间传递上下文(如 TraceID、用户信息)
  • 注意事项
    • 线程复用导致数据污染,注意子线程任务结束后手动清理数据
    • 深拷贝和浅拷贝
      • InheritableThreadLocal 直接复制对象引用,若对象可变,父子线程可能共享同一实例。
      • 重写InheritableThreadLocal的childValue()来避免浅拷贝的问题

线程池

概念

  • 线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。

工作机制

  • 在线程池的编程模式下,任务是提交给整个线程池,而不是直接提交给某个线程,线程池在拿到任务后,就在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲的线程。
  • 一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。

线程池的主要参数

  • corePoolSize(线程池基本大小)

    • 当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,(除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程)
  • maximumPoolSize(线程池最大个数)

    • 线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
  • keepAliveTime(线程存活保持时间)+ 生存时间单位

    • 当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。
    • 在 ThreadPoolExecutor 中的方法 allowCoreThreadTimeOut(boolean value) 设置为 true时,也作用于核心线程。
  • workQueue(任务队列)

    • 用于传输和保存等待执行任务的阻塞队列

    • 通过线程池的 execute(Runnable command) 方法会将任务 Runnable 存储在队列中。

    • 常用的阻塞队列:

    • img

  • threadFactory(线程工厂 非必须参数)

    • 用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)
  • handler(线程饱和策略 非必须参数)

    • 当线程池和队列都满了,再加入线程会执行此策略。 JDK默认提供了四种
      • Abort:默认拒绝处理策略,直接抛出RejectedExecutionException异常
      • Discard:扔掉,丢弃任务且不抛异常
      • DiscardOldest:丢弃队列头部(最旧的)的任务,然后重新尝试执行程序(如果再次失败,重复此过程)
      • CallerRuns:由调用线程处理该任务

调度线程

  • 线程池本身有一个调度线程,这个线程就是用于管理布控整个线程池里的各种任务和事务,例如创建线程、销毁线程、任务队列管理、线程队列管理等等。
    故线程池也有自己的状态。ThreadPoolExecutor类中使用了一些final int常量变量来表示线程池的状态 ,分别为RUNNING、SHUTDOWN、STOP、TIDYING 、TERMINATED。
    1
    2
    3
    4
    5
    6
    // runState is stored in the high-order bits
    private static final int RUNNING = -1 << COUNT_BITS;
    private static final int SHUTDOWN = 0 << COUNT_BITS;
    private static final int STOP = 1 << COUNT_BITS;
    private static final int TIDYING = 2 << COUNT_BITS;
    private static final int TERMINATED = 3 << COUNT_BITS;
    • 线程池创建后处于RUNNING状态。
    • 调用shutdown()方法后处于SHUTDOWN状态,线程池不能接受新的任务,清除一些空闲worker,不会等待阻塞队列的任务完成。
    • 调用shutdownNow()方法后处于STOP状态,线程池不能接受新的任务,中断所有线程,阻塞队列中没有被执行的任务全部丢弃。此时,poolsize=0,阻塞队列的size也为0。
    • 当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。接着会执行terminated()函数。

      ThreadPoolExecutor中有一个控制状态的属性叫ctl,它是一个AtomicInteger类型的变量。线程池状态就是通过AtomicInteger类型的成员变量ctl来获取的。

      获取的ctl值传入runStateOf方法,与~CAPACITY位与运算(CAPACITY是低29位全1的int变量)。

      ~CAPACITY在这里相当于掩码,用来获取ctl的高3位,表示线程池状态;而另外的低29位用于表示工作线程数

    • 线程池处在TIDYING状态时,执行完terminated()方法之后,就会由 TIDYING -> TERMINATED, 线程池被设置为TERMINATED状态。

线程池流程

  • 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
    a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
    b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
    c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
    d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常 RejectExecutionException。

线程复用

  • ThreadPoolExecutor在创建线程时,会将线程封装成工作线程worker,并放入工作线程组中,然后这个worker反复从阻塞队列中拿任务去执行

四种常见的线程池

newCachedThreadPool

  • 用的是SynchronousQueue队列
  • 用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换)
  • 是一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute() 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。注意,可以使用 ThreadPoolExecutor 构造方法创建具有类似属性但细节不同(例如超时参数)的线程池。

newFixedThreadPool

  • 创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重)
  • 线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

newSingleThreadExecutor

  • 创建一个单线程的线程池,适用于需要保证顺序执行各个任务。所有的任务都是串行执行的。
  • 如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

newScheduledThreadPool

  • 创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。 适用于执行延时或者周期性任务
  • 指定了核心线程数量的线程池,但是它能容纳近乎无限(Integer.MaxValue)的普通线程数量

WorkStealingPool

  • 一种特殊类型的ForkJoinPool,使用当前系统的可用线程,默认的线程工厂,没有拒绝处理器的同步线程池

execute()和submit()方法

  • execute(),执行一个任务,没有返回值。
  • submit(),提交一个线程任务,有返回值。
  • submit(Callable task)能获取到它的返回值,通过future.get()获取(阻塞直到任务执行完)。一般使用FutureTask+Callable配合使用(IntentService中有体现)。
  • submit(Runnable task, T result)能通过传入的载体result间接获得线程的返回值。
  • submit(Runnable task)则是没有返回值的,就算获取它的返回值也是null。Future.get方法会使取结果的线程进入阻塞状态,知道线程执行完成之后,唤醒获取结果的线程,然后返回结果。

守护线程

  • 守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。
  • 当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。main() 属于非守护线程。
  • 在线程启动之前使用 setDaemon() 方法可以将一个线程设置为守护线程。
    1
    2
    3
    4
    public static void main(String[] args) {
    Thread thread = new Thread(new MyRunnable());
    thread.setDaemon(true);
    }

线程池的关闭

  • shutdown():不会立即的终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务。
  • shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。相当于调用每个线程的 interrupt() 方法。

如何配置线程池

  • CPU密集型任务
    • 尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。
  • IO密集型任务
    • 可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。
  • 混合型任务
    • 线程数=N(CPU核数)*(1+WT(线程等待时间)/ST(线程时间运行时间))
      • ST = Service Time / CPU Time (线程服务时间 / CPU 运行时间)
    • 可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。
    • 因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。

03线程池
https://x-leonidas.github.io/2022/02/01/04Java/多线程/03线程池/
作者
听风
发布于
2022年2月1日
更新于
2025年6月25日
许可协议