从实验的角度理解线程池

今天正好复习到线程池,几个参数看似简单,但是越想越觉得有交差和不解。新建线程池的方法如下,分别是(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. 线程队列递增

    image

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

  1. Max依次递增

    image

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

  1. Core > Max

    image

报错

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

    image

结论:都在核心进程里面执行,和结论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策略。
    这样即使参数再怎么变化,也能顺利的理解了。

作者:森码
链接:https://www.jianshu.com/p/a118b04f04e9

服务推荐

展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 数字20 设计师: CSDN官方博客
应支付0元
点击重新获取
扫码支付

支付成功即可阅读