线程池的阻塞

  • 问题
  • 线程池的知识
  • 回归问题


问题

今天碰到一个有意思的问题,我把它抽取出来:

package thread_pool;

import java.util.concurrent.*;

public class TestThreadPool {
    public static void main(String[] args) {
        ExecutorService threadPoolExecutor = new ThreadPoolExecutor(2,
                5,
                2L,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
               new ThreadPoolExecutor.AbortPolicy());

        threadPoolExecutor.execute(()->{
            System.out.println("the thread is running...");
        });
    }
}

搞了个线程池,分配给它一个任务,就是打印一句话,控制台“ the thread is running”当然打印了,但是,程序没有结束:

java线程池线程ID java线程池线程一直阻塞_java线程池线程ID

问题是:

  • 为什么阻塞了?
  • 在哪里阻塞了?

线程池的知识

我们可以稍稍聊聊线程池。

池化技术,像连接池、线程池,都是为了管理资源,就像巴士公司对于巴士的管理。当乘客人满了,巴士才会发车开往机场。这里的一辆辆巴士,其实就是一条条线程。当巴士完成工作了,它又停回原地了。

像Arrays、Collections这些工具类,线程也有自己的工具类,那就是Executors。

线程池可以直接通过工具类来创建:

newFixedThreadPool

定长的线程池。

package thread_pool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestExecutors {

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(5);

        Runnable runnable = () -> {
            System.out.println(Thread.currentThread().getName() + " is running...");
        };

        for (int i = 1; i <= 6; i++) {
            executor.execute(runnable);
        }
    }
}

我们搞5个线程,然后丢6个任务进去,最后工作的肯定就只有5个线程:

pool-1-thread-1 is running...
pool-1-thread-5 is running...
pool-1-thread-1 is running...
pool-1-thread-2 is running...
pool-1-thread-4 is running...
pool-1-thread-3 is running...

newCachedThreadPool

长度可伸缩的线程池。

package thread_pool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestExecutors {

    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();

        Runnable runnable = () -> {
            System.out.println(Thread.currentThread().getName() + " is running...");
        };

        for (int i = 1; i <= 20; i++) {
            executor.execute(runnable);
        }
    }
}

丢20个任务,我们看看结果:

pool-1-thread-1 is running...
pool-1-thread-3 is running...
pool-1-thread-2 is running...
pool-1-thread-4 is running...
pool-1-thread-6 is running...
pool-1-thread-7 is running...
pool-1-thread-8 is running...
pool-1-thread-5 is running...
pool-1-thread-6 is running...
pool-1-thread-1 is running...
pool-1-thread-5 is running...
pool-1-thread-1 is running...
pool-1-thread-4 is running...
pool-1-thread-8 is running...
pool-1-thread-2 is running...
pool-1-thread-6 is running...
pool-1-thread-9 is running...
pool-1-thread-10 is running...
pool-1-thread-7 is running...
pool-1-thread-3 is running...

有10条线程在工作。

newSingleThreadExecutor

只有一条线程。

package thread_pool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestExecutors {

    public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();

        Runnable runnable = () -> {
            System.out.println(Thread.currentThread().getName() + " is running...");
        };

        for (int i = 1; i <= 10; i++) {
            executor.execute(runnable);
        }
    }
}

10个任务只有一条线程在工作:

pool-1-thread-1 is running...
pool-1-thread-1 is running...
pool-1-thread-1 is running...
pool-1-thread-1 is running...
pool-1-thread-1 is running...
pool-1-thread-1 is running...
pool-1-thread-1 is running...
pool-1-thread-1 is running...
pool-1-thread-1 is running...
pool-1-thread-1 is running...

newScheduledThreadPool

有延迟地执行任务或有周期地执行任务。

延迟执行任务:

package thread_pool;

import java.util.Date;
import java.util.concurrent.*;

public class TestExecutors {

    public static void main(String[] args) throws InterruptedException {
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);

        Runnable runnable = () -> {
            System.out.println(Thread.currentThread().getName() + " is running..." + " time : " + new Date());
        };

        System.out.println(Thread.currentThread().getName() + " is running..." + " time : " + new Date());

        executor.schedule(runnable, 5, TimeUnit.SECONDS);
        executor.schedule(runnable, 10, TimeUnit.SECONDS);

    }
}

