线程池的简要部分分析

个人学习阶段性心得体会,不保证全对,各位看官合理取舍,不适合对线程池较为陌生的老爷们阅读
适合的看官群体:

  • 对线程池有一定的使用经验的老爷们
  • 想研究一下线程池源码的老爷们
  • 喜欢和自己较劲的老爷们

池化技术

最近在研究池化技术,实际上在java中池化技术是一种很常见的技术,除了声名在外的线程池之外,还有比较经典的连接池等等,总结归纳下来,池化技术有两点优势:

  • 限制资源
  • 复用资源
    总的来讲服务的资源是有限的,一方面要限制资源的无限占用(例如创建N多个线程),一方面要复用已有资源,节约创建销毁对象以及切换上下文(此处主要说的是线程上下文切换)的消耗。

这篇笔记主要是通过对于线程池技术的源码分析来对池化技术进行进一步的总结和理解。
目前还没怎么研究线程池的各个状态,以及源码中关于位运算的高级运用,所以这两部分就先当它没有。

首先,需要明晰几个概念:
工作线程集合
任务队列
这两个东西属于线程池的核心组成部分,先单独拿出来:

private final BlockingQueue<Runnable> workQueue;
private final HashSet<Worker> workers = new HashSet<Worker>();

可以看到工作线程集合是一个hashset,但是其中并没有直接存储线程,而是存储了封装了线程的Worker类,这个是一个内部类。
而任务队列,是一个阻塞队列,在java中BlockingQueue有多种实现,这个显然是在初始化线程池的时候创建的。

提到这个部分首先就记录一下线程池的两种创建方式吧。
第一种比较简单且便捷,Executors工具类提供了四种(貌似是四种)线程池的创建。
但是使用这种方式创建线程池会有明显的弊端,比如默认采用了无界队列,这个就可能导致任务积压过多,再比如封装的方法虽然简便但并不那么灵活。

第二种就是使用ThreadPoolExecutor直接创建,这个就是线程池的类,实际上Executors工具类实际上也是采用这种方式创建的。
这种方式被阿里所推崇,并且极其灵活,可以控制任务队列的长度、可以给线程赋予有意义的名字... ...
不好的一点就是比较麻烦,实际上也没麻烦到哪里去:

// 给线程定义有业务含义的名称
        ThreadFactory threadFactory = new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setName("thread_pool_" + t.getName());
                System.out.println("create thread : " + t.getName());
                return t;
            }
        };
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                5,  // 线程池核心线程数
                10,  // 线程池最大线程数,达到最大值后线程池不会再增加线程
                1000,  // 线程池中超过corePoolSize数目的空闲线程最大存活时间
                TimeUnit.MILLISECONDS,  // 时间单位,毫秒
                new LinkedBlockingQueue<>(50),  // 工作线程等待队列
                threadFactory,  // 自定义线程工厂
                new ThreadPoolExecutor.AbortPolicy()) {
            @Override
            protected void beforeExecute(Thread t, Runnable r) {
                System.out.println("线程执行前,执行此方法");
            }
        };  // 线程池满时的拒绝策略

上面是线程工厂,下面是线程池。
重写的beforeExecute方法实际上没啥必要,这里只是为了试验一下猜想是否正确,在本来的类中,这个方法是一个空方法,在run真正的任务之前会调用一次,这种套路在源码中之前也见过,应该算是所谓的扩展点,也就是说,可以通过重写这种方法,使得在任务执行之前做一些其他事情。

ps:TimeUnit这东西相当好使

线程池的大致流程如下:
不论核心线程数设置多少,在一开始,池中是不存在任何线程的(虽然也可以让它预先创建好线程,那一套的执行流程放到后面研究)
核心线程数->任务队列长度->最大线程数
当一个任务被提交到线程池中后,首先会判断线程数量是否小于核心线程数,如果是,则创建新线程来执行这个任务(也就是调用addWorker方法,添加新的worker)。
如果核心线程添加满了,则尝试将任务存入队列。
如果队列也满了,判断线程数是否超过最大线程数,如果没有,则继续添加worker,超过了的话,相当于此时已经达到了这个线程池的资源上限。
这个时候需要调用拒绝策略,默认提供了几种拒绝策略,也可以实现自己的拒绝策略。

之前我最大的两个困惑就是,线程池中的线程是如何实现复用的?worker中的runWorker是合适开始调用的?
这一切的一切需要从excute方法讲起。
excute方法负责将任务提交到线程池中,然后会执行以上判断,最终决定是添加线程执行,还是放入队列等待,还是拒绝... ...

