工作三年,小胖问我线程池是什么?真的离谱!

线程池

欢迎来到狗哥多线程系列连载。本篇是线程相关的第六篇,前五篇分别是:

创建线程到底有几种方式?
线程有多少种状态?Runnable 一定在执行任务吗?
万字长文,Thread 类源码解析!
wait、notify/notifyAll 解析
线程之生产者消费者模式

什么是线程池?

线程池是一种池化技术,简单来说就是一个管理线程的池子。这个池子里面的线程数是固定的,当任务数量大于线程数量时,会对线程进行复用。一个线程执行完任务,就回到这等待下一个任务的招唤,也不要你销毁。类似的还有我们工作常接触的数据库连接池。java 中的线程池主要是 juc (java.util.concurrent)包来复制,主要是由 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 类来实现,后者在前者的基础上增加了定时执行的功能。

为什么使用线程池?

那为什么要使用线程池呢?手动创建不香吗?真的不香,手动创建的情景仅仅适合很少任务量的情况。比如:只有一个任务,这问题不大。

public class OneTask {

    public static void main(String[] args) {
        Thread thread0 = new Thread(new Task());
        thread0.start();
    }

    static class Task implements Runnable {

        public void run() {
            System.out.println("Thread Name: " + Thread.currentThread().getName());
        }
    }
    
}

那如果我就是有 10000 个任务呢?要这样写吗?

public class OneTask {

    public static void main(String[] args) {
        for (int i = 0; i < 10000; i++) {
            Thread thread0 = new Thread(new Task());
            thread0.start();
        }
    }

    static class Task implements Runnable {

        public void run() {
            System.out.println("Thread Name: " + Thread.currentThread().getName());
        }
    }

}

运行结果:

Thread Name: Thread-9977
Thread Name: Thread-9975
Thread Name: Thread-9973
Thread Name: Thread-9951
Thread Name: Thread-9999

Excuse me? 创建 10000 个线程?疯了吧?java 是一门高级语言,很多底层的工作对我们来说都是黑盒,比如垃圾自动回收。每一个线程从创建到销毁都要占用资源,用完需要回收的

10000 个线程造成的垃圾回收开销得有多大呀,如果还是需要耗费一定时间的任务呢?要是我的线程任务很简单就是打印个日志,使用线程的内存开销比任务执行本身的开销还要大,这时就会得不偿失。

简而言之,频繁创建线程带来两点很不友好的问题:

  • 反复创建线程系统开销比较大,每个线程创建和销毁都需要时间。
  • 过多的线程会占用过多的内存等资源,还会带来过多的上下文切换,同时还会导致系统不稳定。

但我的任务确实多,咋办?这个时候线程池就出现了,它的出现解决了以上两点问题。

首先,针对反复创建线程开销大的问题,线程池用固定数量的线程保持工作状态并复用

其次,针对过多线程占用太多内存资源的问题,线程池根据需要创建线程,控制线程的总数量,避免占用过多内存资源

java 的线程池

线程池嘛,就是个池子。这里面的线程是固定的且可控的,java 提供了 Executor 接口方便我们实现线程池,它的继承关系是这样的:

Executor.png

其中 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 是实现线程池的两个类,区别上文说过了。

另外,还有个 JDK1.7 才出现的线程池:ForkJoinPool,它适合执行可产生子任务的任务,第一步是拆分也就是 Fork,第二步是汇总也就是 Join。继承关系是这样的(后面再单独出一期专门研究这个线程池)。

ForkJoinPool.png

线程池的执行流程

图源:拉勾教育-Java并发编程.png

1、首先提交任务,检查核心线程池是否已满?满了丢进队列。未满则创建线程执行任务。
2、队列是否已满?满了检查整个线程池是否已满?未满则是添加到队列中排队等待。
3、整个线程池都没可用线程了,直接根据拒绝策略处理新任务。

线程池的参数

找到 ThreadPoolExecutor 的构造方法:

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

它一共有如下7 个参数:

图源:拉勾教育-Java并发编程

ThreadPoolExecutor 构造传入这 7 个参数,就可以创建一个线程池了,下面逐一解释:

1、corePoolSize 是核心线程数,就是指定线程池有多少始终活跃的线程,这个是根据业务需求定的,线程池执行过程的第一步就是检查核心线程数是不是都已经满了。

