从实验的角度理解线程池

今天正好复习到线程池,几个参数看似简单,但是越想越觉得有交差和不解。新建线程池的方法如下,分别是(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,handler),通过这几个参数的"相互作用"来从新认识线程池的工作方式。

ThreadPoolExecutor executorService = new ThreadPoolExecutor(
                1, 1,
                1L, TimeUnit.SECONDS,
                new ArrayBlockingQueue<Runnable>(1),
                new ThreadPoolExecutor.DiscardPolicy());//这里为了避免报错,使用DiscardPolicy,默认的Reject是AbortPolicy在队列已满时会报错
问题

首先我们提出这几个问题

  1. 如果core > max 会怎么样?
  2. 如果core > Queue.length 会发生什么?
  3. 如果max > Queue.length 线程池怎么处理?
  4. 如果core = max = Queue.length 线程池如何处理?
  5. 正常情况下的赋值。
测试

测试代码如下

  public static void main(String[] args) {
    ThreadPoolExecutor executorService = new ThreadPoolExecutor(
            1, 1,
            1L, TimeUnit.SECONDS,
            new ArrayBlockingQueue<Runnable>(1),
            new ThreadPoolExecutor.DiscardPolicy());

    executorService.execute(new Runnable() {
        @Override
        public void run() {
            System.out.println("start runnable  1  " + Thread.currentThread());
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("runnable 1 finish  " + Thread.currentThread());
        }
    });

    for (int i = 2; i < 6; i++) {
        final int finalI = i;
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("start runnable  " + finalI + "  " + Thread.currentThread());
            }
        });
    }

    executorService.shutdown();

    System.out.println("main finish ------ " + Thread.currentThread());
}

大家最好自己跑一下,有一个直观的认识,这里用表格表达的也不甚明了,其实如果用动图会好很多,不过自己太懒了=。=

  1. 线程队列递增


    线程队列依次增加

很明显的可以看出,线程池中只有 1 个线程,线程依次执行(并且是在线程1结束之后才开始的),队列越长能执行的线程越多,被舍弃的线程也就越少。

  1. Max依次递增


    Max依次递增

结论:Max的值规定了线程池中线程的上限,其实并不只是有一个Core线程就只能跑一个线程,当Queue里面放不下的时候会开启非核心线程来跑这个『意外』的任务,而且与核心线程无关(这些线程不像图1中那样等待了线程1)

  1. Core > Max


    Core>Max

报错

  1. Core依次递增(Core不能大于Max,所以Max也增加了)


    Core递增

结论:都在核心进程里面执行,和结论2类似

猜想

一直以来,我都是从字面上认为线程池的作用,Core即为能运行最多的线程量,Max就是超过Core需要排队的那一部分,而Queue在我的臆想一直都是无限的。这就造成了一个非常狭隘的思维,对设计者的意图没有思考,对底层的代码没有研究。

这里大家可以好好想一下线程池的工作方式,Core、Max与Queue的相互关系(想清楚这个,其他几个参数也能手到擒来)。

在这里我们从上面的结论再次猜想一下,Core自然是核心线程的数量,当核心线程没有满时,无论线程1是否闲置,都会创建一个新的线程,并且这些线程可以重用(这里就有一个存活时间的思考了);Max是线程池中可以存在的最大线程量,当超过Core线程数且小于Max时,这时线程池会新建一些线程来处理这些『来不及』处理的任务(来源于测试2),同时,Core不能大于Max(这是为什么呢?);作为一个队列,它担任着『缓存队列』的任务,像普通的队列一样,最多能存储多少就存储多少。

验证

当然是从源码角度

   public void execute(Runnable command) {
        if (command == null)// 1
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {// 2
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {// 3
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))// 4
                reject(command);
            else if (workerCountOf(recheck) == 0)// 5
                addWorker(null, false);
        } else if (!addWorker(command, false)) {
            reject(command);// 6
        }
    }

首先,我觉得在看源码时应该明确自己的目的,这样在理解时更有针对性;其次,在阅读时一定不要纠结某一个地方,有时候看的代码成百上千行,不可能把每一个地方都看懂,比如在execute方法中就有位运算的使用、链表的操作还有addWorker一个更复杂的方法,不是说深究没有必要,而是去把这个方法当做某个变量去理解,我们开车是要前往目的地,而不是要了解车的构造。

列出几个方法的作用,更利于理解

ctl.get() //凡事ctl有关的操作其实都是位运算的使用,这里有兴趣的可以去查一下,并不难。
          //这里我们只要知道,它像一个int一样,1代表一个状态,2代表另一个状态。
workerCountOf(c);          //这里都是来取ctl保存的状态,就是字面意思上的,(Core线程)的运行数量
isRunning(c);              //(线程)是否正在运行
addWorker(commad,boolean); //创建一个新线程(重要),boolean表示创建的是核心线程还是非核心线程
workQueue.offer(command)   // 将线程加入到队列中(其实就是链表操作)
reject(command);           // 执行拒绝策略

有了以上的准备,我们在理解时就比较容易了。

  1. 检查新线程是否为空。
  2. 获取线程池状态c,如果正在运行的Core线程小于预定值则创建一个新线程执行。(即使有空闲线程,也会创建新的)
  3. 如果已经超过了Core线程数,检查线程是否正在运行,同时加入线程队列成功。(如测试1)
  4. 双重检查,如果线程恰好执行完毕了,要从阻塞队列中移除该线程。
  5. 如果没有核心线程运行,创建非核心线程运行该任务。(如测试2)
  6. 没有加入到核心线程,创建非核心线程也失败了(如测试1)执行拒绝策略。

这里相信大家对线程池都有了一定的自己的理解了,有什么问题欢迎提出一起讨论进步。


2019年11月06日22:03:13 补充

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);

可以从线程池的核心代码去理解

  1. 当没有超过Core Size时,始终会创建新线程。
  2. 当超过核心线程,但是可以加入到缓存队列时,不会创建新线程。如果当前线程池没有运行,会尝试启动(addWorker)。
  3. 当超过Core Size,同时Max Size有余量时,会尝试创建新线程,并且会在之后复用,否则执行Rejct策略。
    这样即使参数再怎么变化,也能顺利的理解了。

🌰源码
如何优雅的使用和理解线程池

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

推荐阅读更多精彩内容

  • iOS多线程编程 基本知识 1. 进程(process) 进程是指在系统中正在运行的一个应用程序,就是一段程序的执...
    陵无山阅读 5,902评论 1 14
  • 前言 线程池是Java中的一个重要概念,从Android上来说,当我们跟服务端进行数据交互的时候我们都知道主线程不...
    老实任阅读 1,221评论 1 9
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,034评论 1 32
  • 本文已独家授权 鸿洋( hongyangAndroid) 公众号发布! 前言: 本篇文章主要介绍的是Java(...
    骑小猪看流星阅读 32,029评论 36 340
  • 最全的iOS面试题及答案 iOS面试小贴士 ———————————————回答好下面的足够了-----------...
    zweic阅读 2,566评论 0 73