03线程池
线程池
ThreadLocal
- 用于为每个线程提供独立的变量副本,实现线程隔离,避免共享变量的线程安全问题。它在高并发场景下广泛应用
- 实现原理
- 每个
Thread
对象内部维护一个ThreadLocalMap
,本质是一个定制化的哈希表。 ThreadLocalMap
的键(Key)是ThreadLocal
实例,值(Value)是线程的变量副本。ThreadLocal
实例是 弱引用(WeakReference),使用弱引用是为了当ThreadLocal
实例被垃圾回收(GC)时,对应的键会自动被清除,避免内存泄漏- 一个线程中可以有多个ThreadLocal实例
- 哈希冲突解决:开放寻址法(线性探测)。
- 每个
- threadlocal是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据.每个线程都维护着一个ThreadLocalMap
- 底层是数组结构,寻址使用的斐波那契散列法,哈希冲突时使用拉链法解决
- 维护着一个ThreadLocalMap对象,ThreadLocalMap是当前线程Thread一个叫threadLocals的变量中获取的。并未实现Map接口,而且他的Entry是继承WeakReference(弱引用)的,哈希冲突靠开放寻址的线性探测来实现
add操作
内存泄漏
- 强引用链:线程(如线程池线程) →
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 存储在队列中。
常用的阻塞队列:
threadFactory(线程工厂 非必须参数)
- 用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)
handler(线程饱和策略 非必须参数)
- 当线程池和队列都满了,再加入线程会执行此策略。 JDK默认提供了四种
- Abort:默认拒绝处理策略,直接抛出RejectedExecutionException异常
- Discard:扔掉,丢弃任务且不抛异常
- DiscardOldest:丢弃队列头部(最旧的)的任务,然后重新尝试执行程序(如果再次失败,重复此过程)
- CallerRuns:由调用线程处理该任务
- 当线程池和队列都满了,再加入线程会执行此策略。 JDK默认提供了四种
调度线程
- 线程池本身有一个调度线程,这个线程就是用于管理布控整个线程池里的各种任务和事务,例如创建线程、销毁线程、任务队列管理、线程队列管理等等。
故线程池也有自己的状态。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
4public 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密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。
- 因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。
- 线程数=N(CPU核数)*(1+WT(线程等待时间)/ST(线程时间运行时间))
03线程池
https://x-leonidas.github.io/2022/02/01/04Java/多线程/03线程池/