2、maximumPoolSize 是整个线程池的最大线程数,超出核心线程的部分有空闲,是可以进行回收的。所以正常情况下,线程池中的线程数量会处在 corePoolSize 与 maximumPoolSize 的闭区间内。

二者区别

maximumPoolSize 包含 corePoolSize 和 maximumPoolSize减corePoolSize。他两就像长工和临时工的区别。打个比方外包公司接到大项目,需要 100 个程序员才能搞定,可公司内部就只有 10 个猿。咋办?招 90 个临时的呗,干完活就开掉。原有的 10 个就是长工对应 corePoolSize ,即使没这项目他在公司还有活干。而剩下就 90 个就是临时的,对应 maximumPoolSize - corePoolSize,做完项目就不需要了。残酷吧?

所以,maximumPoolSize = corePoolSize + 临时工

3、keepAliveTime + 时间单位用于定义核心线程以外的线程(临时工,如果有的话)的存活时间,也就是说,这是定义临时工能活多久的参数。

4、ThreadFactory 是线程工厂,用于创建线程。有默认的,也可自定义实现。

5、workQueue 是阻塞队列,也就是暂时存任务的地方。

6、Handler 是拒绝策略,后面专门有一篇文章来探讨。

了解了这 7 个参数,现在我们设定 corePoolSize = 5,maximumPoolSize = 10,阻塞队列长度 = 100。再来看一个动图,你对上面的流程图的理解会更深

图源:拉勾教育 Java 并发编程

有哪 6 种线程池,如何使用?

除了自定义,良心的 java 给我们实现了 3 类,6 个线程池,分别是:

由 ThreadPoolExecutor 创建

  • FixedThreadPool
  • CachedThreadPool
  • SingleThreadExecutor

由 ScheduledThreadPoolExecutor 创建

  • ScheduledThreadPool
  • SingleThreadScheduledExecutor

JDk 1.7 出现

  • ForkJoinPool (原理较复杂,后面再讲)

FixedThreadPool(固定数目的线程池)

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
}
  • 固定线程数的线程池,核心线程数与最大线程数一样。
  • 即使任务数 > 核心线程数,也不会再创建线程,而是扔到队列等待。
  • 队列也满了,那就走拒绝策略。
  • 线程闲置,马上回收。

线程数量固定,比较适用于耗时较长的任务。避免频繁回收和分配线程

执行过程:线程池有 t0 ~ t9 十个线程,他们不断执行任务,期间任务不会减少不会增加,因为核心线程数 = 最大线程数

FixedThreadPool 图源:拉勾教育 Java 并发编程

用法:用它生成 10 个线程,来执行 10000 个任务:

public class MyThreadPoolTest {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10000; i++) {
            executorService.execute(new Thread(new Task()));
        }
        executorService.shutdown();
    }

    static class Task implements Runnable {

        public void run() {
            System.out.println("Thread Name: " + Thread.currentThread().getName());
        }

    }
}

执行结果:可以看到来来去去都是 1~10 这几个线程在跑任务,并没有编号为 11 的线程。

Thread Name: pool-1-thread-1
Thread Name: pool-1-thread-2
Thread Name: pool-1-thread-6
Thread Name: pool-1-thread-8
Thread Name: pool-1-thread-7
Thread Name: pool-1-thread-7
Thread Name: pool-1-thread-1
Thread Name: pool-1-thread-2
Thread Name: pool-1-thread-6
Thread Name: pool-1-thread-10
Thread Name: pool-1-thread-3

CachedThreadPool(可缓存线程的线程池)

上源码:

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
  • 核心线程数 = 0,要是一直没任务线程数就 = 0。
  • 最大线程数是无限增加的(最大可到 Integer.MAX_VALUE,为 2^31-1,基本不可能达到)。
  • 线程数并非固定不变,默认闲置线程超过 60s 没工作,则销毁。
  • 队列是 SynchronousQueue 容量是 0,不存储任务,只做中转。

适用于耗时较短的任务、任务处理速度 > 任务提交速度。就不会造成不断创建新线程

用法:用它提交 10000 个任务,并执行。

