深入浅出线:程池的线程回收--回收的是非核心线程吗?

本文通过实例分析了Java线程池的工作原理,探讨了线程池中线程如何从等待队列中获取任务,以及线程的回收策略。在特定情况下,尽管线程池允许核心线程超时,但回收的线程并不一定是因为其是非核心线程,而是基于等待时间。线程的调度并非随机,而是遵循一定的顺序,如AQS的等待队列。文章还深入到源码层面,解释了线程的阻塞、唤醒和回收过程。
摘要由CSDN通过智能技术生成

写这篇文章的初衷是在和同事讨论线程池中线程去等待队列里面去取任务是随机的还是有序的,什么意思呢?举个例子,我自定义了下面一个线程池,如下:

ExecutorService executorService =
                    ThreadPoolExecutor(
                        2,
                        3,
                        60_000,
                        TimeUnit.MILLISECONDS,
                        LinkedBlockingQueue(2),
                        ZjtThreadFactory()
                    )

即核心池的大小是2,最大线程数是3,等待队列的大小是2,非核心线程存活的时间是60秒

假设我现在向线程池中提交5个任务,每个任务耗时1s,那么按照线程池的原理,肯定是先创建2个线程(线程1和线程2)分别执行任务1和任务2,然后任务3和任务4放到等待队列中,然后再创建一个线程执行任务5,等1s任务结束后,线程1和线程2去等待队列中去取任务3和任务4执行。再然后就是任务执行完了,线程1和线程2和线程3都在等待任务,等待60秒之后会回收线程3

注意本例中是回收线程3并不是因为线程3是非核心线程,而是线程1和线程2执行完任务1和任务2后又去队列中取任务3和任务4执行了,线程3最先等待,所以会先回收线程3。

测试代码如下:

executorService =
    ThreadPoolExecutor(
        2,
        3,
        60_000,
        TimeUnit.MILLISECONDS,
        LinkedBlockingQueue(2),
        ZjtThreadFactory()
    )
for (i in 1..5) {
    executorService.execute {
        Log.e("zzzzz", "${Thread.currentThread().name} >> 任务 $i 开始执行")
        Thread.sleep(1000)
        Log.e("zzzzz", "${Thread.currentThread().name} >> 任务 $i 执行结束")
    }
}
 

看下结果是不是这样啊:

2022-01-07 10:45:13.806 30299-31786/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-1 >> 任务 1 开始执行
2022-01-07 10:45:13.806 30299-31787/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-2 >> 任务 2 开始执行
2022-01-07 10:45:13.807 30299-31788/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-3 >> 任务 5 开始执行
2022-01-07 10:45:14.807 30299-31786/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-1 >> 任务 1 执行结束
2022-01-07 10:45:14.807 30299-31787/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-2 >> 任务 2 执行结束
2022-01-07 10:45:14.807 30299-31786/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-1 >> 任务 3 开始执行
2022-01-07 10:45:14.807 30299-31788/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-3 >> 任务 5 执行结束
2022-01-07 10:45:14.807 30299-31787/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-2 >> 任务 4 开始执行
2022-01-07 10:45:15.808 30299-31786/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-1 >> 任务 3 执行结束
2022-01-07 10:45:15.809 30299-31787/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-2 >> 任务 4 执行结束

从 log 中可以看出结果和我们上面分析的一样。

  1. 首先创建线程1 和 线程2 去执行任务1 和 任务2
  2. 任务3 和 任务4 放入等待队列
  3. 创建线程3去执行任务5
  4. 线程1和线程2执行完任务1和任务2后,去等待队列中去取任务3和任务4执行
  5. 线程3执行完任务,由于等待队列中没有任务了就开始等待
  6. 线程1和线程2执行完任务后也开始等待
  7. 60秒后回收线程3,因为线程3是最先等待的,而不是因为线程3是非核心线程。

问题一、先往上面的线程池抛5个任务,延时10秒后,我们开始每隔3秒往线程池中抛一个任务,那么线程3最后会被回收吗?

条件是:10秒后,线程1,2,3都执行完任务都在等待了,线程3最先等待,然后是线程1和2。

那么问题来了:该选择哪个线程来执行这个任务呢?是随机选一个吗?

先看测试结果。
加上下面的测试代码如下:

Thread.sleep(10_000)
Thread{
    for(i in 6..10) {
        Thread.sleep(3000)
        executorService.execute {
            Log.e("zzzzz", "${Thread.currentThread().name} >> 任务 $i 开始执行")
            Thread.sleep(1000)
            Log.e("zzzzz", "${Thread.currentThread().name} >> 任务 $i 执行结束")
        }
    }
}.start()

看下结果:

