JAVA并发包(七):ThreadPoolExecutor

从上篇文章中我们知道ThreadPoolExecutor继承于AbstractExecutorService,它是java并发的主要实现类,我们通常使用的三类线程池,都是通过ThreadPoolExecutor实现的。可以这样说,只要熟悉了ThreadPoolExecutor,那么对java线程池运作原理有了80%的掌握。

一、线程的执行

上一篇文章的UML图中可以看到,线程池的最上层接口Executor里面只有一个execute()方法,这个方法的具体实现就在ThreadPoolExecutor中看,我们从这里开始研究线程池的运行原理。首先来看源码

1. execute方法

 public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
      	/*下面有3块分支逻辑:
      	* 1.如果工作线程数小于核心线程数,则创建一个线程运行提交的任务
      	* 2.如果任务入队列成功,还要再次判断线程池是否在运行状态(存在一种可能是入队列过程中,线程池死掉了,或者线程池被shotdown了)。如果线程池不在运行状态,则拒绝这个人任务。如果线程池在运行状态,且线程数为0,则创建一个线程,去拉取队列中的任务执行。
      	* 3.如果入队列和创建新的线程都是失败了(创建新线程失败是因为线程数大于最大允许的线程数了),则拒绝这个任务	
		*/
        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);
    }

接下来是对上面出现的几个方法的深入解析,篇幅比较长,请结合开发工作上的源码,耐心阅读。

2. 线程池的几种状态

下面的状态可以不用管移位,理解状态是一位数就好了,比如RUNNING可以理解为-1,SHUTDOWN理解为0.

  1. RUNNING = -1 << 29。高30-32位为-1,编码为111,表示线程池在运行状态
  2. SHUTDOWN = 0 << 29。高30-32位为0,编码000,表示线程池处于正在关闭状态(当调用shutdown方法时为此状态),此时不再接受新任务,会执行之前已经提交的任务。
  3. STOP = 1<< 29。高30-32位为2,编码001,表示处于停止状态。会中断正在执行的任务,不再执行等待的任务,不再接受新任务。当调用shutdownNow方法时处于此状态。
  4. TIDYING = 2 << 29。高30-32位为2,编码010,表示处于正在终止状态,当工作线程为0,执行terminated前为此状态。
  5. TERMINATED = 3 << 29。高30-32位为3,编码011,表示线程池为终止状态,这是线程池最后的状态。表示线程池的工作线程为0,等待队列为0,不再接受新任务。

3. addWorker方法

