聊一聊java线程池ThreadPoolExecutor(二)——线程池状态

我们知道一旦有任务来了,线程池就会分配线程去处理任务,或者把任务放到缓存队列中去。那么线程池是如何高效处理任务的呢?多个任务进入了缓存队列,多线程取任务,这又是如何处理并发问题的呢?线程都有有状态的,那么线程池有状态么?我们一点一点来看看哈。

首先,说下基础的线程池状态属性。

线程池状态:线程池一共有五种状态,并且对这五种状态进行了一定的封装,他们分别是:

  • RUNNING:能接受新提交的任务,并且也能处理阻塞队列中的任务,就是在运行。
  • SHUTDOWN:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时,调用 shutdown()方法会使线程池进入到该状态。我在上一篇的代码中有用到哦。
  • STOP:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态,最好不要直接调用这个,有些任务没处理完,会抛出异常。
  • TIDYING:如果所有的任务都已终止了,workerCount (有效线程数) 为0,线程池进入该状态后会调用 terminated() 方法进入TERMINATED 状态。
  • TERMINATED:在terminated() 方法执行完后进入该状态,默认terminated()方法中什么也没有做。

我们进入代码中看一看:

   private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
     // 计数位 29
	private static final int COUNT_BITS = Integer.SIZE - 3;
	// 左移29位然后-1,0001 1111 1111 1111 1111 1111 1111 1111
	private static final int CAPACITY   = (1 << COUNT_BITS) - 1;
	
	// 五种线程状态
	//-1左移29位,也就是-536870912
	//二进制展示是1010 0000 0000 0000 0000 0000 0000 0000
    private static final int RUNNING    = -1 << COUNT_BITS;
	//二进制展示是0000 0000 0000 0000 0000 0000 0000 0000
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
	//二进制展示是0010 0000 0000 0000 0000 0000 0000 0000
    private static final int STOP       =  1 << COUNT_BITS;
	//二进制展示是0100 0000 0000 0000 0000 0000 0000 0000
    private static final int TIDYING    =  2 << COUNT_BITS;
	//二进制展示是0110 0000 0000 0000 0000 0000 0000 0000
    private static final int TERMINATED =  3 << COUNT_BITS;

    // 获取线程状态
	//~代表非,那么~CAPACITY也就是1110 0000 0000 0000 0000 0000 0000 0000
	// c 假设是                   1010 0000 0000 0000 0000 0000 0000 0000
	// 那么返回的就是             1010 0000 0000 0000 0000 0000 0000 0000
    private static int runStateOf(int c)     { return c & ~CAPACITY; }

线程池通过ctl来控制线程池的运行状态和线程池中有效线程数量,使用的是线程安全的AtomicInteger ,ctl共有32位,前面三位是状态位,最后面的29位才是线程数量计数,也就是2^29-1=536870912,5亿多,不少了,超出这么多线程,那估计就hold不住了,应该用不了那么多,CPU就GG了,O(∩_∩)O哈哈~。

我们可以看到线程的状态是-1,0,1,2,3左移29位的数字代表的,注意:绝不是-1,0,1,2,3啊,这么一说,就知道你是没看过源码,扫了一眼就哗啦哗啦开始扯了。。贼尴尬

接着,在获取状态的时候, 通过c & ~CAPACITY直接获取,数量的话也可以通过ctl直接获取,这么一看设计是不是厉害的样纸?嘿嘿

