理解线程池,看这篇足够了。第一篇

线程池原理详解 :

什么是线程池

线程池的概念大家应该都很清楚,帮我们重复管理线程,避免创建大量的线程增加开销。

除了降低开销以外,线程池也可以提高响应速度,了解点 JVM 的同学可能知道,一个对象的创建大概需要经过以下几步:

检查对应的类是否已经被加载、解析和初始化

类加载后,为新生对象分配内存

将分配到的内存空间初始为 0

对对象进行关键信息的设置,比如对象的哈希码等

然后执行 init 方法初始化对象

创建一个对象的开销需要经过这么多步,也是需要时间的嘛,那可以复用已经创建好的线程的线程池,自然也在提高响应速度上做了贡献。

线程池的处理流程

创建线程池需要使用 ThreadPoolExecutor 类,它的构造函数参数如下:

public ThreadPoolExecutor(int corePoolSize,    //核心线程的数量

  int maximumPoolSize,    //最大线程数量

  long keepAliveTime,    //超出核心线程数量以外的线程空余存活时间

  TimeUnit unit,    //存活时间的单位

  BlockingQueue<Runnable> workQueue,    //保存待执行任务的队列

  ThreadFactory threadFactory,    //创建新线程使用的工厂

  RejectedExecutionHandler handler // 当任务无法执行时的处理器

  ) {...}

参数介绍如注释所示,要了解这些参数左右着什么,就需要了解线程池具体的执行方法ThreadPoolExecutor.execute:

public void execute(Runnable command) {

if (command == null)

throw new NullPointerException();

int c = ctl.get();

//1.当前池中线程比核心数少,新建一个线程执行任务

if (workerCountOf(c) < corePoolSize) { 

if (addWorker(command, true))

return;

c = ctl.get();

}

//2.核心池已满,但任务队列未满,添加到队列中

if (isRunning(c) && workQueue.offer(command)) { 

int recheck = ctl.get();

if (! isRunning(recheck) && remove(command))    //如果这时被关闭了,拒绝任务

reject(command);

else if (workerCountOf(recheck) == 0)    //如果之前的线程已被销毁完,新建一个线程

addWorker(null, false);

}

//3.核心池已满,队列已满,试着创建一个新线程

else if (!addWorker(command, false))

reject(command);    //如果创建新线程失败了,说明线程池被关闭或者线程池完全满了,拒绝任务

}

可以看到,线程池处理一个任务主要分三步处理,代码注释里已经介绍了,我再用通俗易懂的例子解释一下:

(线程比作员工,线程池比作一个团队,核心池比作团队中核心团队员工数,核心池外的比作外包员工)

有了新需求,先看核心员工数量超没超出最大核心员工数,还有名额的话就新招一个核心员工来做

需要获取全局锁

核心员工已经最多了,HR 不给批 HC 了,那这个需求只好攒着,放到待完成任务列表吧

如果列表已经堆满了,核心员工基本没机会搞完这么多任务了,那就找个外包吧

需要获取全局锁

如果核心员工 + 外包员工的数量已经是团队最多能承受人数了,没办法,这个需求接不了了

结合这张图,这回流程你明白了吗?

这里写图片描述

由于 1 和 3 新建线程时需要获取全局锁,这将严重影响性能。因此 ThreadPoolExecutor 这样的处理流程是为了在执行 execute() 方法时尽量少地执行 1 和 3,多执行 2。

在 ThreadPoolExecutor 完成预热后(当前线程数不少于核心线程数),几乎所有的 execute() 都是在执行步骤 2。

前面提到的 ThreadPoolExecutor 构造函数的参数,分别影响以下内容:

corePoolSize:核心线程池数量

在线程数少于核心数量时,有新任务进来就新建一个线程,即使有的线程没事干

等超出核心数量后,就不会新建线程了,空闲的线程就得去任务队列里取任务执行了

maximumPoolSize:最大线程数量

包括核心线程池数量 + 核心以外的数量

如果任务队列满了,并且池中线程数小于最大线程数,会再创建新的线程执行任务

keepAliveTime:核心池以外的线程存活时间,即没有任务的外包的存活时间

如果给线程池设置 allowCoreThreadTimeOut(true),则核心线程在空闲时头上也会响起死亡的倒计时

如果任务是多而容易执行的,可以调大这个参数,那样线程就可以在存活的时间里有更大可能接受新任务

