Java线程池详解(执行原理、拒绝策略、Executors三种线程池对比)

Java线程池详解

    • 1. 简介
    • 2. 参数说明
    • 3. 执行机制
    • 4. 阻塞队列
    • 5. 创建新线程的工厂ThreadFactory
    • 6. 拒绝策略
    • 7. Executors下三种线程池对比
        • newFixedThreadPool
        • newCachedThreadPool
        • newSingleThreadExecutor

1. 简介

        线程池,顾名思义,存放线程的池子,线程池的创建与管理是需要消耗一定的资源的,现在假设一个场景,在你的程序中,存在一些高并发的任务,而且任务执行时间往往都不长,那你就需要频繁的创建与销毁线程,这样对服务器资源来说是极大的浪费,如果采用线程池,将线程提前创建好,用的时候直接拿,不用的时候放回去,则会节省很多资源。当然线程池不仅仅能维护好线程,并且能根据不同的场景定制化不同的线程池,线程数量也有弹性,还能维护需要执行的任务,下面我们来详细探讨。

2. 参数说明

        Java中线程池类为ThreadPoolExecutor,其全参构造方法如下:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

例如我现在要创建一个线程池可以这么定义:

ThreadPoolExecutor ex = new ThreadPoolExecutor(
												2,
												5,
												0L,
												TimeUnit.MICROSECONDS,
												new LinkedBlockingDeque<>(1024));
// 使用如下, 这是lmbda表达式的方式, 你也可以自己定义Runnable类型的线程, 并创建对象然后传入
ex.submit(() -> {
	// 线程执行体
});
参数名 备注 解释说明
corePoolSize 核心线程数 默认初始化时线程池中没有线程,当任务第一次到达的时候,才会创建线程,但是可以通过调用prestartAllCoreThreads()方法来预热所有核心线程。默认情况下,核心线程一旦被创建,就会一直存活,但可以通过allowCoreThreadTimeOut(true)来设定核心线程超过空闲时间自动销毁
maximumPoolSize 最大线程数 当核心线程全用上,并且阻塞队列已满的情况下,如果最大线程数大于核心线程数,则会创建新的线程用于执行任务,如果等于核心线程数,则触发拒绝策略。在线程池中的线程没有核心和最大线程之分,只看线程的数量。
keepAliveTime 线程空闲时间 默认情况下,当线程池中的线程大于核心线程数时才生效,超过核心线程数的额外线程空闲时间超过设定时间后会自动销毁,但上面介绍过,核心线程也可以通过方法来配置超时自动销毁。
unit 线程空闲时间的单位 参数keepAliveTime的单位,直接从TimeUnit类中获取
workQueue 阻塞队列 当核心线程数全不空闲,则提交的任务存放于阻塞队列(队列不满的情况下),当线程空闲后,从阻塞队列取出任务并执行
threadFactory 创建线程工厂 用于线程池创建新线程,一般我们采用默认的即可,特殊情况,例如我们需要给线程重新命名,可以重写,后续会用代码举例
handler 拒绝策略 当阻塞队列已满,线程池中的线程数已是最大线程数且没有空闲线程,则触发拒绝策略。一般我们使用默认的即可,但特殊情况,例如我们想要用线程池达到一种生产者消费者模式,就需要我们自定义拒绝策略,,后续会详细介绍

3. 执行机制

        默认情况下,线程池初始化完成后,线程池中的线程数量为0,当任务第一次提交到线程池中时,会创建线程去执行任务,直到线程数等于核心线程数,如果还有任务继续提交进来且没有空闲线程,那么提交进来的任务会放入阻塞队列;如果阻塞队列已满,且最大线程数大于核心线程数,则创建新线程去执行任务,直到线程数等于最大线程数;如果线程池中的线程数也达到最大线程数量,且阻塞队列放满,也没有空闲线程,此时还有线程提交进来,则触发拒绝策略。
        当线程池中的线程超过空闲时间时,会被销毁,默认核心线程不会被销毁。

4. 阻塞队列

        Java提供了多种阻塞队列,针对不同的业务,我们可以选择不同的阻塞队列,阻塞队列的类型为BlockingQueue,其实现类有很多,下面只列举几个常用的:

SynchronousQueue: 直接提交队列,因为是个没有容量的队列,来的任务会直接提交给线程去执行,如果没有空闲线程,则创建新线程,如果已经达到最大线程,则触发拒绝策略,这种阻塞队列比较适用于最大线程数为无界的线程池,避免在执行过程中出发拒绝策略。

LinkedBlockingDeque: 无界阻塞队列,默认其大小为Integer.MAX_VALUE,take和put分别有一把锁,效率更高,在使用的时候尽量传值设定一下队列大小,避免任务过多导致OOM

ArrayBlockingQueue: 有界阻塞队列,构造方法必须传队列大小

DelayQueue:延迟阻塞队列,是无界的,队列中的每个元素都有过期时间,只有过期的元素才会出队,队列头部的元素是最早过期的元素,也正是从队列头部取元素,如果没有过期元素,则会阻塞。

