别再说你不懂线程池——做个优雅的攻城狮

作者:爱撸铁的攻城狮
链接:https://juejin.im/post/5a743c526fb9a063557d7eba
来源:掘金

什么是线程池

线程池,顾名思义就是装线程的池子。其用途是为了帮我们重复管理线程,避免创建大量的线程增加开销,提高响应速度。

为什么要用线程池

作为一个严谨的攻城狮,不会希望别人看到我们的代码就开始吐槽,new Thread().start()会让代码看起来混乱臃肿,并且不好管理和维护,那么我们就需要用到了线程池。
在编程中经常会使用线程来异步处理任务,但是每个线程的创建和销毁都需要一定的开销。如果每次执行一个任务都需要开一个新线程去执行,则这些线程的创建和销毁将消耗大量的资源;并且线程都是“各自为政”的,很难对其进行控制,更何况有一堆的线程在执行。线程池为我们做的,就是线程创建之后为我们保留,当我们需要的时候直接拿来用,省去了重复创建销毁的过程。

线程池的处理逻辑

线程池ThreadPoolExecutor构造函数

//五个参数的构造函数
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue)

//六个参数的构造函数-1
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory)

//六个参数的构造函数-2
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler)

//七个参数的构造函数
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) 

虽然参数多,只是看着吓人,其实很好理解,下面会一一解答。

我们拿最多参数的来说:

1. corePoolSize -> 该线程池中核心线程数最大值

核心线程:默认情况下,会一直存在于线程池中(即使这个线程啥都不干),有任务要执行时,如果核心线程没有被占用,会优先用核心线程执行任务。数量一般情况下设置为CPU核数的二倍即可。

2. maximumPoolSize -> 该线程池中线程总数最大值

线程总数=核心线程数+非核心线程数

非核心线程:简单理解,即核心线程都被占用,但还有任务要做,就创建非核心线程

3. keepAliveTime -> 非核心线程闲置超时时长

这个参数可以理解为,任务少,但池中线程多,非核心线程不能白养着,超过这个时间不工作的就会被干掉,但是核心线程会保留。

4. TimeUnit -> keepAliveTime的单位

TimeUnit是一个枚举类型,其包括:
NANOSECONDS : 1微毫秒 = 1微秒 / 1000
MICROSECONDS : 1微秒 = 1毫秒 / 1000
MILLISECONDS : 1毫秒 = 1秒 /1000
SECONDS : 秒
MINUTES : 分
HOURS : 小时
DAYS : 天

5. BlockingQueue workQueue -> 线程池中的任务队列

默认情况下,任务进来之后先分配给核心线程执行,核心线程如果都被占用,并不会立刻开启非核心线程执行任务,而是将任务插入任务队列等待执行,核心线程会从任务队列取任务来执行,任务队列可以设置最大值,一旦插入的任务足够多,达到最大值,才会创建非核心线程执行任务。

常见的 workQueue 有四种:

  1. SynchronousQueue:这个队列接收到任务的时候,会直接提交给线程处理,而不保留它,如果所有线程都在工作怎么办?那就新建一个线程来处理这个任务!所以为了保证不出现 线程数达到了 maximumPoolSize 而不能新建线程的错误,使用这个类型队列的时候,maximumPoolSize 一般指定成 Integer.MAX_VALUE,即无限大
  2. LinkedBlockingQueue:这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建线程(核心线程)处理任务;如果当前线程数等于核心线程数,则进入队列等待。由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了 maximumPoolSize 的设定失效,因为总线程数永远不会超过 corePoolSize
  3. ArrayBlockingQueue:可以限定队列的长度,接收到任务的时候,如果没有达到 corePoolSize 的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了 maximumPoolSize,并且队列也满了,则发生错误,或是执行实现定义好的饱和策略
  4. DelayQueue:队列内元素必须实现 Delayed 接口,这就意味着你传进去的任务必须先实现 Delayed 接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务

6. ThreadFactory threadFactory -> 创建线程的工厂

可以用线程工厂给每个创建出来的线程设置名字。一般情况下无须设置该参数。

7. RejectedExecutionHandler handler -> 饱和策略