workQueue:保存待执行任务的阻塞队列

不同的任务类型有不同的选择,下一小节介绍

threadFactory:每个线程创建的地方

可以给线程起个好听的名字,设置个优先级啥的

handler:饱和策略,大家都很忙,咋办呢,有四种策略

CallerRunsPolicy:只要线程池没关闭,就直接用调用者所在线程来运行任务

AbortPolicy:直接抛出 RejectedExecutionException 异常

DiscardPolicy:悄悄把任务放生,不做了

DiscardOldestPolicy:把队列里待最久的那个任务扔了,然后再调用 execute() 试试看能行不

我们也可以实现自己的 RejectedExecutionHandler 接口自定义策略,比如如记录日志什么的

保存待执行任务的阻塞队列

当线程池中的核心线程数已满时,任务就要保存到队列中了。

线程池中使用的队列是 BlockingQueue 接口,常用的实现有如下几种:

ArrayBlockingQueue:基于数组、有界,按 FIFO(先进先出)原则对元素进行排序

LinkedBlockingQueue:基于链表,按FIFO (先进先出) 排序元素

吞吐量通常要高于 ArrayBlockingQueue

Executors.newFixedThreadPool() 使用了这个队列

SynchronousQueue:不存储元素的阻塞队列

每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态

吞吐量通常要高于 LinkedBlockingQueue

Executors.newCachedThreadPool使用了这个队列

PriorityBlockingQueue:具有优先级的、无限阻塞队列

关于阻塞队列的详细介绍请看这篇:

创建自己的线程池

了解上面的内容后,我们就可以创建自己的线程池了。

①先定义线程池的几个关键属性的值:

private static final int CORE_POOL_SIZE = Runtime.getRuntime().availableProcessors() * 2; // 核心线程数为 CPU 数*2

private static final int MAXIMUM_POOL_SIZE = 64;    // 线程池最大线程数

private static final int KEEP_ALIVE_TIME = 1;    // 保持存活时间 1秒

1

设置核心池的数量为 CPU 数的两倍,一般是 4、8,好点的 16 个线程

最大线程数设置为 64

空闲线程的存活时间设置为 1 秒

②然后根据处理的任务类型选择不同的阻塞队列

如果是要求高吞吐量的,可以使用 SynchronousQueue 队列;如果对执行顺序有要求,可以使用 PriorityBlockingQueue;如果最大积攒的待做任务有上限,可以使用 LinkedBlockingQueue。

private final BlockingQueue<Runnable> mWorkQueue = new LinkedBlockingQueue<>(128);

1

1

③然后创建自己的 ThreadFactory

在其中为每个线程设置个名称:

private final ThreadFactory DEFAULT_THREAD_FACTORY = new ThreadFactory() {

private final AtomicInteger mCount = new AtomicInteger(1);

public Thread newThread(Runnable r) {

Thread thread = new Thread(r, TAG + " #" + mCount.getAndIncrement());

thread.setPriority(Thread.NORM_PRIORITY);

return thread;

}

};

1

④然后就可以创建线程池了

private ThreadPoolExecutor mExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME,

TimeUnit.SECONDS, mWorkQueue, DEFAULT_THREAD_FACTORY,

new ThreadPoolExecutor.DiscardOldestPolicy());

这里我们选择的饱和策略为 DiscardOldestPolicy,你可以可以创建自己的。

⑤完整代码:

public class ThreadPoolManager {

private final String TAG = this.getClass().getSimpleName();

private static final int CORE_POOL_SIZE = Runtime.getRuntime().availableProcessors() * 2; // 核心线程数为 CPU数*2

private static final int MAXIMUM_POOL_SIZE = 64;    // 线程队列最大线程数

private static final int KEEP_ALIVE_TIME = 1;    // 保持存活时间 1秒

private final BlockingQueue<Runnable> mWorkQueue = new LinkedBlockingQueue<>(128);

private final ThreadFactory DEFAULT_THREAD_FACTORY = new ThreadFactory() {

private final AtomicInteger mCount = new AtomicInteger(1);

public Thread newThread(Runnable r) {

Thread thread = new Thread(r, TAG + " #" + mCount.getAndIncrement());

thread.setPriority(Thread.NORM_PRIORITY);

return thread;

}

};

private ThreadPoolExecutor mExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME,

TimeUnit.SECONDS, mWorkQueue, DEFAULT_THREAD_FACTORY,

new ThreadPoolExecutor.DiscardOldestPolicy());