结果:

main is running... time : Mon Mar 02 21:38:10 GMT+08:00 2020
pool-1-thread-1 is running... time : Mon Mar 02 21:38:15 GMT+08:00 2020
pool-1-thread-2 is running... time : Mon Mar 02 21:38:20 GMT+08:00 2020

显然是每隔5秒执行一次任务。

或者可以这么写:

package thread_pool;

import java.util.Date;
import java.util.concurrent.*;

public class TestExecutors {

    public static void main(String[] args) throws InterruptedException {
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);

        Runnable runnable = () -> {
            System.out.println(Thread.currentThread().getName() + " is running..." + " time : " + new Date());
        };

        System.out.println(Thread.currentThread().getName() + " is running..." + " time : " + new Date());

        executor.scheduleAtFixedRate(runnable, 2, 5, TimeUnit.SECONDS);

        TimeUnit.SECONDS.sleep(20);

        executor.shutdown();

    }
}

初始延迟两秒,每个任务之间相隔五秒,20秒之后关闭线程池。

main is running... time : Mon Mar 02 21:44:45 GMT+08:00 2020
pool-1-thread-1 is running... time : Mon Mar 02 21:44:47 GMT+08:00 2020
pool-1-thread-1 is running... time : Mon Mar 02 21:44:52 GMT+08:00 2020
pool-1-thread-1 is running... time : Mon Mar 02 21:44:57 GMT+08:00 2020
pool-1-thread-2 is running... time : Mon Mar 02 21:45:02 GMT+08:00 2020

但是,我们不能这么建线程池,如阿里规范所说:

java线程池线程ID java线程池线程一直阻塞_System_02


默认的方式队列长度是整型的最大值:

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
/**
     * Creates a {@code LinkedBlockingQueue} with a capacity of
     * {@link Integer#MAX_VALUE}.
     */
    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

所以我们要用ThreadPoolExecutor

/**
     * Creates a new {@code ThreadPoolExecutor} with the given initial
     * parameters.
     *
     * @param corePoolSize the number of threads to keep in the pool, even
     *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
     * @param maximumPoolSize the maximum number of threads to allow in the
     *        pool
     * @param keepAliveTime when the number of threads is greater than
     *        the core, this is the maximum time that excess idle threads
     *        will wait for new tasks before terminating.
     * @param unit the time unit for the {@code keepAliveTime} argument
     * @param workQueue the queue to use for holding tasks before they are
     *        executed.  This queue will hold only the {@code Runnable}
     *        tasks submitted by the {@code execute} method.
     * @param threadFactory the factory to use when the executor
     *        creates a new thread
     * @param handler the handler to use when execution is blocked
     *        because the thread bounds and queue capacities are reached
     * @throws IllegalArgumentException if one of the following holds:<br>
     *         {@code corePoolSize < 0}<br>
     *         {@code keepAliveTime < 0}<br>
     *         {@code maximumPoolSize <= 0}<br>
     *         {@code maximumPoolSize < corePoolSize}
     * @throws NullPointerException if {@code workQueue}
     *         or {@code threadFactory} or {@code handler} is null
     */
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

我们要了解其中的参数含义。

举个银行办理业务的例子:

java线程池线程ID java线程池线程一直阻塞_System_03


银行一共有5个柜台处理业务,这就是maximumPoolSize

现在开放2个,也就是一号柜台和二号柜台,其余三个都是闲置状态(工作人员可能去吃午饭了)。这正在工作的2个柜台叫做corePoolSize

然后还有侯客区,侯客区是等待办理业务的人坐的地方,这里侯客区的大小是3。侯客区对应线程池里的workQueue

银行就相当于threadFactory。线程工厂是用来创建线程的,就像银行是用来组织工作人员的。以前你跑一个线程需要new Thread(Runnable r),现在传一个线程工厂就行了。ThreadFactory是一个接口,你可以这么去实现:

