线程池

线程池:
JDK的线程池有:ThreadPoolExecutor,ScheduledThreadPoolExecutor(带任务调度)
ThreadPoolExecutor构造方法参数说明:ThreadPoolExecutor(核心线程数,最大线程数,救急线程存活时间,存活时间单位,阻塞队列、线程工厂、拒绝策略),
救急线程数=最大线程数-核心线程数,当任务数多于线程数时,如果有救急线程,就会创建救急线程,如果没有就会进入阻塞队列等待.

 Executors提供的三种线程池:

1 创建固定大小的线程池:
ExecutorService pool = Executors.newFixedThreadPool(2);固定大小线程池适合任务数固定的情况
底层实现:return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());

  • 配置:核心线程数 = 最大线程数 = n,空闲线程不回收,使用无界队列(LinkedBlockingQueue)。

  • 特点:线程数量固定,任务执行平稳。但致命弱点是无界队列,如果任务积压速度超过处理速度,队列会无限增长,最终导致 OOM(内存溢出)。

  • 适用:任务量相对平稳,对响应速度要求高的场景。

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>());

  • 特点:来任务就创建线程,线程空闲60秒后自动销毁。理论上可以创建无限多线程。

  • 致命弱点:最大线程数接近无限大(Integer.MAX_VALUE),在高并发下会创建海量线程,导致 CPU 飙升和 OOM(创建线程过多)。

  • 适用:执行大量短生命周期的任务(如网络请求、普通计算)。

阿里巴巴Java开发手册》明确禁止使用 Executors 的这3个静态方法,理由就是上述的“无界队列”和“无限线程”风险。

在生产环境,正确的做法是:
直接 new ThreadPoolExecutor,并显式传递有界阻塞队列(如 ArrayBlockingQueue)和明确的拒绝策略。

生产环境推荐写法示例:

// 推荐:自定义核心参数,使用有界队列,防止OOM
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    5,                    // 核心线程数
    10,                   // 最大线程数
    60L,                  // 空闲存活时间
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(200), // 有界队列(核心!)
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略(让提交任务的线程自己跑)
);

 阻塞队列(BlockingQueue<Runnable>)

  • 作用:当核心线程全部繁忙时,新来的任务会被放入此队列中排队等候。

  • 关键误区(面试必坑):队列的类型直接决定了最大线程数是否生效!

    • 如果是无界队列(如 LinkedBlockingQueue),队列永远装不满,就永远不会创建超过核心线程数的线程(最大线程数形同虚设)。

    • 如果是有界队列(如 ArrayBlockingQueue),队列满了才会触发创建新线程(直到达到最大线程数)。

最大线程数(maximumPoolSize)

  • 作用:线程池允许创建的最高上限(核心线程 + 紧急临时线程总数)。

  • 触发条件:只有在核心线程全忙 且 阻塞队列已满 的情况下,才会创建新的临时线程。

  • 风险:如果设置过大,会频繁切换上下文损耗 CPU;设置过小,任务会被拒绝。

线程池的执行过程:

当你调用 executor.execute(task) 提交一个任务时,内部执行顺序是严格按三步走的,一步卡住才走下一步:

第1步:如果当前工作线程数 < 核心线程数,直接新建核心线程处理任务(无视队列是否为空)。
第2步:如果当前工作线程数 >= 核心线程数,尝试将任务放入阻塞队列。
第3步:如果队列已满,尝试新建临时线程(直到达到最大线程数)。
第4步:如果当前线程数 == 最大线程数 且 队列也满了,执行拒绝策略。

注意这个顺序非常反直觉:并不是“核心满了 -> 创建临时线程 -> 放队列”,而是 “核心满了 -> 先塞队列 -> 队列满了才创建临时线程”!

线程池执行任务方法: pool.execute(Runnable接口)无返回值,pool.submit(Runnable接口)有返回值
线程池里核心线程是一直存活的,没有任务执行程序也不会结束,想要结束线程池需要调用pool.shutDown方法,调用这个方法后
正在执行的线程会继续执行完,阻塞队列里的任务也会执行完,但不会接受新的任务

 

使用线程池遇到的问题:

 线程池拒绝策略的坑
我们知道线程池主要有四种拒绝策略,如下:

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方法不会抛异常,因为 submit 返回的是 Future,异常被作为返回结果存储在了 Future 内部(相当于把异常“打包”了)

比如代码中如果出现空指针异常不会被发现,想发现异常就用try...catch捕获或改为execute方法执行.

Future<?> future = executor.submit(() -> {
    int i = 1 / 0; // 这里会报错
});

// 如果不调用下面这行,程序静默执行,你看不到任何报错!
try {
    future.get(); // 这里才会抛出 ExecutionException
} catch (ExecutionException e) {
    Throwable realException = e.getCause(); // realException 就是 ArithmeticException
    System.out.println("捕获到真实异常:" + realException);
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

 

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 不要一个项目所有用多线程的地方都用一个线程池,避免有的业务慢或者阻塞导致影响所有线程.

 
posted @ 2023-05-22 15:46  杨吃羊  阅读(73)  评论(0)    收藏  举报