private static volatile ThreadPoolManager mInstance = new ThreadPoolManager();

public static ThreadPoolManager getInstance() {

return mInstance;

}

public void addTask(Runnable runnable) {

mExecutor.execute(runnable);

}

@Deprecated

public void shutdownNow() {

mExecutor.shutdownNow();

}

}

这样我们就有了自己的线程池。

JDK 提供的线程池及使用场景

JDK 为我们内置了五种常见线程池的实现,均可以使用 Executors 工厂类创建。

1.newFixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads) {

return new ThreadPoolExecutor(nThreads, nThreads,

  0L, TimeUnit.MILLISECONDS,

  new LinkedBlockingQueue<Runnable>());

}

1

不招外包,有固定数量核心成员的正常互联网团队。

可以看到,FixedThreadPool 的核心线程数和最大线程数都是指定值,也就是说当线程池中的线程数超过核心线程数后,任务都会被放到阻塞队列中。

此外 keepAliveTime 为 0,也就是多余的空余线程会被立即终止(由于这里没有多余线程,这个参数也没什么意义了)。

而这里选用的阻塞队列是 LinkedBlockingQueue,使用的是默认容量 Integer.MAX_VALUE,相当于没有上限。

因此这个线程池执行任务的流程如下:

线程数少于核心线程数,也就是设置的线程数时,新建线程执行任务

线程数等于核心线程数后,将任务加入阻塞队列

由于队列容量非常大,可以一直加加加

执行完任务的线程反复去队列中取任务执行

FixedThreadPool 用于负载比较重的服务器,为了资源的合理利用,需要限制当前线程数量。

2.newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {

return new FinalizableDelegatedExecutorService

(new ThreadPoolExecutor(1, 1,

0L, TimeUnit.MILLISECONDS,

new LinkedBlockingQueue<Runnable>()));

}

1

不招外包,只有一个核心成员的创业团队。

从参数可以看出来,SingleThreadExecutor 相当于特殊的 FixedThreadPool,它的执行流程如下:

线程池中没有线程时,新建一个线程执行任务

有一个线程以后,将任务加入阻塞队列,不停加加加

唯一的这一个线程不停地去队列里取任务执行

听起来很可怜的样子 - -。

SingleThreadExecutor 用于串行执行任务的场景,每个任务必须按顺序执行,不需要并发执行。

3.newCachedThreadPool

public static ExecutorService newCachedThreadPool() {

return new ThreadPoolExecutor(0, Integer.MAX_VALUE,

  60L, TimeUnit.SECONDS,

  new SynchronousQueue<Runnable>());

}

1

全部外包,没活最多待 60 秒的外包团队。

可以看到,CachedThreadPool 没有核心线程,非核心线程数无上限,也就是全部使用外包,但是每个外包空闲的时间只有 60 秒,超过后就会被回收。

CachedThreadPool 使用的队列是 SynchronousQueue,这个队列的作用就是传递任务,并不会保存。

因此当提交任务的速度大于处理任务的速度时,每次提交一个任务,就会创建一个线程。极端情况下会创建过多的线程,耗尽 CPU 和内存资源。

它的执行流程如下:

没有核心线程,直接向 SynchronousQueue 中提交任务

如果有空闲线程,就去取出任务执行;如果没有空闲线程,就新建一个

执行完任务的线程有 60 秒生存时间,如果在这个时间内可以接到新任务,就可以继续活下去,否则就拜拜

由于空闲 60 秒的线程会被终止,长时间保持空闲的 CachedThreadPool 不会占用任何资源。

CachedThreadPool 用于并发执行大量短期的小任务,或者是负载较轻的服务器。

4.newScheduledThreadPool

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {

return new ScheduledThreadPoolExecutor(corePoolSize);

}

public ScheduledThreadPoolExecutor(int corePoolSize) {

super(corePoolSize, Integer.MAX_VALUE,

  DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,

  new DelayedWorkQueue());

}

private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;

1

定期维护的 2B 业务团队,核心与外包成员都有。

ScheduledThreadPoolExecutor 继承自 ThreadPoolExecutor, 最多线程数为 Integer.MAX_VALUE ,使用 DelayedWorkQueue 作为任务队列。