class SimpleThreadFactory implements ThreadFactory {
    public Thread newThread(Runnable r) {
    	return new Thread(r);
   }

当然,我们有默认的工厂。对于这个参数,传一个默认的就行了。

如果5个柜台同时办公,并且侯客区坐满了人,那么如果此时新来一个人要办业务,就必须要拒绝他,如何拒绝他,就是这里的拒绝策略RejectedExecutionHandler handler。你可以让他等,让他走,等等的,我们下面讲。

如果2个工作的柜台都在工作,并且侯客区坐满了人,此时新来一个人,那么银行就会新开一个柜台受理业务(比如三号柜台)。等那个人走了,三号柜台不可能一直开着,它要等一会关闭的,要等多久呢,就是keepAliveTime,时间单位由unit给定。

我们一个一个场景看:

有两个人来办理业务

package thread_pool;

import java.util.concurrent.*;

public class TestThreadPool {
    public static void main(String[] args) {
        
        ExecutorService threadPoolExecutor = new ThreadPoolExecutor(2,
                5,
                2L,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
               new ThreadPoolExecutor.AbortPolicy());

       Runnable runnable = ()->{
           System.out.println(Thread.currentThread().getName()+ "正在受理业务...");
       };

        for (int i = 1; i <= 2; i++) {
            threadPoolExecutor.execute(runnable);
        }

    }
}

有两个人来办理业务,因为开放的柜台就两个,所以就由这两个柜台来受理业务:

pool-1-thread-1正在受理业务...
pool-1-thread-2正在受理业务...

如果有第三个人来,那么他会先坐在侯客室(阻塞队列),等到1号或者2号柜台办理完成后,他就可以去办理了:

pool-1-thread-2正在受理业务...
pool-1-thread-2正在受理业务...
pool-1-thread-1正在受理业务...

现在有六个人来办理(for循环中i<=6),侯客室坐不下了,银行不得不再开一个窗口来受理,因此会有三条线程在工作。

pool-1-thread-2正在受理业务...
pool-1-thread-2正在受理业务...
pool-1-thread-2正在受理业务...
pool-1-thread-2正在受理业务...
pool-1-thread-1正在受理业务...
pool-1-thread-3正在受理业务...

但是这个也不是绝对的,你可以想一下,如果1号窗口受理的快,它照样可以办理第三个人的业务,最后可能还是两个窗口在受理(这和你的cpu有关)。

如果开了第三个窗口(就像我打印出来的),因为我们设置了keepAliveTime为两秒,所以两秒后,第三个窗口会关掉,活跃线程数会减一。

for (int i = 1; i <= 6; i++) {
            threadPoolExecutor.execute(runnable);
            System.out.println("活跃线程数(执行中): " + Thread.activeCount());
        }

        TimeUnit.SECONDS.sleep(3);

        System.out.println("活跃线程数(执行后): " + Thread.activeCount());

为了让效果明显,主线程我睡了三秒。

结果:

活跃线程数(执行中): 3
活跃线程数(执行中): 4
活跃线程数(执行中): 4
活跃线程数(执行中): 4
活跃线程数(执行中): 4
活跃线程数(执行中): 5
pool-1-thread-1正在受理业务...
pool-1-thread-1正在受理业务...
pool-1-thread-1正在受理业务...
pool-1-thread-1正在受理业务...
pool-1-thread-2正在受理业务...
pool-1-thread-3正在受理业务...
活跃线程数(执行后): 4

因为jvm中一直有main和gc两条线程(所以Thread.activeCount()至少是2),所以我们减二来看就对了。

执行时最高开到了五条线程(其实是有3条线程在工作,因为核心线程数量是2,所以临时是多开了一个窗口在受理业务),执行后线程数少1,因为多开的窗口关掉了。

现在我要演示拒绝策略,理论上说,8个人是受理的极限了,因为最大线程数+阻塞队列大小为8。但是也可能承受住9或者10。

所以我们直接来15吧。

Exception in thread "main" 
java.util.concurrent.RejectedExecutionException:

直接报错了。

拒绝策略有四种:

抛出异常

/**
     * A handler for rejected tasks that throws a
     * {@code RejectedExecutionException}.
     */
    public static class AbortPolicy implements RejectedExecutionHandler {
        /**
         * Creates an {@code AbortPolicy}.
         */
        public AbortPolicy() { }

        /**
         * Always throws RejectedExecutionException.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         * @throws RejectedExecutionException always
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }

相当于银行无法受理更多的人,于是发出警报。

哪来的回哪儿去

/**
     * A handler for rejected tasks that runs the rejected task
     * directly in the calling thread of the {@code execute} method,
     * unless the executor has been shut down, in which case the task
     * is discarded.
     */
    public static class CallerRunsPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code CallerRunsPolicy}.
         */
        public CallerRunsPolicy() { }