这是当任务队列和线程池都满了时所采取的应对策略,默认是 AbordPolicy, 表示无法处理新任务,并抛出 RejectedExecutionException 异常。此外还有 3 种策略,它们分别如下。

  1. CallerRunsPolicy:用调用者所在的线程来处理任务。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。
  2. DiscardPolicy:不能执行的任务,并将该任务删除。
  3. DiscardOldestPolicy:丢弃队列最近的任务,并执行当前的任务。

别晕,接下来上图,相信结合图你能大彻大悟~


如何使用线程池

说了半天原理,接下来就要用了,java 为我们提供了 4 种线程池 FixedThreadPoolCachedThreadPoolSingleThreadExecutorScheduledThreadPool,几乎可以满足我们大部分的需要了:

1. FixedThreadPool

可重用固定线程数的线程池,超出的线程会在队列中等待,在Executors类中我们可以找到创建方式:

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

FixedThreadPoolcorePoolSizemaximumPoolSize 都设置为参数 nThreads,也就是只有固定数量的核心线程,不存在非核心线程。keepAliveTime0L 表示多余的线程立刻终止,因为不会产生多余的线程,所以这个参数是无效的。FixedThreadPool 的任务队列采用的是 LinkedBlockingQueue

创建线程池的方法,在我们的程序中只需要,后面其他种类的同理:

public static void main(String[] args) {
        // 参数是要线程池的线程最大值
        ExecutorService executorService = Executors.newFixedThreadPool(10);
}

2. CachedThreadPool

CachedThreadPool 是一个根据需要创建线程的线程池

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

CachedThreadPoolcorePoolSize0maximumPoolSizeInt 的最大值,也就是说 CachedThreadPool 没有核心线程,全部都是非核心线程,并且没有上限。keepAliveTime60 秒,就是说空闲线程等待新任务 60 秒,超时则销毁。此处用到的队列是阻塞队列 SynchronousQueue,这个队列没有缓冲区,所以其中最多只能存在一个元素,有新的任务则阻塞等待。

3. SingleThreadExecutor

SingleThreadExecutor 是使用单个线程工作的线程池。其创建源码如下:

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

我们可以看到总线程数和核心线程数都是 1,所以就只有一个核心线程。该线程池才用链表阻塞队列 LinkedBlockingQueue,先进先出原则,所以保证了任务的按顺序逐一进行。

4. ScheduledThreadPool

ScheduledThreadPool 是一个能实现定时和周期性任务的线程池,它的创建源码如下:

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

这里创建了 ScheduledThreadPoolExecutor,继承自 ThreadPoolExecutor,主要用于定时延时或者定期处理任务。ScheduledThreadPoolExecutor 的构造如下:

public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE,
              DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
              new DelayedWorkQueue());
    }

可以看出 corePoolSize 是传进来的固定值,maximumPoolSize 无限大,因为采用的队列 DelayedWorkQueue 是无解的,所以 maximumPoolSize 参数无效。该线程池执行如下:

当执行 scheduleAtFixedRate 或者 scheduleWithFixedDelay 方法时,会向 DelayedWorkQueue 添加一个实现 RunnableScheduledFuture 接口的 ScheduledFutureTask (任务的包装类),并会检查运行的线程是否达到 corePoolSize。如果没有则新建线程并启动 ScheduledFutureTask,然后去执行任务。如果运行的线程达到了 corePoolSize 时,则将任务添加到 DelayedWorkQueue 中。DelayedWorkQueue 会将任务进行排序,先要执行的任务会放在队列的前面。在跟此前介绍的线程池不同的是,当执行完任务后,会将 ScheduledFutureTask 中的 time 变量改为下次要执行的时间并放回到 DelayedWorkQueue 中。

如何合理配置线程池的大小

一般需要根据任务的类型来配置线程池大小:
如果是 CPU 密集型任务,就需要尽量压榨 CPU,参考值可以设为 NCPU + 1
如果是 IO 密集型任务,参考值可以设置为 2 * NCPU
当然,这只是一个参考值,具体的设置还需要根据实际情况进行调整,比如可以先将线程池大小设置为参考值,再观察任务运行情况和系统负载、资源利用率来进行适当调整。

结语

java为我们提供的线程池就介绍到这了,墙裂建议大家还是动手去敲一敲,毕竟实践过心里才有底。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容