5. 创建新线程的工厂ThreadFactory

        用于线程池创建新的线程,一般我们无需传这个参数,让线程池使用默认的就好,但是我们的开发标准是,线程池中的线程一定要重新命名,默认情况下,线程池中线程的命名规则为thread-pool-,如果我们不重新命名,那么出现问题了,并不能区分出问题出在哪里,如果用于a业务的线程池命名为a-thread-pool-,b业务线程池的线程命名规则为b-thread-pool-,这样我们在查日志的时候看线程名字就知道是哪块业务出的问题。使用方式如下:

static ThreadPoolExecutor ex = new ThreadPoolExecutor(5, 5,0L, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(30), 
	new CustomizableThreadFactory("iot-push-pools-"));

本质是实现ThreadFactory接口,实现newthread(),如下:

public class ThreadFactorySelf implements ThreadFactory {
    private final AtomicInteger atomicInteger = new AtomicInteger(0);
    @Override
    public Thread newThread(Runnable r) {
        String name = "self-thread-pool-" + atomicInteger.getAndIncrement();
        return new Thread(r, name);
    }
}

你可以按照你任何的方式定义新的线程,然后将实现类的对象作为参数传入线程池。

6. 拒绝策略

        上述已经介绍过什么时候触发拒绝策略了,此处不再赘述。Java自带的拒绝策略有四种,父接口为RejectedExecutionHandler
AbortPolicy(默认):丢弃任务并抛出 RejectedExecutionException 异常。
CallerRunsPolicy:由提交任务的线程处理该任务。
DiscardPolicy:丢弃任务,但是不抛出异常。。
DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。

        除了上面四种拒绝策略外,我们可以自定义拒绝策略,例如靠拒绝策略实现不丢失任务且不会OOM的生产者消费者模式,代码如下:

创建线程池

private static final ThreadPoolExecutor ex = new ThreadPoolExecutor(
			2, 
			2, 
			0L,
 			TimeUnit.SECONDS,
            new CustomizableThreadFactory("batch-handler-pools-"), 
            new LinkedBlockingQueue<Runnable>(512), 
            new BatchHandlerReject());

创建同步监视器, 实际就是随便的一个对象

public class BatchHandlerLock {
    public static final Object o = new Object();
}

提交任务

for (int i = 0; i < 100000; i++) {
    ex.submit(() -> {
        try {
            // 任务逻辑, 这里睡眠500ms模拟
            Thread.sleep(500);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
			// 任务执行结束, 唤醒因拒绝策略导致阻塞的线程
			synchronized (BatchHandlerLock.o) {
				BatchHandlerLock.o.notify();
			}
		}
    });
}

拒绝策略

@Slf4j
public class BatchHandlerReject implements RejectedExecutionHandler {

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        try {
            // 当线程池的阻塞队列满了以后, 让提交任务的线程阻塞
            // 在任务处理结束后, 唤醒线程, 然后继续提交任务
            synchronized (BatchSendSmsLock.o) {
                log.info("【 阻塞队列已满, 当前线程被阻塞 】");
                BatchSendSmsLock.o.wait();//任务执行结束后会唤醒线程,线程会从这里继续往后执行
            }
            log.info("【 有任务执行完毕, 队列有空余, 线程被唤醒, 继续提交任务 】");
            e.submit(r);
        } catch (InterruptedException interruptedException) {
            interruptedException.printStackTrace();
        }
    }
}

7. Executors下三种线程池对比

        Executors下提供了几种创建线程池的方法,个有不同,只有在了解他们的特性之后,才能在不同的业务场景中选择最合适的线程池,下面拎出来三个作一下介绍和对比,需要说的是下面三个均可能会发生OOM,是否可用根据自己服务器配置和任务数量考虑。

newFixedThreadPool

        定长线程池,核心线程与最大线程数相同,空闲超时时间为0,阻塞队列采用LinkedBlockingQueue,队列长度为Integer.MAX_VALUE,无界阻塞队列,这种线程池适用于CPU密集型的任务场景,因为线程数是固定的,阻塞队列无边界,如果你的任务需要cpu不停计算,那么要再多的线程也无济于事,最好是的方式就是线程固定,任务放入阻塞队列,按部就班地进行消费。

newCachedThreadPool

        变长线程池,核心线程数为0,最大线程数为Integer.MAX_VALUE,阻塞队列采用无缓存队列SynchronousQueue,空闲超时时间为60S,这种线程池适用于IO密集型的任务场景,IO是会阻塞的,一个线程所发生的IO操作都会使CPU空闲,我们都知道CPU的时间是非常宝贵的,我们就是要CPU不能停下来,而这个线程池,每次接受任务如果没有空闲线程都会不断创建先新的线程,如此一来,才能榨干CPU的资源,合理利用

newSingleThreadExecutor

        单线程池,核心线程数与最大线程数均为1,阻塞队列采用无边界线程池LinkedBlockingQueue,这种比较好理解,可以用来处理对顺序有要求的任务。

好了,本次介绍就到这里了。能力有限,若有不足之处欢迎指正~

你可能感兴趣的:(java技术分享,Java面试总结,java,ThreadPool)