暴力突破 Java 并发 - 线程池

一、线程池的定义


相比 new Thread() 方法创建线程,Java 提供线程池的好处在于:

  • 重用存在的线程,减少线程创建、消亡的开销,性能佳。 
  • 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。 
  • 提供定时执行、定期执行、单线程、并发数控制等功能。

ExecutorService 是最初的线程池接口,ThreadPoolExecutor 类是对线程池的具体实现,它通过构造方法来配置线程池的参数,我们来分析一下它常用的构造函数吧。

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

参数解释:

  • corePoolSize:核心线程的数量,默认情况下,即使核心线程没有任务在执行它也存在的,我们固定一定数量的核心线程且它一直存活,这样就避免了一般情况下 CPU 创建和销毁线程带来的开销。我们如果将 ThreadPoolExecutor 的 allowCoreThreadTimeOut 属性设置为 true,那么闲置的核心线程就会有超时策略,这个时间由 keepAliveTime 来设定,即 keepAliveTime 时间内如果核心线程没有回应则该线程就会被终止。allowCoreThreadTimeOut 默认为false,核心线程没有超时时间。
  • maximumPoolSize:最大线程数,当任务数量超过最大线程数时其它任务可能就会被阻塞。最大线程数 = 核心线程 + 非核心线程。非核心线程只有当核心线程不够用且线程池有空余时才会被创建,执行完任务后非核心线程会被销毁。
  • keepAliveTime:非核心线程的超时时长,当执行时间超过这个时间时,非核心线程就会被回收。当 allowCoreThreadTimeOut 设置为 true 时,此属性也作用在核心线程上。
  • unit:枚举时间单位,TimeUnit。
  • workQueue:任务队列,我们提交给线程池的 runnable 会被存储在这个对象上。
  • threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。
  • handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略。

线程池的分配遵循这样的规则:

  1. 当 currentSize < corePoolSize 时,直接启动一个核心线程并执行任务。
  2. 当 currentSize >= corePoolSize、并且 workQueue 未满时,添加进来的任务会被安排到 workQueue 中等待执行。
  3. 当 workQueue 已满,但是 currentSize < maximumPoolSize 时,会立即开启一个非核心线程来执行任务。
  4. 当 currentSize >= corePoolSize、workQueue 已满、并且 currentSize > maximumPoolSize 时,那么线程池则拒绝执行该任务,且 ThreadPoolExecutor 会调用 RejectedtionHandler 的 rejectedExecution 方法来通知调用者。

 

二、任务队列


当线程数量大于等于 corePoolSize,workQueue 未满时,则将新任务插入 workQueue。这里要考虑使用什么类型的容器缓存新任务,通过 JDK 文档介绍,我们可知道有 3 种类型的容器可供使用,分别是同步队列,有界队列和无界队列。对于有优先级的任务,这里还可以增加优先级队列。Java 一共为我们提供了7种阻塞队列的实现以:

实现类类型说明
SynchronousQueue同步队列该队列不存储元素,每个插入操作必须等待另一个线程调用移除操作,否则插入操作会一直阻塞
ArrayBlockingQueue有界队列基于数组的阻塞队列,按照 FIFO 原则对元素进行排序
LinkedBlockingQueue无界队列基于链表的阻塞队列,按照 FIFO 原则对元素进行排序
PriorityBlockingQueue优先级队列具有优先级的阻塞队列
DelayQueue 类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。要求元素都实现Delayed接口,通过执行时延从队列中提取任务,时间没到任务取不出来。
LinkedBlockingDeque 使用双向队列实现的有界双端阻塞队列。双端意味着可以像普通队列一样FIFO(先进先出),也可以像栈一样FILO(先进后出)
LinkedTransferQueue 它是ConcurrentLinkedQueue、LinkedBlockingQueue和SynchronousQueue的结合体,但是把它用在ThreadPoolExecutor中,和LinkedBlockingQueue行为一致,但是是无界的阻塞队列。

 

三、线程工厂


线程工厂指定创建线程的方式,需要实现 ThreadFactory 接口,并实现 newThread(Runnable r) 方法。该参数可以不用指定,Executors 框架已经为我们实现了一个默认的线程工厂:

private static class DefaultThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                              Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +
                      poolNumber.getAndIncrement() +
                     "-thread-";
    }

    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                              namePrefix + threadNumber.getAndIncrement(),
                              0);
        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}

 

四、拒绝策略


 线程数量大于等于 maximumPoolSize 且 workQueue 已满,则使用拒绝策略处理新任务。Java 线程池提供了 4 种拒绝策略实现类,如下:

实现类说明
AbortPolicy丢弃新任务,并抛出 RejectedExecutionException
DiscardPolicy不做任何操作,直接丢弃新任务
DiscardOldestPolicy丢弃队列队首的元素,并执行新任务
CallerRunsPolicy由调用线程执行新任务