public void execute(Runnable command) {
        //step1:判空
        if (command == null)
            throw new NullPointerException();
        //step2:执行上述判断
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))//添加核心线程
                return;
            c = ctl.get();
        }
        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);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

具体的就不展开论述了,否则笔记根本就记不完。
假设现在提交的是第一个任务,线程池也处于运行状态,一切正常的那种。
那么首先就要进入添加核心线程的阶段,传入的参数是任务本身,和true:

private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }

        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    // Recheck while holding lock.
                    // Back out on ThreadFactory failure or if
                    // shut down before lock acquired.
                    int rs = runStateOf(ctl.get());

                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

首先说一下第二个参数bool值true的含义,实际上添加worker,也就是添加工作线程(核心线程、非核心线程)的时候,调用的都是addWorker方法,核心线程传true,非核心线程传false。
因为在方法中有一个判断:

            int wc = workerCountOf(c); //获取现有线程数
                if (wc >= CAPACITY ||//和线程池容量对比
                    wc >= (core ? corePoolSize : maximumPoolSize))//和核心线程数或最大线程数对比(重点在这里,也是唯一用了这个bool属性的位置)
                    return false;

添加worker的方法简单来说就是,先判断一下能不能添加线程,多线程环境情况可能瞬息万变,所以只是判断还不够,判断只能说明当前那一瞬间是否可以添加新的线程,最终需要用cas来保证原子性,将线程数+1,这个时候,成功的话,证明可以放心向worker集合中添加worker了,这一块很难用语言描述,要建立在对CAS原子性操作有一定程度认知的基础上,再理解这一块。

然后要创建worker,将其加入hashset中,但是,hashset是非线程安全的,所以此时需要使用mainlock这个排它锁来锁一下。
加入后,将worker中的线程开启就可以了。

实际上有一个关键点在于worker的创建,这一个细节在我前几次阅读这一块的源码时都忽略了,直接导致前后逻辑串不上:

Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }

状态先不管他。
首先,firstTask就是任务本身,而thread属性存放的是worker封装的线程,在JDK中提供了一个默认的线程工厂,这个不用去管,都比较简单,直接看newThread的参数,传入的参数是worker本身
在此处存在了一个无限循环的镜中世界
worker中包含了thread,这个thread中又包含了当前这个worker... ...
这里就利用了java的多态特性,worker同时实现了Runnable接口:

public void run() {
            runWorker(this);
        }

而thread实际上只调用了这个run方法,(这一块的知识属于多线程的基础部分,参考Thread启动Runnable的语法),所以,真相只有一个
线程启动的时候,调用的是“Runnable”的run方法,而这个run方法是由Worker实现的,所以也就调用到了runWorker方法。

这个设计真的是... ...

final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) {
                w.lock();
                // If pool is stopping, ensure thread is interrupted;
                // if not, ensure thread is not interrupted.  This
                // requires a recheck in second case to deal with
                // shutdownNow race while clearing interrupt
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

这个执行过程简单来说就是,判断任务是否为null,不为null则直接执行,为null则从任务队列中取出一个任务。
执行完后,就将task赋值为null,继续循环判断,取出任务,直到没有任务可执行。
就会跳出循环,这个过程就是线程的复用,综上所述,就解释了之前的两点疑惑。

还有一点很重要的是,线程的回收:

private void processWorkerExit(Worker w, boolean completedAbruptly) {
        if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
            decrementWorkerCount();

        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            completedTaskCount += w.completedTasks;
            workers.remove(w);
        } finally {
            mainLock.unlock();
        }

        tryTerminate();

        int c = ctl.get();
        if (runStateLessThan(c, STOP)) {
            if (!completedAbruptly) {
                int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
                if (min == 0 && ! workQueue.isEmpty())
                    min = 1;
                if (workerCountOf(c) >= min)
                    return; // replacement not needed
            }
            addWorker(null, false);
        }
    }

这个回收方法实际上就是将worker从集合中删掉,如果任务队列有任务,则补充一个线程去干活。当然这属于极简描述,有时间再详细研究一下线程池的各种状态啥的。

以上的万字长文,知识线程池部分的冰山一角,还有很多,诸如线程过期时间等,都没有研究完... ...

posted @ 2021-09-02 15:34  无心大魔王  阅读(41)  评论(0编辑  收藏  举报