2022-01-07 11:52:21.263 3701-4715/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-1 >> 任务 1 开始执行
2022-01-07 11:52:21.265 3701-4716/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-2 >> 任务 2 开始执行
2022-01-07 11:52:21.265 3701-4717/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-3 >> 任务 5 开始执行
2022-01-07 11:52:22.265 3701-4715/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-1 >> 任务 1 执行结束
2022-01-07 11:52:22.265 3701-4715/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-1 >> 任务 3 开始执行
2022-01-07 11:52:22.265 3701-4716/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-2 >> 任务 2 执行结束
2022-01-07 11:52:22.265 3701-4716/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-2 >> 任务 4 开始执行
2022-01-07 11:52:22.266 3701-4717/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-3 >> 任务 5 执行结束
2022-01-07 11:52:23.266 3701-4715/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-1 >> 任务 3 执行结束
2022-01-07 11:52:23.266 3701-4716/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-2 >> 任务 4 执行结束

// 10秒之后,线程3 开始执行任务6,每隔3秒 开始轮训 是 线程1 线程2 再到 线程3

2022-01-07 11:52:34.269 3701-4717/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-3 >> 任务 6 开始执行
2022-01-07 11:52:35.270 3701-4717/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-3 >> 任务 6 执行结束
2022-01-07 11:52:37.270 3701-4715/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-1 >> 任务 7 开始执行
2022-01-07 11:52:38.271 3701-4715/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-1 >> 任务 7 执行结束
2022-01-07 11:52:40.271 3701-4716/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-2 >> 任务 8 开始执行
2022-01-07 11:52:41.273 3701-4716/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-2 >> 任务 8 执行结束
2022-01-07 11:52:43.273 3701-4717/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-3 >> 任务 9 开始执行
2022-01-07 11:52:44.274 3701-4717/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-3 >> 任务 9 执行结束
2022-01-07 11:52:46.274 3701-4715/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-1 >> 任务 10 开始执行
2022-01-07 11:52:47.275 3701-4715/com.zjt.startmodepro E/zzzzz: zjt-pool-2-thread-1 >> 任务 10 执行结束

可以看到 10 秒之后,线程3 开始执行任务6,3秒后线程1执行任务7,再3秒后 线程2执行任务8,依次轮询下去直到不再有任务插入。

即在我们的案例中,虽然线程都是空闲的,但是当任务来的时候不是随机调用的,而是轮询。那么是不是所有的都是这样轮询的呢?下面我们通过源码来分析下:

3个线程都是由于等待队列为空阻塞等待,首先是线程3getTask 获取不到任务在阻塞等待,代码如下:

 private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?
        for (; ; ) {
            int c = ctl.get();
            int rs = runStateOf(c);
            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) { // 注1
                decrementWorkerCount();
                Log.e("test thread pool", "--getTask  c = " + ctl.get());
                return null;
            }
            int wc = workerCountOf(c);
            // allowCoreThreadTimeOut 默认为false,
            // 这里存活的线程数为3大于corePoolSize的2,所以time 为 true
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; 
            if ((wc > maximumPoolSize || (timed && timedOut))
                    && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }
           
            try {
                Runnable r = timed ?
                        workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                        workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

通过上面对代码的注释,可以看到 线程1线程2 以及线程3通过 workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) 进行阻塞等待的,我这里的队列用的是 LinkedBlockingQueuepoll 方法代码如下:

public E poll(long timeout, TimeUnit unit) throws InterruptedException {
    E x = null;
    int c = -1;
    long nanos = unit.toNanos(timeout);
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly();
    try {
        while (count.get() == 0) {
            if (nanos <= 0L)
                return null;
            // 发现队列为空,通过 ReentrantLock 的 Condition 来实现阻塞等待线程存活时间
            nanos = notEmpty.awaitNanos(nanos);
        }
        x = dequeue();
        c = count.getAndDecrement();
        if (c > 1)
            notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
    if (c == capacity)
        signalNotFull();
    return x;
}

notEmpty.awaitNanos(nanos) 的部分代码如下:

public final long awaitNanos(long nanosTimeout)
        throws InterruptedException {
    。。。。。。
    // 这里是会把 这个线程作为一个结点插入到等待队列,是队列当然是有序的
   // 注意,这里的等待队列不是线程池的等待队列,是AQS中的等待队列,可以看我关于AQS的文章分析
    Node node = addConditionWaiter();
    long savedState = fullyRelease(node);
    int interruptMode = 0;
    。。。。。。
    long remaining = deadline - System.nanoTime(); // avoid overflow
    return (remaining <= initialNanos) ? remaining : Long.MIN_VALUE;
}

关于 ReentrantLock 以及 Condition 的源码分析可参考我的另一篇文章:
ReentrantLock 以及 Condition深度解析

上面分析知道,线程3 先等待 然后是线程1,最后是线程2所以AQS(AbstractQueuedLongSynchronizer) 中等待队列的顺序是:

在这里插入图片描述
等3秒之后往线程池里面抛一个任务后,就会唤醒 AQS中阻塞队列的第一个线程,即线程3被唤醒然后去执行任务。executorService.execute(runnable),因为当前存活的线程数为3大于核心线程的个数2,所以直接插入队列 workQueue.offer(command):

 public boolean offer(E e) {
     if (e == null) throw new NullPointerException();
     final AtomicInteger count = this.count;
     if (count.get() == capacity)
         return false;
     int c = -1;
     Node<E> node = new Node<E>(e);
     final ReentrantLock putLock = this.putLock;
     putLock.lock();
     try {
         if (count.get() < capacity) {
             // 插入线程池的阻塞队列
             enqueue(node);
             // 注意:getAndIncrement 的操作是先将值付给c,然后再加1,
             //所以,count = 1,但是 c = 0,然后执行后面signalNotEmpty
             c = count.getAndIncrement();
             if (c + 1 < capacity)
                 notFull.signal();
         }
     } finally {
         putLock.unlock();
     }
     if (c == 0)
         signalNotEmpty();
     return c >= 0;
 }

signalNotEmpty 代码如下:

private void signalNotEmpty() {
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        notEmpty.signal(); // 唤醒AQS中阻塞的线程
    } finally {
        takeLock.unlock();
    }
}