以上 4 个拒绝策略中,AbortPolicy 是线程池实现类所使用的策略。我们也可以通过下面这个方法修改线程池决绝策略:

setRejectedExecutionHandler(RejectedExecutionHandler) 
 

五、线程池的分类


Java 中为我们提供了四类具有不同特性的线程池分别为 FixThreadPool、CachedThreadPool、ScheduleThreadPool 以及 SingleThreadExecutor。当然,我们也可以通过 ThreadPoolExecutor 自定义线程池。

5.1、FixThreadPool

public static ExecutorService newFixThreadPool(int nThreads){
    return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, 
                        new LinkedBlockingQueue<Runnable>());
}

//使用
Executors.newFixThreadPool(5).execute(r);

从配置参数来看,FixThreadPool 只有核心线程,并且数量固定的,也不会被回收。使用 LinkedBlockingQueue 无界队列,所有线程都活动时,因为队列没有限制大小,新任务会等待执行。由于线程不会回收,FixThreadPool会更快地响应外界请求。

我们可以这么想,FixThreadPool 其实就像一堆人排队上公厕一样,可以无数多人排队,但是厕所位置就那么多,而且没人上时,厕所也不会被拆迁。由于线程不会回收,FixThreadPool 会更快地响应外界请求,这也很容易理解,就好像有人突然想上厕所,公厕不是现用现建的。

5.2、SingleThreadPool

public static ExecutorService newSingleThreadPool (){
    return new FinalizableDelegatedExecutorService ( new ThreadPoolExecutor (1, 1, 0, TimeUnit. MILLISECONDS, 
                        new LinkedBlockingQueue<Runnable>()) );
}

//使用
Executors.newSingleThreadPool ().execute(r);

从配置参数可以看出,SingleThreadPool 只有一个核心线程,确保所有任务都在同一线程中按顺序完成。因此不需要处理线程同步的问题。跟 FixThreadPool 一样,它也是使用 LinkedBlockingQueue 无界队列。它的意义在于,统一所有的外界任务到同一线程中,让调用者可以忽略线程同步问题。

可以把 SingleThreadPool 简单的理解为 FixThreadPool 的参数被手动设置为 1 的情况,即Executors.newFixThreadPool(1).execute(r)。所以 SingleThreadPool 可以理解为公厕里只有一个坑位,先来先上。

5.3、CachedThreadPool

public static ExecutorService newCachedThreadPool(int nThreads){
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit. SECONDS, 
                        new SynchronousQueue<Runnable>());
}

//使用
Executors.newCachedThreadPool().execute(r);

CachedThreadPool 只有非核心线程,最大线程数非常大,所有线程都活动时,会为新任务创建新线程,否则利用空闲线程(60s空闲时间,过了就会被回收,所以线程池中有0个线程的可能)处理任务。采用 SynchronousQueue 同步队列相当于一个空集合,导致任何任务都会被立即执行。比较适合执行大量的耗时较少的任务。

可以这么想,CachedThreadPool 就像是一堆人去一个很大的咖啡馆喝咖啡,里面服务员也很多,随时去,随时都可以喝到咖啡。但是为了响应国家的“光盘行动”,一个人喝剩下的咖啡会被保留60秒,供新来的客人使用。如果你运气好,没有剩下的咖啡,你会得到一杯新咖啡。但是以前客人剩下的咖啡超过60秒,就变质了,会被服务员回收掉。

5.4、ScheduledThreadPool

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize){
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

public ScheduledThreadPoolExecutor(int corePoolSize){
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedQueue ());
}

//使用,延迟1秒执行,每隔2秒执行一次Runnable r
Executors.newScheduledThreadPool(5).scheduleAtFixedRate(r, 1000, 2000, TimeUnit.MILLISECONDS);

核心线程数固定,非核心线程(闲着没活干会被立即回收)数没有限制。从上面代码也可以看出,ScheduledThreadPool 主要用于执行定时任务以及有固定周期的重复任务。

通过Executors的newScheduledThreadPool()方法来创建,ScheduledThreadPool线程池像是上两种的合体,它有数量固定的核心线程,且有数量无限多的非核心线程,但是它的非核心线程超时时间是0s,所以非核心线程一旦空闲立马就会被回收。这类线程池适合用于执行定时任务和固定周期的重复任务。

 

六、关闭线程池


其实无非就是两个方法 shutdown()、shutdownNow()。但他们有着重要的区别:

  • shutdown():执行后停止接受新任务,会把队列的任务执行完毕。将线程池状态变为 SHUTDOWN。
  • shutdownNow():也是停止接受新任务,但会中断所有的任务,将线程池状态变为 STOP。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值