addWorker方法可以看到线程是如何被创建和启动的。

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

            /*这里是判断是否还有需要创建新线程。如果状态是大于等于SHUTDOWN,并且满足下面三种条件任何一种则不再创建新的线程:
            * 1.状态大于SHUTDOWN.
            * 2.任务不为空。任务为空的话可能是为了创建新线程去抓取队列中的等待线程执行。
            * 3.任务队列为空。如果任务队列都为空了,则没必要创建新线程了。
			*/
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                // 如果工作线程数大于容量,或者工作线程大于核心线程数(大于最大线程数),则返回false,不创建线程
                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 {
        	// Worker对象里面有重要的代码,工作线程长期存活的逻辑在这里面,后面讲解
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                // 这里加锁主要是下面要操作largestPoolSize和workers
                mainLock.lock();
                try {
                    // 首先检查线程池的状态
                    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;
    }

4. Worker对象

从下面代码可以看到,Worker继承了AQS,实现了Runnable,所以它既有了加锁的属性,也可以作为一个线程任务运行。对于Worker,了解下面几个方法就差不多了

	private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
	{
		Worker(Runnable firstTask) {
			// 初始化的状态为-1
            setState(-1); 
            this.firstTask = firstTask;
            // 这里是线程工厂生成一个新的线程
            this.thread = getThreadFactory().newThread(this);
        }

        // 这里是运行线程的入口,调用了外部类的runWorker方法,runWorker是重点,后面讲解
        public void run() {
            runWorker(this);
        }
		
		// 释放锁的时候会把状态置为0
		protected boolean tryRelease(int unused) {
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        public void lock()        { acquire(1); }
        public boolean tryLock()  { return tryAcquire(1); }
        public void unlock()      { release(1); }
        public boolean isLocked() { return isHeldExclusively(); }
	}

其实看Worker对象也看不出线程池是如何运行的,得去研究runWorker方法,才能了解到里面的精髓。

5. runWorker方法

从runWorker方法中,我们就能基本明白线程池的运行奥妙了

final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        // 解锁其实就是把状态设置为0
        w.unlock(); 
        boolean completedAbruptly = true;
        try {
        	// 这个轮询就是工作线程一直会运行的奥妙所在了,它会不停的去任务队列中获取新任务,一旦获取到就执行,没有任务时它会阻塞或者直接退出
            while (task != null || (task = getTask()) != null) {
                w.lock();
                // 这个if主要用来判断是否需要中断当前线程的。首先判断线程池的状态是否大于STOP,如果没有大于STOP,则检查工作线程是否被中断了,如果中断了则需要再次检查线程池状态是否大于STOP。最后再判断工作线程是否被清除了中断,如果被清除了,则需要恢复中断状态。
                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);
        }
    }

线程的获取任务以及阻塞逻辑在getTask方法里面

private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?

        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // 如果线程池状态大于等于SHUTDOWN,并且队列为空(或者线程池状态大于等于STOP),则该工作线程退出,返回null任务
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }

            int wc = workerCountOf(c);

            // allowCoreThreadTimeOut默认是false,则判断工作线程数据是否大于核心线程数
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
			
			// 下面满足两种条件工作线程会退出:第一工作线程数大于最大线程数,或者超时了;第二,在第一条件满足的前提下,工作线程数大于1,或者工作列队为空(为什么要判断工作线程数是否大于1,是因为要保留最少一个线程去消耗掉任务队列的等待执行的任务)
            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }

            try {
            	// timed的值,如果工作线程数大于核心线程数的话为true,所以如果获取任务会调取poll方法,等待一定时间就返回。在三个普通线程池中固定线程池的的keepAliveTime是0,所以会立刻返回,如果没有获取的任务就会回到上一步的逻辑,判断是否退出。而缓存线程池的话,keepAliveTime为60秒,等待60秒后看是否获取到任务。
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

二、线程池的关闭

线程池的关闭方法有两种,分别是shutdown和shutdownNow。

1. shutdown方法

public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
        	// 安全检查,是否有权限终止,暂时不研究,一般情况下都是有权限的
            checkShutdownAccess();
            // 把线程池状态设置为SHUTDOWN
            advanceRunState(SHUTDOWN);
            // 中断空闲的线程
            interruptIdleWorkers();
			// 钩子方法,ScheduledThreadPoolExecutor类中实现
            onShutdown(); 
        } finally {
            mainLock.unlock();
        }
        // 尝试终止线程池,后面讲解
        tryTerminate();
    }

	private void interruptIdleWorkers() {
        interruptIdleWorkers(false);
 	}
	
	// 中断空闲线程的逻辑在下面这个方法,参数onlyOne表示是否只中断一个线程
	private void interruptIdleWorkers(boolean onlyOne) {
        final ReentrantLock mainLock = this.mainLock;
        // 首先需要加锁
        mainLock.lock();
        try {
            for (Worker w : workers) {
                Thread t = w.thread;
                // 第一判断是否已经中断了,第二尝试加锁,如果线程正在运行中的话,这个尝试加锁会失败,所以说它是中断空闲的线程。如果空闲的线程被中断的话,一般情况下会从阻塞中恢复过来,然后判断线程是否退出,从上文的分析中可以看出具体的逻辑。
                if (!t.isInterrupted() && w.tryLock()) {
                    try {
                        t.interrupt();
                    } catch (SecurityException ignore) {
                    } finally {
                        w.unlock();
                    }
                }
                if (onlyOne)
                    break;
            }
        } finally {
            mainLock.unlock();
        }
    }

	final void tryTerminate() {
        for (;;) {
            int c = ctl.get();
            // 满足下面三个条件如何一种,该方法会退出。第一,线程池在运行状态;第二,线程池的状态大于等于TIDYIING,说明线程池已经准备终止完毕,无须再调用终止方法。第三,线程池状态为SHUTDOWN,并且任务队列不为空,这时不能终止,应该等任务队列的任务被执行完毕再考虑终止。
            if (isRunning(c) ||
                runStateAtLeast(c, TIDYING) ||
                (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
                return;
			// 这一步是考虑工作线程是否都退出了,如果没有退出完毕,则中断一个空闲的线程,让中断传播下去(注意线程退出也会调用tryTerminate方法,所以中断具有传播效应)
            if (workerCountOf(c) != 0) { // Eligible to terminate
                interruptIdleWorkers(ONLY_ONE);
                return;
            }

            final ReentrantLock mainLock = this.mainLock;
            // 加锁,一般需要改变线程池状态之前都需要加锁
            mainLock.lock();
            try {
            	// 把线程池状态设置为TIDYING,然后再调用terminated方法
                if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
                    try {
                    	// 在这里是空方法
                        terminated();
                    } finally {
                    	// 把线程池的状态设置TERMINATED,这是最终的状态了
                        ctl.set(ctlOf(TERMINATED, 0));
                        // 唤醒等待线程池终止的线程
                        termination.signalAll();
                    }
                    return;
                }
            } finally {
                mainLock.unlock();
            }
        }
    }
   

2. shutdownNow方法

shutdownNow跟shutdown有稍微的区别,我们可以从源码中看到

public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            // 状态设置为STOP
            advanceRunState(STOP);
            // 中断正在运行的线程
            interruptWorkers();
            // 清除掉所有等待队列的任务
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
        // 尝试终止线程池,这上面分析过,不再赘述
        tryTerminate();
        return tasks;
    }

	private void interruptWorkers() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
        	// 遍历所有工作线程,然后中断已经启动的线程(因为创建工作线程的时候是先入workers队列,然后再启动线程的,所以需要判断线程是否启动)
            for (Worker w : workers)
                w.interruptIfStarted();
        } finally {
            mainLock.unlock();
        }
    }

	void interruptIfStarted() {
            Thread t;
            // 线程状态大于等于0,说明是线程已经启动了
            if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                }
            }
        }
	
	// 清除等待队列的逻辑其实就是调用BlockingQueue的drainTo方法,它会把队列里的所有元素一个个放到入参的集合里,然后清空队列
	private List<Runnable> drainQueue() {
        BlockingQueue<Runnable> q = workQueue;
        ArrayList<Runnable> taskList = new ArrayList<Runnable>();
        q.drainTo(taskList);
        if (!q.isEmpty()) {
            for (Runnable r : q.toArray(new Runnable[0])) {
                if (q.remove(r))
                    taskList.add(r);
            }
        }
        return taskList;
    }