signal 代码如下:

public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

这段代码最终走到的是 LockSupport.unpark(node.thread); 来唤醒线程。这段代码还是比较复杂的,可以先去看看我之前贴的链接的那篇文章。

所以1s 之后线程3完成任务,插入到AQS阻塞队列的尾部,然后再次往线程池里面抛任务的时候,这时是AQS的阻塞队列是线程1在前面,所以唤醒的是线程1去执行任务。后续逻辑就差不多了。

回收非核心线程的逻辑

也是在getTask中,部分代码如下:

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

        for (; ; ) {
            int c = ctl.get();
            。。。。。。
            int wc = workerCountOf(c);

            // Are workers subject to culling?
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; //注1

            if ((wc > maximumPoolSize || (timed && timedOut))
                    && (wc > 1 || workQueue.isEmpty())) {//注2
                if (compareAndDecrementWorkerCount(c)) // 注3
                    return null;
                continue;
            }
           
            try {
                Runnable r = timed ?
                        workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                        workQueue.take();//注4
                if (r != null)
                    return r;
                timedOut = true;//注5
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

注释1boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; 由于allowCoreThreadTimeOut默认是 false,但是当前线程数量wc=3 ,大于核心线程数量corePoolSize = 2,所以 timed = true。

注释2(wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty()) wc = 3 ,且 maximumPoolSize = 3.所以不成立,timedOut = false,所以注释2 的条件不成立。

走到注释4workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) ,线程销毁时间60s时间到了之后,唤醒该线程,走到注释5timedOut = true,接着getTaskfor循环:

继续走注释2,由于此时timedOut = true,所以注释2成立。
接着走注释3,把存活的线程数量减1。然后return null。表示无任务了。

执行任务的方法 runWorker:

 final void runWorker(Worker w) {
    ......
    try {
      while (task != null || (task = getTask()) != null) {
         ......
      }
      completedAbruptly = false;
    } finally {
      processWorkerExit(w, completedAbruptly);
    }
 }

最后执行 processWorkerExit

private void processWorkerExit(Worker w, boolean completedAbruptly) {
    Log.e("test thread pool", Thread.currentThread().getName()+ " >>> pr
    if (completedAbruptly) // If abrupt, then workerCount wasn't adjuste
        decrementWorkerCount();
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        completedTaskCount += w.completedTasks;
        workers.remove(w); // 从工作列表中删除,于是线程就被回收了。
    } finally {
        mainLock.unlock();
    }
    tryTerminate(); // 中断空闲的线程
    ......
  }

所以当超过指定时间后,线程会被回收。

那么被回收的这个线程是核心线程还是非核心线程呢?

不知道。

因为在线程池里面,核心线程和非核心线程仅仅是一个概念而已,其实拿着一个线程,我们并不能知道它是核心线程还是非核心线程。

这个地方就是一个证明,因为当工作线程多余核心线程数之后,所有的线程都在 poll,也就是说所有的线程都有可能被回收:
在这里插入图片描述
举个例子来说明:

ThreadPoolExecutor(
                        1,
                        2,
                        60_000,
                        TimeUnit.MILLISECONDS,
                        LinkedBlockingQueue(2),
                        ZjtThreadFactory()
                    )

假设有4个任务,任务耗时1秒。

  1. 创建线程1 执行任务1
  2. 任务2和3放入队列
  3. 创建线程2执行任务4
  4. 1秒后线程1去队列中取任务2执行,线程2取任务3执行
  5. 再1秒后,线程1取队列中取任务,由于队列中没有任务,所以阻塞等待60秒,同理,线程2 也会阻塞等待60s
  6. 60s 后,由于是线程1先等待的,所以回收了线程1,而线程1是第一个创建的是属于"核心线程"

以上分析可知 回收的是超出核心线程数量的线程,并不一定是”非核心线程“。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值