public class MyThreadPoolTest {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 10000; i++) {
            executorService.execute(new Thread(new Task()));
        }
        executorService.shutdown();
    }

    static class Task implements Runnable {

        public void run() {
            System.out.println("Thread Name: " + Thread.currentThread().getName());
        }

    }
}

执行结果:只要有任务提交就新建线程执行

Thread Name: pool-1-thread-826
Thread Name: pool-1-thread-827
Thread Name: pool-1-thread-233
Thread Name: pool-1-thread-303
Thread Name: pool-1-thread-321
Thread Name: pool-1-thread-833
Thread Name: pool-1-thread-825
Thread Name: pool-1-thread-832
Thread Name: pool-1-thread-69
Thread Name: pool-1-thread-18
Thread Name: pool-1-thread-830
Thread Name: pool-1-thread-829

SingleThreadExecutor(单线程的线程池)

源码:

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
  • 最大线程数和核心线程都 = 1,有且只有一个线程。

这货有啥使用场景?还真有,比如:用于所有任务都需要按被提交的顺序依次执行的场景

用法:

public class MyThreadPoolTest {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10000; i++) {
            executorService.execute(new Thread(new Task()));
        }
        executorService.shutdown();
    }

    static class Task implements Runnable {

        public void run() {
            System.out.println("Thread Name: " + Thread.currentThread().getName());
        }

    }
}

结果:

Thread Name: pool-1-thread-1
Thread Name: pool-1-thread-1
Thread Name: pool-1-thread-1
Thread Name: pool-1-thread-1
Thread Name: pool-1-thread-1
···

ScheduledThreadPool(定时或周期的线程池)

源码:

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

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

适用场景:定时或周期性执行任务,它有三个重要的方法:

ScheduledExecutorService service = Executors.newScheduledThreadPool(10);

// 延迟指定时间后执行一次任务(这里是 10s 后执行完任务,结束)
service.schedule(new Task(), 10, TimeUnit.SECONDS);

// 以固定的频率执行任务(表示第一次延时后每次延时多长时间执行一次),第二个参数是第一次延迟的时间,第三个参数是周期
service.scheduleAtFixedRate(new Task(), 10, 10, TimeUnit.SECONDS);

// 类似于第二个,区别在于周期的定义。第二个方法的周期是以任务开始时间为起始时间计时,而这个是以任务结束的时间为起始时间
service.scheduleWithFixedDelay(new Task(), 10, 10, TimeUnit.SECONDS);

用法:

public class MyThreadPoolTest {

    public static void main(String[] args) {
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(10);
        for (int i = 0; i < 10000; i++) {
            executorService.schedule(new Thread(new Task()), 10, TimeUnit.SECONDS);
        }
        executorService.shutdown();
    }

    static class Task implements Runnable {

        public void run() {
            System.out.println("Thread Name: " + Thread.currentThread().getName());
        }

    }
}

SingleThreadScheduledExecutor(定时或周期的单线程线程池)

源码:

public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
    return new DelegatedScheduledExecutorService
            (new ScheduledThreadPoolExecutor(1));
}

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

SingleThreadScheduledExecutor 只有一个线程且支持定时、周期功能。很明显是 ScheduledThreadPool 和 SingleThreadExecutor 的结合体。适用于对执行顺序有要求,且需要定时或周期执行的任务

用法:

public class MyThreadPoolTest {

    public static void main(String[] args) {
        ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
        for (int i = 0; i < 10000; i++) {
            executorService.schedule(new Thread(new Task()), 10, TimeUnit.SECONDS);
        }
        executorService.shutdown();
    }

    static class Task implements Runnable {

        public void run() {
            System.out.println("Thread Name: " + Thread.currentThread().getName());
        }

    }
}

巨人的肩膀

总结

本文聊了聊线程池是什么?为什么?怎么用?以及分析了线程池的执行过程,各参数含义、Java 各线程池的使用以及使用场景。相信你看完会有所收获,当然,由于篇幅原因,阻塞队列、拒绝策略等参数后面再发文探讨。另外,如文章有错,请友善指正,感激不尽。

小福利

如果看到这里,喜欢这篇文章的话,请帮点个好看。微信搜索一个优秀的废人,关注后回复电子书送你 100+ 本编程电子书 ,不只 Java 哦,详情看下图。回复 1024送你一套完整的 java 视频教程。

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

推荐阅读更多精彩内容