Java 线程池

什么是 Executor 框架?

Executor 框架,是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。

无限制的创建线程,会引起应用程序内存溢出。所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。利用 Executor 框架,可以非常方便的创建一个线程池。

为什么使用 Executor 框架?

  1. 每次执行任务创建线程 new Thread() 比较消耗性能,创建一个线程是比较耗时、耗资源的。
  2. 调用 new Thread() 创建的线程缺乏管理,被称为野线程,而且可以无限制的创建,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪,还有线程之间的频繁交替也会消耗很多系统资源。
  3. 接使用 new Thread() 启动的线程不利于扩展,比如定时执行、定期执行、定时定期执行、线程中断等都不便实现。

在 Java 中 Executor 和 Executors 的区别?

  • Executors 是 Executor 的工具类,不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
  • Executor 接口对象,能执行我们的线程任务。
    • ExecutorService 接口,继承了 Executor 接口,并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。
      • 使用 ThreadPoolExecutor ,可以创建自定义线程池。
    • Future 表示异步计算的结果,他提供了检查计算是否完成的方法,以等待计算的完成,并可以使用 #get() 方法,获取计算的结果。

创建线程池的几种方式?

Java 类库提供一个灵活的线程池以及一些有用的默认配置,我们可以通过Executors 的静态方法来创建线程池。

Executors 创建的线程池,分成普通任务线程池,和定时任务线程池。

  • 普通任务线程池
    • 1、#newFixedThreadPool(int nThreads) 方法,创建一个固定长度的线程池。
      • 每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化。
      • 当线程发生未预期的错误而结束时,线程池会补充一个新的线程。
    • 2、#newCachedThreadPool() 方法,创建一个可缓存的线程池。
      • 如果线程池的规模超过了处理需求,将自动回收空闲线程。
      • 当需求增加时,则可以自动添加新线程。线程池的规模不存在任何限制。
    • 3、#newSingleThreadExecutor() 方法,创建一个单线程的线程池。
      • 它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它。
      • 它的特点是,能确保依照任务在队列中的顺序来串行执行。
  • 定时任务线程池
    • 4、#newScheduledThreadPool(int corePoolSize) 方法,创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似 Timer 。
    • 5、#newSingleThreadExecutor() 方法,创建了一个固定长度为 1 的线程池,而且以延迟或定时的方式来执行任务,类似 Timer 。

如何使用 ThreadPoolExecutor 创建线程池?

Executors 提供了创建线程池的常用模板,实际场景下,我们可能需要自动以更灵活的线程池,此时就需要使用 ThreadPoolExecutor 类。

// ThreadPoolExecutor.java

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 参数,核心线程数大小,当线程数 < corePoolSize ,会创建线程执行任务。
  • maximumPoolSize 参数,最大线程数, 当线程数 >= corePoolSize 的时候,会把任务放入 workQueue 队列中。
    • keepAliveTime 参数,保持存活时间,当线程数大于 corePoolSize 的空闲线程能保持的最大时间。
    • unit 参数,时间单位。
  • workQueue 参数,保存任务的阻塞队列。
    • handler 参数,超过阻塞队列的大小时,使用的拒绝策略。
  • threadFactory 参数,创建线程的工厂。

ThreadPoolExecutor 有哪些拒绝策略?

ThreadPoolExecutor 默认有四个拒绝策略:

  • ThreadPoolExecutor.AbortPolicy() ,直接抛出异常 RejectedExecutionException 。
  • ThreadPoolExecutor.CallerRunsPolicy() ,直接调用 run 方法并且阻塞执行。
  • ThreadPoolExecutor.DiscardPolicy() ,直接丢弃后来的任务。
  • ThreadPoolExecutor.DiscardOldestPolicy() ,丢弃在队列中队首的任务。

如果我们有需要,可以自己实现 RejectedExecutionHandler 接口,实现自定义的拒绝逻辑。当然,绝大多数是不需要的。

线程池的关闭方式有几种?

ThreadPoolExecutor 提供了两个方法,用于线程池的关闭,分别是:

  • #shutdown() 方法,不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务。
  • #shutdownNow() 方法,立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。

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

  • IO密集型或者存在阻塞操作的任务,则需要设置大一点的线程数,有一些博客上会写到IO密集型设置2N+1的线程数,其实是不正确的,实际上针对IO密集的任务,需要看真正的线程CPU使用时间和线程等待时间的比例.

书中给了一个公式:给定N=CPU的核数,U=目标CPU使用率(0<=U<=1),W/C = 线程等待时间/线程CPU使用时间。一个合理的线程池数量=N * U * ( 1 + W/C)。
我们在开发过程中遇到的最多的IO密集型任务就是远程接口调用了,一般来说,远程接口调用,W/C的比例一般在9以上,即线程等待时间达到90%,cpu时间占10%,因为绝大部分时间都在网络通信上,如果按照W/C = 90/10的话,针对一个4核CPU的计算机,如果我们期望CPU的使用率达到100%(一般我们都希望100%压榨cpu的性能),也就是U=1,则合理的线程数=4 * 1 * (1 + 90/10)= 40。

Java 线程池大小为何会大多被设置成 CPU 核心数 +1 ?

