为什么使用线程池#
使用线程池的好处有三点:
- 降低资源的消耗,可以重复利用已经创建的线程。
- 提高响应速度,任务来的时候不用等待先创建线程在执行了。
- 提高可管理性,线程的调度、创建都统一交给了线程池进行管理
Executors 的四种创建线程池的方法#
这四种方法是不推荐使用的,因为有的是最大线程数没有限制,有的是无界阻塞队列,都可能会造成OOM。
newSingleThreadExecutor “单线程化线程池”#
适用场景:任务按照顺序一个个执行
- 这种线程池里面核心线程数和最大线程数都是1,也就是说只会创建一个线程,并且存活的时间是无限的
- 线程池中的任务是按照顺序进行提交的
- 如果线程正在繁忙的时候,新提交的任务会进入一个无界的阻塞队列里面
- 如果此时执行shutdown方法来关闭线程池,线程池将拒绝新的任务,并且如果再来新任务就抛出拒绝异常,然后不会立刻退出,等把线程池中的任务执行完毕再退出。
- 执行shutdownNow方法的时候,线程池状态会直接变成STOP,并且停止所有的任务,不在处理阻塞队列中的任务,将没有处理的任务返
List<Runnable>runnables=executorService.shutdownNow();
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()));}newFixedThreadPool “固定数量的线程池”#
弊端:无界队列:如果大量的任务进来,会导致队列中的空间无限增大,浪费系统资源
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());}-
如果没有达到固定值,每来一个任务就会创建一个线程,达到之后不会i新建线程
-
池中线程都繁忙的时候,再来新任务就会进无界阻塞队列中去
-
某个线程因为出现异常而终结,会再新建一个线程
newCachedThreadPool “可缓存线程池”#
弊端:最大线程数没有限制,大量异步任务过来的时候会导致大量创建线程,资源耗尽
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());}- 底层实现核心线程数为0 ,最大线程数是MAX,每个空闲线程存活的时间是60s
newScheduledThreadPool “可调度线程池”#
支持定时和周期性任务执行的线程池的方法。使用于延迟执行任务、定期重复执行任务
public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), threadFactory);}- 提供延时或者是周期性的任务调度功能
ThreadPoolExecutor 详解#
参数说明#
public ThreadPoolExecutor( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)- corePoolSize :核心线程数
- maximumPoolSize :最大线程数
- keepAliveTime。空闲的线程存活时间(默认对核心线程不做超时处理,但是可以设置allowCoreThreadTimeOut(true) 让核心线程也收到影响。
- unit 时间单位
- workQueue:阻塞队列
- threadFactory 线程工厂(创建线程的方式)
- handler 拒绝策略
执行流程#
当一个任务到达线程池之后会经历以下流程:
- 判断核心线程数是否满了,如果没有满直接创建新线程执行任务
- 如果核心线程满了则判断阻塞队列是不是满了,如果队列没有满,放到队列里面。
- 如果阻塞队列也满了,就再判断一下最大线程数是不是已经满了。如果满了就触发拒绝策略
- 如果最大线程数还有空余,就创建新线程执行任务
如何确定线程池线程数#
IO密集型#
- 多用于读写操作比较多的时候
核心线程数 = CPU * 2
最大线程数 = CPU * 4 ~ CPU * 10
CPU 密集型#
- 多用于计算比较多的时候
核心线程数 = CPU
最大线程数 = CPU
混合型#
- 多用于读写操作多,并且计算也多的类型,一般是HTTP请求啥的
核心线程数 = CPU * ((线程等待时间 / CPU 时间) + 1)
阻塞队列#
- ArrayBlockingQueue:数组实现的有界阻塞队列,创建时需要指定大小
- LinkedBlockingQueue:基于链表实现的无界阻塞队列,也可以通过有参构造使其有界
- PriorityBlockingQueue:具有优先级的无界阻塞队列
- DelayBlockingQueue:无界阻塞延迟队列,只有过期的元素才会出队。适合于定时任务或者延迟执行的任务
- SynchronousQueue:同步阻塞队列,不存储任何任务,也就是说任务到阻塞队列后,直接根据最大线程数去判断是创建还是拒绝
ThreadFactory 线程工厂#
- 平时不传线程参数的时候,我们调用的是默认的线程工厂(DefaultThreadFactory)
- 我们可以自己自定义一个线程工厂,用来创建线程。
- 我们需要实现ThreadFactory,然后重写里面的方法
class CustomeThreadFactory implements ThreadFactory{ static AtomicInteger threadNo = new AtomicInteger(1); //实现其唯一的创建线程方法 @Override public Thread newThread(Runnable target) { String threadName = "simpleThread-" + threadNo.get(); threadNo.incrementAndGet(); //设置线程名称 Thread thread = new Thread(target, threadName); //设置为守护线程 thread.setDaemon(true); return thread; }}线程池的拒绝策略#
当线程池被关闭或者是工作队列满了并且最大线程数也满了,那么新来的任务就会被拒绝
- AbortPolicy 拒绝,并且抛出异常
- DiscardPolicy 安静的拒绝
- DiscardOldestPolicy :把队列中最老的元素移除,腾出空间
- CallerRunsPolicy: 调用者执行策略,交给上层线程调用。简单来说就是与当前线程池线程无关了,哪个线程提交的任务,哪个线程来执行。这样做的坏处就是会导致新任务提交的速度降低,影响整体性能
- 自定义策略,我们可以实现RejectExecutionHandler 然后 自定义一个策略
调度器钩子方法#
ThreadPoolExecutor 方法里面定义了三个钩子方法,我们可以在线程终止时,执行前、执行后调用。
@Overrideprotected void beforeExecute(Thread t, Runnable r) { super.beforeExecute(t, r);}
@Overrideprotected void afterExecute(Runnable r, Throwable t) { super.afterExecute(r, t);}
@Overrideprotected void terminated() { super.terminated();}线程池的状态#
- RUNNING :这是运行状态
- SHUTDOWN :这个状态的时候不会再接收新任务,但是会把任务队列中的任务执行完毕
- STOP :这个状态的时候不会再接收新任务,不会处理任务队列中的任务中断所有线程
- TIDYING :这个状态的时候,所有的终止处理完毕,执行钩子方法 terminated
- TERMINATED:钩子方法执行完毕
优雅关闭线程池#
-
首先,关闭线程池有三种方法,shutdown、shutdownNow、awaitTermination。
- shutdown:不会再接收新任务,但是会把任务队列中的任务执行完毕
- shutdownNow:直接关闭,并且不在处理任务队列中的任务,但是会返回没有处理的任务
- awaitTermination,等待处理完毕然后关闭
-
我们先执行shutdown 然后给等待时间 awaitTermination,如果等待一直不行就循环等待,再不行就直接关闭shutdownNow
实践#
//配置类@Configurationpublic class ThreadPoolConfig { @Autowired private ThreadPoolRegistry registry;
@Bean public ExecutorService orderPool() { ThreadPoolExecutor pool = new ThreadPoolExecutor( 4, 8, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), r -> new Thread(r, "OrderWorker"), new ThreadPoolExecutor.CallerRunsPolicy() ); return registry.register(pool); }}
//管理类@Componentpublic class ThreadPoolRegistry implements DisposableBean { private final List<ExecutorService> executors = new CopyOnWriteArrayList<>();
public ExecutorService register(ExecutorService executor) { executors.add(executor); return executor; }
@Override public void destroy() { executors.forEach(e -> { e.shutdown(); try { if (!e.awaitTermination(10, TimeUnit.SECONDS)) { e.shutdownNow(); } } catch (InterruptedException ex) { e.shutdownNow(); Thread.currentThread().interrupt(); } }); }}ThreadLocal原理#
ThreadLocal的作用就是存储不同线程的独立的值,我们可以想象成是个Map集合,里面存放的是Thread类和对应的Value
ThreadLocal 的结构#
- ThreadLocal 里面封装了一个 ThreadLocalMap,在这个map里,他的key是ThreadLocal,并且这个key是一个弱引用。为什么是弱引用后面再分析。
- Thread类里面有一个变量
ThreadLocal.ThreadLocalMap threadLocals = null;,也就是说 ThreadLocalMap是在Thread类里面维护的
ThreadLocal 为什么会导致内存泄漏#
- 首先,在ThreadLocal 里封装的ThreadLocalMap 他的key是指向ThreadLocal的弱引用,当线程中某个方法创建ThreadLocal后,会有一个强引用指向 ThreadLocal实例。但是执行完毕后,方法的栈帧被销毁了,也就是说ThreadLocal 的强引用没了,指向他的只有一个弱引用,就会导致,key实例被回收,而value无法回收,就导致了内存泄漏