线程池
线程池:
JDK的线程池有:ThreadPoolExecutor,ScheduledThreadPoolExecutor(带任务调度)
ThreadPoolExecutor构造方法参数说明:ThreadPoolExecutor(核心线程数,最大线程数,救急线程存活时间,存活时间单位,阻塞队列),
救急线程数=最大线程数-核心线程数,当任务数多于线程数时,如果有救急线程,就会创建救急线程,如果没有就会进入阻塞队列等待.
1 创建固定大小的线程池:
ExecutorService pool = Executors.newFixedThreadPool(2);固定大小线程池适合任务数固定的情况
底层实现:return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
1.如果线程数量没有达到“固定数量”,则每次提交一个任务池内就创建一个新的线程,直到到达固定的数量
2.线程池的大小一旦达到“固定数量”就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
3.如果池中的所有线程均在繁忙状态,对于新任务会进入阻塞队列中(无界的阻塞队列)。
使用场景:需要任务长期执行的场景。“固定数量的线程池”的线程数能够比较稳定保证一个数,能够避免频繁回收线程和创建线程,故适用于处理 CPU 密集型的任务,在 CPU 被工作线程长时间使用的情况下,能确保尽可能少的分配线程。 弊端:内部使用无界队列来存放排队任务,当大量任务超过线程池最大容量需要处理时,队列无线增大,使服务器资源迅速耗尽。
2 创建单一线程的线程池:ExecutorService pool = Executors.newSingleThreadExecutor(),这种线程池和自己创建单线程的区别:
某一个任务报错时单一线程的线程池会再创建一个线程继续执行下面的任务,单线程需要自己处理异常才会向下执行.
单一线程池的底层创建代码:
(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
1.单线程化的线程池中的任务,是按照提交的次序,顺序执行的
2.池中的唯一线程的存活时间是无限的
3.当池中的唯一线程正繁忙时,新提交的任务实例会进入内部的阻塞队列中,并且其阻塞队列是无界的。
总体来说,单线程化的线程池所适用的场景是:任务按照提交次序,一个任务一个任务逐个执行的场景。
3 newCachedThreadPool 创建“可缓存线程池”
ExecutorService pool = Executors.newCachedThreadPool();
底层实现:return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,newSynchronousQueue<Runnable>());
1.在接收新的异步任务 target 执行目标实例时,如果池内所有线程繁忙,此线程池会添加新线程来处理任务。
2.此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。
3.如果部分线程空闲,也就是存量线程的数量超过了处理任务数量,那么就会回收空闲(60 秒不执行任务)线程。
适用场景:需要快速处理突发性强、耗时较短的任务场景,如 Netty 的NIO 处理场景、REST API 接口的瞬时削峰场景。“可缓存线程池”的线程数量不固定,只要有空闲线程就会被回收;接收到的新异步任务执行目标,查看是否有线程处于空闲状态,如果没有就直接创建新的线程。 弊端:线程池没有最大线程数量限制,如果大量的异步任务执行目标实例同时提交,可能导致创线程过多会而导致资源耗尽。
线程池的执行过程:
(1)如果线程池中的线程数未达到核心线程数,则创建核心线程处理任务。
(2)如果线程数大于或者等于核心线程数,则将任务加入任务队列,线程池中的空闲线程会不断地从 任务队列中取出任务进行处理。
(3)如果任务队列满了,并且线程数没有达到最大线程数,则创建非核心线程去处理任务。
(4)如果线程数超过了最大线程数,则执行饱和策略

线程池执行任务方法: pool.execute(Runnable接口)无返回值,pool.submit(Runnable接口)有返回值
线程池里核心线程是一直存活的,没有任务执行程序也不会结束,想要结束线程池需要调用pool.shutDown方法,调用这个方法后
正在执行的线程会继续执行完,阻塞队列里的任务也会执行完,但不会接受新的任务
使用线程池遇到的问题:
1.线程池默认使用无界队列,任务过多导致OOM
public class NewFixedTest {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < Integer.MAX_VALUE; i++) {
executor.execute(() -> {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
//do nothing
}
});
}
}
}
使用newFixedThreadPool创建的线程池,是会有坑的,它默认是无界的阻塞队列,如果任务过多,会导致OOM问题。运行一下以上代码,出现了OOM。这是因为newFixedThreadPool使用了无界的阻塞队列的LinkedBlockingQueue,如果线程获取一个任务后,任务的执行时间比较长(比如,上面demo代码设置了10秒),会导致队列的任务越积越多,导致机器内存使用不停飙升, 最终出现OOM。
因此,工作中,建议大家自定义线程池,并使用指定长度的阻塞队列。
2. 线程池创建线程过多,导致OOM
newCachedThreadPool线程池的最大线程数是Integer.MAX_VALUE,可能发生OOM.
3 线程池拒绝策略的坑
我们知道线程池主要有四种拒绝策略,如下:
AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。(默认拒绝策略)
DiscardPolicy:丢弃任务,但是不抛出异常。
DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务。
CallerRunsPolicy:由调用方线程处理该任务。
如果线程池拒绝策略设置不合理,就容易有坑。我们把拒绝策略设置为DiscardPolicy或DiscardOldestPolicy并且在被拒绝的任务,Future对象调用get()方法,那么调用线程会一直被阻塞。
public class DiscardThreadPoolTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 一个核心线程,队列最大为1,最大线程数也是1.拒绝策略是DiscardPolicy
ThreadPoolExecutor executorService = new ThreadPoolExecutor(1, 1, 1L, TimeUnit.MINUTES,
new ArrayBlockingQueue<>(1), new ThreadPoolExecutor.DiscardPolicy());
Future f1 = executorService.submit(()-> {
System.out.println("提交任务1");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Future f2 = executorService.submit(()->{
System.out.println("提交任务2");
});
Future f3 = executorService.submit(()->{
System.out.println("提交任务3");
});
System.out.println("任务1完成 " + f1.get());// 等待任务1执行完毕
System.out.println("任务2完成" + f2.get());// 等待任务2执行完毕
System.out.println("任务3完成" + f3.get());// 等待任务3执行完毕
executorService.shutdown();// 关闭线程池,阻塞直到所有任务执行完毕
}
}
上面的任务3永远不会执行,因为任务一需要3秒执行完,这期间线程池会将第二个任务放到阻塞队列,阻塞队列长为1,此时已经满了再去执行第三个线程时发现线程池的线程不够用会丢掉第三个任务,等执行到f3.get()时这个任务已经没了,就会一直等下去.
日常开发中,使用 Future.get() 时,尽量使用带超时时间的,因为它是阻塞的。 future.get(1, TimeUnit.SECONDS);
4 线程池的submit方法不会抛异常,比如代码中如果出现空指针异常不会被发现,想发现异常就用try...catch捕获或改为execute方法执行.
5 ThreadLocal和线程池一起使用,线程池复用可能造成ThreadLocal中获取到之前的值,所以ThreadLocal用完一定要用remove()清空
6 创建线程池时一定要给线程池起名字,便于后面查日志
ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(20),new CustomizableThreadFactory("Mysession-Thread-pool")); executorOne.execute(()->{ System.out.println("xxxxxxxxxx"); throw new NullPointerException(); });
7 不要一个项目所有用多线程的地方都用一个线程池,避免有的业务慢或者阻塞导致影响所有线程.
浙公网安备 33010602011771号