一般说来,大家认为线程池的大小经验值应该这样设置:(其中 N 为CPU的个数)

  • 如果是 CPU 密集型应用,则线程池大小设置为 N+1

    因为 CPU 密集型任务使得 CPU 使用率很高,若开过多的线程数,只能增加上下文切换的次数,因此会带来额外的开销。(N指CPU的核数,理想情况下设置N个线程是对CPU使用的效率最高的,但是即使是计算密集型的任务,也有可能因为一些其他原因中断导致线程切换,所以额外设置一个线程)

  • 如果是 IO 密集型应用,则线程池大小设置为 2N+1

    IO密集型任务 CPU 使用率并不高,因此可以让 CPU 在等待 IO 的时候去处理别的任务,充分利用 CPU 时间。
    其实是不正确的,实际上针对IO密集的任务,需要看真正的线程CPU使用时间和线程等待时间的比例.
    书中给了一个公式:给定N=CPU的核数,U=目标CPU使用率(0<=U<=1),W/C = 线程等待时间/线程CPU使用时间。一个合理的线程池数量=N * U * ( 1 + W/C)。
    我们在开发过程中遇到的最多的IO密集型任务就是远程接口调用了,一般来说,远程接口调用,W/C的比例一般在9以上,即线程等待时间达到90%,cpu时间占10%,因为绝大部分时间都在网络通信上,如果按照W/C = 90/10的话,针对一个4核CPU的计算机,如果我们期望CPU的使用率达到100%(一般我们都希望100%压榨cpu的性能),也就是U=1,则合理的线程数=4 * 1 * (1 + 90/10)= 40。

  • 如果是混合型应用,那么分别创建线程池

    可以将任务分成 IO 密集型和 CPU 密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。

    因为如果划分之后两个任务执行时间相差甚远,那么先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。

线程池容量的动态调整?

ThreadPoolExecutor 提供了动态调整线程池容量大小的方法:

  • setCorePoolSize:设置核心池大小。
  • setMaximumPoolSize:设置线程池最大能创建的线程数目大小。

当上述参数从小变大时,ThreadPoolExecutor 进行线程赋值,还可能立即创建新的线程来执行任务。

什么是 Callable、Future、FutureTask ?

1)Callable

Callable 接口,类似于 Runnable ,从名字就可以看出来了,但是Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。

简单来说,可以认为是带有回调的 Runnable 。

2)Future

Future 接口,表示异步任务,是还没有完成的任务给出的未来结果。所以说 Callable 用于产生结果,Future 用于获取结果。

3)FutureTask

在 Java 并发程序中,FutureTask 表示一个可以取消的异步运算。

  • 它有启动和取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。
  • 一个 FutureTask 对象,可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是继承了 Runnable 接口,所以它可以提交给 Executor 来执行。

线程池执行任务的过程?

image.png

刚创建时,里面没有线程调用 execute() 方法,添加任务时:

  1. 如果正在运行的线程数量小于核心参数 corePoolSize ,继续创建线程运行这个任务
    • 否则,如果正在运行的线程数量大于或等于 corePoolSize ,将任务加入到阻塞队列中。
    • 否则,如果队列已满,同时正在运行的线程数量小于核心参数 maximumPoolSize ,继续创建线程运行这个任务。
    • 否则,如果队列已满,同时正在运行的线程数量大于或等于 maximumPoolSize ,根据设置的拒绝策略处理。
  2. 完成一个任务,继续取下一个任务处理。
    • 没有任务继续处理,线程被中断或者线程池被关闭时,线程退出执行,如果线程池被关闭,线程结束。
    • 否则,判断线程池正在运行的线程数量是否大于核心线程数,如果是,线程结束,否则线程阻塞。因此线程池任务全部执行完成后,继续留存的线程池大小为 corePoolSize

线程池中 submit 和 execute 方法有什么区别?

两个方法都可以向线程池提交任务。

  • #execute(...) 方法,返回类型是 void ,它定义在 Executor 接口中。
  • #submit(...) 方法,可以返回持有计算结果的 Future 对象,它定义在 ExecutorService 接口中,它扩展了 Executor 接口,其它线程池类像 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 都有这些方法。

如果你提交任务时,线程池队列已满,这时会发生什么?

重点在于线程池的队列是有界还是无界的。

  • 如果你使用的 LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务。
  • 如果你使用的是有界队列比方说 ArrayBlockingQueue 的话,任务首先会被添加到 ArrayBlockingQueue 中,ArrayBlockingQueue满了,则会使用拒绝策略 RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy 。

Fork/Join 框架是什么?

可能了解的人不多,我也是。大体知道就好。

Oracle 的官方给出的定义是:Fork/Join 框架是一个实现了 ExecutorService接口 的多线程处理器。它可以把一个大的任务划分为若干个小的任务并发执行,充分利用可用的资源,进而提高应用的执行效率。

我们再通过 Fork 和 Join 这两个单词来理解下 Fork/Join 框架。

  • Fork 就是把一个大任务切分为若干子任务并行的执行,Join 就是合并这些子任务的执行结果,最后得到这个大任务的结果。
  • 比如计算 1+2+...+10000 ,可以分割成 10 个子任务,每个子任务分别对 1000 个数进行求和,最终汇总这 10 个子任务的结果。

感兴趣的胖友,可以看看如下文章:

如何让一段程序并发的执行,并最终汇总结果?

  • 1、CountDownLatch:允许一个或者多个线程等待前面的一个或多个线程完成,构造一个 CountDownLatch 时指定需要 countDown 的点的数量,每完成一点就 countDown 一下。当所有点都完成,CountDownLatch 的 #await() 就解除阻塞。

  • 2、CyclicBarrier:可循环使用的 Barrier ,它的作用是让一组线程到达一个 Barrier 后阻塞,直到所有线程都到达 Barrier 后才能继续执行。

    CountDownLatch 的计数值只能使用一次,CyclicBarrier 可以通过使用 reset 重置,还可以指定到达栅栏后优先执行的任务。

  • 3、Fork/Join 框架,fork 把大任务分解成多个小任务,然后汇总多个小任务的结果得到最终结果。使用一个双端队列,当线程空闲时从双端队列的另一端领取任务。

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

推荐阅读更多精彩内容