备注

  1. 以下代码没有shutdown线程池
  2. Java,spring线程池同理

前提

  1. 创建一个线程池,线程池大小固定为10,阻塞队列大小为10,最大线程池为20,拒绝策略为默认AbortPolicy。
  2. 分页处理,有100页任务需要处理,需要处理100次

1. 常见固定线程池处理

public static void main(String[] args) {
        
        ExecutorService executorService = new ThreadPoolExecutor(10, 20,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(10));

        //假如要分页处理100页数据
        int totalPage = 100;
        
        //一共10个线程
        int totalThreadNum = 10;
        
        //直接开10个固定线程去处理任务
        for (int i = 0; i < 10; i++) {
            executorService.submit(() -> {
                     
                /**
                 *   线程1,查询,处理1-10页
                 *   线程2,查询,处理11-20页
                 *   线程3,查询,处理21-30页
                 *   ....
                 */
            });
        }
    }

以上代码,开了10个固定线程,每个线程查询,处理10页数据。

如果我们的场景是需要要将100页数据从主线程提交到线程池中处理,而不是上述在线程池中直接查询100页数据进行处理,该怎么办呢?

2. 提交到线程池处理

我们可能会如下处理:

public static void main(String[] args) {

        ExecutorService executorService = new ThreadPoolExecutor(10, 20,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(10));

        int totalPage = 100;
        int totalThreadNum = 10;


        //直接把100页提交到线程池
        executorService.submit(() -> {
            try {
                //任务需要处理1秒钟
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(a);
        });
    }

运行结果如下:

Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@567d299b rejected from java.util.concurrent.ThreadPoolExecutor@2eafffde[Running, pool size = 20, active threads = 20, queued tasks = 10, completed tasks = 0]
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
	at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:112)
	at com.iboxpay.business.strategy.impl.TestService.main(TestService.java:25)
0
5
1

由线程池的基础原理。我们得知,其实上述线程,在并发太大的情况下,已经执行了拒绝策略,相应的任务就已经丢弃了。

这肯定不是我们要的结果,我们肯定是希望100页数据全部执行完成。

这时候我们肯定会,怎么让任务一页一页执行,不丢弃呢?

当线程池满了,阻塞后续进入的线程,让其等待?没错,这是个很好的解决思路。

3. 线程池满了,阻塞线程,慢慢等待

public static void main(String[] args) {

        ExecutorService executorService = new ThreadPoolExecutor(10, 20,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(10), new RejectedExecutionHandler() {
            //变更点,注意,此处把线程池中的阻塞队列拿出来,重新put Runnable
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                try {
                    executor.getQueue().put(r);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        int totalPage = 100;
        int totalThreadNum = 10;


        //直接把100页提交到线程池
        for (int i = 0; i < totalPage; i++) {

            final int a = i;
            executorService.submit(() -> {
                try {
                    //任务需要处理1秒钟
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(a);
            });
        }
    }

上述代码运行就可以把所有任务执行完成(不展示运行结果了)。

  1. 仔细查看上述代码变更点。仅仅做了一步操作,就是把线程池中的阻塞队列拿出来,重新把这个任务放进去。
  2. 这样有什么用?
  3. 我们来看一下线程池ThreadPoolExecutor的源码,仅仅看下面注释的部分
public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();

        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
     	//此处,把任务放到阻塞队列中,采取的是offer方法
        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);
    }

我们自己的拒绝策略是用的put方法,而线程池是用的offer方法。差别就在于offer方法是不阻塞的,插入不了了,就往下走;而put方法是一直阻塞,直到元素插入到阻塞队列中

更详细的阻塞队列的插入和获取元素的不同方法及其原理可以自行了解一下

4. 其他提交线程池阻塞方式

待下一篇分析rocketmq是如何固定线程池并发消费
RocketMQ并发消费如何固定线程池