下面来看下运行吧。

    public void execute(Runnable command) {
	    // 运行的对象都没有,只能抛出异常了
        if (command == null)
            throw new NullPointerException();
		// 获取ctl的value,线程状态和workerCount都在里面,没它不行啊
        int c = ctl.get();
		//上面说了获取线程状态。workerCountOf(c) 这个就是获取线程数量了
		//小于核心线程数,那就分配线程直接运行呗
        if (workerCountOf(c) < corePoolSize) {
			// 添加到任务
            if (addWorker(command, true))
                return;
			//添加任务失败,这个时候ctl也许会改变,需要重新获取ctl
            c = ctl.get();
        }
		//如果当前线程是运行状态并且添加任务到列表成功
        if (isRunning(c) && workQueue.offer(command)) {
			// 获取ctl
            int recheck = ctl.get();
			//再次判断线程池的运行状态,如果不是运行状态,把刚刚加到workQueue中的command移除
            if (! isRunning(recheck) && remove(command))
				// 拒绝策略
                reject(command);
			//有效线程数为0,那就要执行addWork方法了
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
		//再次调用addWorker方法,但第二个参数传入为false,
		//将线程池的有限线程数量的上限设置为maximumPoolSize;
        else if (!addWorker(command, false))
            reject(command);
    }

上面是线程池运行的执行任务的主要方法,简单的注释已经写了。这段代码主要就是分为三步。

  1. 如果运行的线程数少于corePoolSize,直接使用线程执行任务
  2. 如果任务排队成功,那么仍要检查是不是应该加入线程,因为可能刚刚检查的线程已经执行结束了。
  3. 如果无法进入缓存队列,那就是缓存队列满了,只能重新启动线程了,在最大线程允许的情况下,如果启动线程也失败,那只能拒绝任务了。

我们接着看这个核心的方法addWorker~

    private boolean addWorker(Runnable firstTask, boolean core) {
		//retry 这个是标志位,就是在循环中continue,break的时候可以进行多层跳出
		//个人觉得这段代码使用while处理可能更亲切一点,哈哈
        retry:
		// 无限循环,不多说
        for (;;) {
			//获取ctl以及状态
            int c = ctl.get();
            int rs = runStateOf(c);

            // 线程池的状态是int类型,从运行到关闭是逐渐增大的,所以直接使用大于小于对比
			// rs >= SHUTDOWN,表示此时不再接收新任务
			// rs == SHUTDOWN,这时表示关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务
		    // 既然不接受新任务,那 firstTask当然要为空
			// 如果workQueue不为空,就是处理workQueue里面的任务,没问题,但是加了一个否定,那还处理啥?
            if (rs >= SHUTDOWN && !(rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty()))
                return false;

            for (;;) {
				// 工作的线程的计数,就是第几个工作的线程
                int wc = workerCountOf(c);
				// 超出最大限制(默认2亿多,这个是可以初始化设置的,不会真是这么多)还搞啥?GG,
				// 根据core判断,是取corePoolSize还是maximumPoolSize,超过了线程数目,当然有也GG
                if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
				// 尝试增加workerCount,成功的话就跳出循环
                if (compareAndIncrementWorkerCount(c))
					//跳出retry下面的这个循环
                    break retry;
				// 重新获取ctl	
                c = ctl.get(); 
				// 获取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);
			// 获取worker的线程
			//这个ThreadFactory如果没设置的话,会默认使用默认java.util.concurrent.Executors.DefaultThreadFactory
            final Thread t = w.thread;
	        //线程肯定不能为空,为空的话那就增加任务失败,执行不了呗
            if (t != null) {
				//获取新的ReentrantLock锁(排它锁)
                final ReentrantLock mainLock = this.mainLock;
				//加锁,不多说哈,不熟悉的可以看下之前锁的讲解
				// 因为这是一个线程池,肯定存在多线程竞争的情况,比如同时去取一个任务,同时去执行等等
                mainLock.lock();
                try {
                    //获取ctl状态
                    int rs = runStateOf(ctl.get());
					// rs < SHUTDOWN表示是RUNNING
					// 如果rs是RUNNING状态或者rs是SHUTDOWN状态并且firstTask为null,继续下面的逻辑
					// SHUTDOWN状态下,不会添加新的任务,只会执行缓存列表中的任务
                    if (rs < SHUTDOWN ||(rs == SHUTDOWN && firstTask == null)) {
						//预检查t是否可启动
                        if (t.isAlive())
                            throw new IllegalThreadStateException();
						// private final HashSet<Worker> workers = new HashSet<Worker>();
						// workers是一个HashSet
                        workers.add(w);
						// largestPoolSize 线程池的最大线程数量
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
				// 任务增加成功,也分配到线程了
                if (workerAdded) {
				    // 那就启动线程,运行任务呗
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
			// 启动失败的话,就把已经加入到workers里面的任务移除掉
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

addWorker只有两个入参,firstTask和core,也就是addworker(增加任务)只关心任务和线程池的大小,而线程池的大小是根据core的传参确定的。

进入这个方法,首先是对状态进行过滤,线程池有5中状态,除了RUNNING和SHOWDOWN,其他的状态都不会再执行任务了,而SHUTDOWN状态,也只是执行缓存队列中的任务,所以addWorker首先对这个做了判断处理。

接着,就是对线程数量的判断,线程的数量肯定不能超过方法允许的最大线程数,超过了就没有线程可以处理任务了,也就是会直接GG,返回false。

当然如果执行的中途状态改变,比如,调用shutdown或者调用shutdownNow方法,就要重新走一遍for循环。

好了上面检查都没问题,就要开始加任务了。

新建任务,获取线程,然后就是加锁,一个排它锁。因为线程池是多线程,存在并发现象,而且workers这个HashSet并不是线程安全的,所以要加锁。

加锁之后,worker成功加入到workers中,就代表加入任务成功,之后启动便可。

在启动的时候,是直接t.start()。大家可以看下这个t是怎么来的。t是从worker中取的,worker的构造方法

Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }
    this.thread = getThreadFactory().newThread(this); 这个线程中的runnable,其实就是传进来的firstTask!所以t.strat(),也就是运行firstTask。
    大致的流程就是这样哈。

      差不多了,别的我们下一次再看哈~

No sacrifice,no victory~

 

猜你喜欢

转载自blog.csdn.net/zsah2011/article/details/110230276
今日推荐