3. 两个关闭方法的区别

  1. shutdown中断空闲线程,任务队列的任务会被执行完。shutdownNow中断所有启动的线程,任务队列的任务会被清空,不再执行。
  2. shutdown设置线程池状态为SHUTDOWN,shutdownNow是STOP。
  3. shutdown返回空,shutdownNow返回等的执行的任务集合。
  4. 两者都会让线程池不再接受新任务。

三、工作线程的退出

从上文线程的执行模块分析中,我们留了一个processWorkerExit方法没讲解,这个方法在工作线程退出的时候会执行

// 这里有两个入参,第一个是工作对象,第二个代表线程是否在运行中异常退出了,可能是任务抛出了异常没有捕获
private void processWorkerExit(Worker w, boolean completedAbruptly) {
        // 如果是异常退出,则工作线程数没有做减1
        if (completedAbruptly)
            decrementWorkerCount();

        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
        	// 统计线程池完成的任务数
            completedTaskCount += w.completedTasks;
           	// 从工作线程队列中删除这个工作线程
            workers.remove(w);
        } finally {
            mainLock.unlock();
        }
		// 尝试终止线程池,之前分析过
        tryTerminate();

        int c = ctl.get();
        // 如果线程池状态小于STOP,则需要保证有足够的工作线程去执行任务队列的任务
        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);
        }
    }

写到这里,我联想到当初我们做项目的时候直接new Thread,一开始就固定线程的数量,后面不会再添加。当执行消息队列任务的时候,出现了线程数变少的问题,如果用线程池是不会出现这种情况了。就算线程池中的线程被莫名的退出,核心线程数也不会减少。

四、线程池的拒绝策略

线程池有4中拒绝策略:

  1. AbortPolicy。默认策略,直接抛出异常。这种策略很好地保护了服务器资源,避免任务队列满了还在同步执行任务
  2. CallerRunsPolicy:同步执行,在提交任务的线程中执行run方法。
  3. DiscardPolicy:抛弃策略。就是啥都不做,直接放弃掉提交的任务。
  4. DiscardOldestPolicy:抛弃最老策略。抛弃队列中的最老的元素,并提交新任务(e.execute(t))

五、总结

至此我们可以说掌握了线程池原理的80%,还剩下的20%需要从它的子类中、以及阻塞队列中去做补充了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值