ScheduledThreadPoolExecutor 添加任务和执行任务的机制与ThreadPoolExecutor 有所不同。

ScheduledThreadPoolExecutor 添加任务提供了另外两个方法:

scheduleAtFixedRate() :按某种速率周期执行

scheduleWithFixedDelay():在某个延迟后执行

它俩的代码如下:

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,

  long initialDelay,

  long period,

  TimeUnit unit) {

if (command == null || unit == null)

throw new NullPointerException();

if (period <= 0L)

throw new IllegalArgumentException();

ScheduledFutureTask<Void> sft =

new ScheduledFutureTask<Void>(command,

  null,

  triggerTime(initialDelay, unit),

  unit.toNanos(period),

  sequencer.getAndIncrement());

RunnableScheduledFuture<Void> t = decorateTask(command, sft);

sft.outerTask = t;

delayedExecute(t);

return t;

}

public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,

long initialDelay,

long delay,

TimeUnit unit) {

if (command == null || unit == null)

throw new NullPointerException();

if (delay <= 0L)

throw new IllegalArgumentException();

ScheduledFutureTask<Void> sft =

new ScheduledFutureTask<Void>(command,

  null,

  triggerTime(initialDelay, unit),

  -unit.toNanos(delay),

  sequencer.getAndIncrement());

RunnableScheduledFuture<Void> t = decorateTask(command, sft);

sft.outerTask = t;

delayedExecute(t);

return t;

}

1

可以看到,这两种方法都是创建了一个 ScheduledFutureTask 对象,调用 decorateTask() 方法转成 RunnableScheduledFuture 对象,然后添加到队列中。

看下 ScheduledFutureTask 的主要属性:

private class ScheduledFutureTask<V>

extends FutureTask<V> implements RunnableScheduledFuture<V> {

//添加到队列中的顺序

private final long sequenceNumber;

//何时执行这个任务

private volatile long time;

//执行的间隔周期

private final long period;

//实际被添加到队列中的 task

RunnableScheduledFuture<V> outerTask = this;

//在 delay queue 中的索引,便于取消时快速查找

int heapIndex;

//...

}

15

DelayQueue 中封装了一个优先级队列,这个队列会对队列中的 ScheduledFutureTask 进行排序,两个任务的执行 time 不同时,time 小的先执行;否则比较添加到队列中的顺序 sequenceNumber ,先提交的先执行。

ScheduledThreadPoolExecutor 的执行流程如下:

调用上面两个方法添加一个任务

线程池中的线程从 DelayQueue 中取任务

然后执行任务

具体执行任务的步骤也比较复杂:

线程从 DelayQueue 中获取 time 大于等于当前时间的 ScheduledFutureTask

DelayQueue.take()

执行完后修改这个 task 的 time 为下次被执行的时间

然后再把这个 task 放回队列中

DelayQueue.add()

ScheduledThreadPoolExecutor 用于需要多个后台线程执行周期任务,同时需要限制线程数量的场景。

两种提交任务的方法

ExecutorService 提供了两种提交任务的方法:

execute():提交不需要返回值的任务

submit():提交需要返回值的任务

execute

void execute(Runnable command);

1

1

execute() 的参数是一个 Runnable,也没有返回值。因此提交后无法判断该任务是否被线程池执行成功。

ExecutorService executor = Executors.newCachedThreadPool();

executor.execute(new Runnable() {

@Override

public void run() {

//do something

}

});

submit

<T> Future<T> submit(Callable<T> task);

<T> Future<T> submit(Runnable task, T result);

Future<?> submit(Runnable task);

submit() 有三种重载,参数可以是 Callable 也可以是 Runnable。

同时它会返回一个 Funture 对象,通过它我们可以判断任务是否执行成功。

获得执行结果调用 Future.get() 方法,这个方法会阻塞当前线程直到任务完成。

提交一个 Callable 任务时,需要使用 FutureTask 包一层:

FutureTask futureTask = new FutureTask(new Callable<String>() {    //创建 Callable 任务

@Override

public String call() throws Exception {

String result = "";

//do something

return result;

}

});

Future<?> submit = executor.submit(futureTask);    //提交到线程池

try {

Object result = submit.get();    //获取结果

} catch (InterruptedException e) {

e.printStackTrace();

} catch (ExecutionException e) {

e.printStackTrace();

}

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