        /**
         * Executes task r in the caller's thread, unless the executor
         * has been shut down, in which case the task is discarded.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }
    }

让9个人过来:

for (int i = 1; i <= 9; i++) {
            threadPoolExecutor.execute(runnable);
        }
ExecutorService threadPoolExecutor = new ThreadPoolExecutor(2,
                5,
                2L,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
               new ThreadPoolExecutor.CallerRunsPolicy());

这时候的结果为:

main正在受理业务...
pool-1-thread-1正在受理业务...
pool-1-thread-1正在受理业务...
pool-1-thread-1正在受理业务...
pool-1-thread-1正在受理业务...
pool-1-thread-2正在受理业务...
pool-1-thread-3正在受理业务...
pool-1-thread-5正在受理业务...
pool-1-thread-4正在受理业务...

任务是main线程派来的,就回到main线程去处理。

不理你

/**
     * A handler for rejected tasks that silently discards the
     * rejected task.
     */
    public static class DiscardPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code DiscardPolicy}.
         */
        public DiscardPolicy() { }

        /**
         * Does nothing, which has the effect of discarding task r.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        }
    }

银行对你不理不睬,啥也不做,能处理多少就处理多少。

这种策略不会有异常。

尝试退掉第一个人让你进来

/**
     * A handler for rejected tasks that discards the oldest unhandled
     * request and then retries {@code execute}, unless the executor
     * is shut down, in which case the task is discarded.
     */
    public static class DiscardOldestPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code DiscardOldestPolicy} for the given executor.
         */
        public DiscardOldestPolicy() { }

        /**
         * Obtains and ignores the next task that the executor
         * would otherwise execute, if one is immediately available,
         * and then retries execution of task r, unless the executor
         * is shut down, in which case task r is instead discarded.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }
    }

这种情况是,线程池让第一个人走开,然后让你进来,它会尝试,如果尝试成功,就这么做,否则就不管你。

举个例子:

ThreadPoolExecutor executor = new ThreadPoolExecutor(1,1,0,TimeUnit.MILLISECONDS
        ,new ArrayBlockingQueue<>(2),new ThreadPoolExecutor.DiscardOldestPolicy());

        executor.execute(()->{
            System.out.println("a simple task...");
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        BlockingQueue<String> queue = new LinkedBlockingQueue<>();
        executor.execute(()->{queue.offer("First");});
        executor.execute(()->{queue.offer("Second");});
        executor.execute(()->{queue.offer("Third");});

        TimeUnit.SECONDS.sleep(2);

        List<String> results = new ArrayList<>();
        queue.drainTo(results);

        System.out.println(results);

第一个任务占了100毫秒,第二个任务往queue中加一个“First”,第三个任务加入了“Second”,当第四个任务进来,由于DiscardOldestPolicy,尝试移除“First”,腾出空间给第四个任务。

结果:

a simple task...
[Second, Third]

回归问题

我们讲了线程池的知识,但是最后还要回到最初的阻塞问题。

其实,上面的例子我都没有写threadPoolExecutor.shutdown();

正确的语法是:

ExecutorService threadPoolExecutor = new ThreadPoolExecutor(2,
                5,
                2L,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        try {
            threadPoolExecutor.execute(() -> {
                System.out.println("the thread is running...");
            });
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPoolExecutor.shutdown();
        }

虽然我知道语法,但是我还是对这个阻塞很感兴趣。

打开java VisualVM,我们找到该线程池的thread dump:

"pool-1-thread-1" #11 prio=5 os_prio=0 tid=0x0000000057fdc000 nid=0x1198 waiting on condition [0x000000005895e000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x00000000ec408fd8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
        at java.util.concurrent.ArrayBlockingQueue.take(ArrayBlockingQueue.java:403)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
        at java.lang.Thread.run(Thread.java:748)

   Locked ownable synchronizers:
        - None

估计是停在java.util.concurrent.ArrayBlockingQueue.take这里了。

debug走一波:

java线程池线程ID java线程池线程一直阻塞_java_04


终于知道了,是BlockingQueue干的事。