固基篇|重要的线程池知识

1,461 阅读7分钟

Java提供的线程池

常用的线程池

1.newCachedThreadPool

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

ThreadPoolExecutor构造方法的参数可以看出:

该类型线程池有0个核心线程,但却不限制最大线程的数量(可达到 Integer.MAX_VALUE 个)。如果线程闲置的时间超过一定的时间60s,则被终止并移出缓存。所以即使长时间闲置,这种线程池也不会消耗什么资源。

所以该类型线程池适用于场景:比较适合处理大量短时间的工作任务。

比较有意思的是,最后一个参数用的是SynchronousQueue,而不是常用的LinkedBlockingQueue,这是为什么呢?

这需要我们对SynchronousQueue有一定的了解:

SynchronousQueue从真正意义上来说并不能算是一个队列,将其理解为一个用于线程之间通信的组件更为恰当。它没有容量的概念,当一个线程在执行完入队列操作之后,必须等待另外一个线程与之匹配完成出队列后方可继续再次入队列。

正因为SynchronousQueue队列没有容量,不能存放任务,所以当有一个任务过来,此时没有空闲线程,就会去创建新线程。从下面的源码中可以验证:

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();
    }
 	  //SynchronousQueue的offer方法会返回false,跳过该判断
    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);
    }
    //直接addWorker,创建新线程
    else if (!addWorker(command, false))
        reject(command);
}

2.newFixedThreadPool(int nThreads)

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

corePoolSize maximumPoolSize 设置成了相同的值(fixed:固定),就是说存在的线程都是核心线程数量,所以KeepAlive的设置没有用,直接设置为0。任务队列使用的是不限制大小的 LinkedBlockingQueue ,由于是无界队列所以容纳的任务数量没有上限。

该线程池的优点是能够保证所有的任务都被执行,永远不会拒绝新的任务。同时它的缺点是队列数量没有限制,在任务执行时间无限延长的这种极端情况下会造成内存问题。

3.newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>(),
                                threadFactory));
}

它的特点在于工作线程数目被限制为 1,所以可以保证了所有任务的都会被顺序执行。

可以看到它还使用FinalizableDelegatedExecutorService类进行了包装。这个包装类的主要目的是为了屏蔽ThreadPoolExecutor中部分功能,只委托了部分方法对外使用。另外重写了finalize方法,该方法会在JVM进行垃圾回收清理对象时调用,此时shutdown方法会被调用。

private static class FinalizableDelegatedExecutorService
        extends DelegatedExecutorService {
    FinalizableDelegatedExecutorService(ExecutorService executor) {
        super(executor);
    }
    protected void finalize() {
        super.shutdown();
    }
}

根据场景选择线程池

  • newFixedThreadPool:希望所有提交的任务能被执行的情况,不需要开辟大量线程的场景。
  • newSingleThreadExecutor:希望只用一个线程顺序的处理任务场景。
  • newCachedThreadPool:希望提交的任务尽快分配线程执行,适合量大耗时短的任务。

自定义线程池

以上三种线程池在任务无限多的情况下,都可能会造成内存问题。如果在任务很多、严重超时的时候允许拒绝后续的任务情况下,我们可以自定义线程池,定义最大的线程数,并设置RejectedExecutionHandler处理任务被拒绝的情况。例如:

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                2,                 //核心线程数2                                             
                20,			           //最大线程数20
                30, TimeUnit.SECONDS,    	//非核心线程数量的线程闲置30秒之后会退出                              
                new ArrayBlockingQueue<Runnable>(100),      //队列长度为100
                new MyRejectedExecutionHandler()            //任务被拒绝以后的处理类
        );

其他线程池

1. newSingleThreadScheduledExecutor

创建的是个ScheduledExecutorService,可以进行定时或周期性的工作调度。

2.newWorkStealingPool(int parallelism)

这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序。

线程池的关闭

有一天小A同学去一个大厂面试,被问到这样的问题:

面试官:说说如何停止一个正在运行的线程?

窃喜的小A心想这还不简单:调用了线程的stop方法就可以终止线程。

面试官追问:嗯,那用它会不会有什么问题呀?

小A有点懵,不确信的答道:应该没问题吧,官方提供的api。

面试官:比如安全方面有没有什么隐患?

迷惑的小A,猜这么问十有八九是有了,但有什么安全隐患呢???想不通面试官想问什么知识点的他只能被迫回答:安全方面没有深入了解过。

面试官:好的。那我们来做道算法题吧。

小A:好...

在介绍线程池的中断之前,先介绍线程的中断,解惑面试官想get的点是什么。

线程Thread的中断

不推荐使用的stop方法

线程启动完毕后,在运行时可能需要终止,Java提供的终止方式只有一个stop方法。但这个方法在较新的jdk中已经被标记为过时方法,从编码规范上说肯定不建议使用过时方法。

但为什么会被标记成过时呢?

根本原因是它的不安全性。stop方法是一种“恶意”的中断,一旦执行stop方法,即终止当前正在运行的线程,不管线程逻辑是否完整,这可能会导致出现不可以预期的错误,是非常危险的。

比如下面这个简单的例子:开启一个子线程,执行10次循环,第5次的时候,睡眠3s模拟耗时操作,这段时间可能在处理重要的逻辑操作和释放一些资源。但开启该线程1s后,会调用stop终止线程。

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++){
                    System.out.println("cur i: " + i);
                    if (i == 5){
                        try {
                            //处理比较重要的耗时操作
                            System.out.println("sleep start...");
                            //子线程睡眠3s
                            Thread.sleep(3_000);
                            //释放资源
                            System.out.println("sleep end...");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            return;
                        }
                    }
                }
            }
        });
        //开启子线程
        thread.start();
        //主线程睡眠1s
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //中止子线程
        thread.stop();
    }

这会发生什么呢?看下控制台打印:

cur i: 0
cur i: 1
cur i: 2
cur i: 3
cur i: 4
cur i: 5
sleep start...

Process finished with exit code 0

可以看到在sleep还没结束的时候,程序就结束了运行。正是因为调用了stop方法该线程就被立刻终止了,run方法里面所有代码都不再执行,产生了业务逻辑不完整的情况,并且一些释放资源的操作也都没能执行。

除了这种情况,stop方法在多线程安全的情况下也会出现一些问题:当使用了同步锁的时候,stop方法却会带来麻烦,它会丢弃所有的锁,导致原子逻辑受损。这里牵涉到多线程安全和锁的知识,限于本文主题,这里不再详细介绍,多线程和锁的内容后续笔者会再开文章介绍。

所以,stop所谓的安全问题存在上述情况。这也是上述场景的面试官期待的答案。

推荐使用的interrupt()

stop不适合使用,正确的处理方式是什么呢?

Thread提供了一个interrupt()方法,译为中断。该方法会设置一个中断标记位,并不会终止线程。开发者可根据该标记位在线程任务中根据业务需要自己做处理。该标记位的结果可通过isInterrupted()获取。

再举一个简单的场景:开启一个子线程,执行10次耗时任务。通过sleep模拟耗时。开启完线程后,立刻去中断线程。

public static void main(String[] args) {
    Thread thread = new Thread() {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println("cur i: " + i);
                try {
                    //可在耗时操作前判断中断标识位,结束线程任务
                    if (isInterrupted()) {
                        System.out.println("interrupted " + isInterrupted());
                        //处理后事:比如释放资源
                        break;
                    }
                    System.out.println("sleep start...");
                    //子线程睡眠500ms
                    Thread.sleep(500);
                    System.out.println("sleep end...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    //处理后事:比如释放资源
                    break;
                } finally {
                    System.out.println("finally...");
                }
            }
        }
    };
    //开启子线程
    thread.start();
    //中断子线程
    thread.interrupt();
}

查看控制台打印结果,只打印了第一次循环的i值:

cur i: 0
interrupted true
finally...

Process finished with exit code 0

这是因为在代码中,我们在耗时任务前面使用isInterrupted()判断线程是否中断了。如果中断了, 就处理后事,比如把需要释放的资源释放掉,然后结束任务。

如果细心的话,你可能会观察到任务块中还捕获了InterruptedException异常。这是因为什么呢?中断线程不是设置个标记位就够了,为啥会出现这个所谓的中断异常呢?

这是在java中使用sleep方法强制捕获的,有的人看到异常就有点担心,是不是代码写的有问题。其实不用担心,这是因为当一个线程在处于睡眠状态时,线程此时的状态的稳定的。所以当设置了线程中断,此时对该线程进行关闭是安全的。

所以在线程设置线程中断后,在线程sleep的时候会捕获到InterruptedException异常。这个时候我们就可以做相应的处理,比如结束任务,释放资源。

isInterrupted() 和 Thread.interrupted() 的区别

isInterrupted:只测试此线程是否被中断 ,不清除中断状态。即只要中断,多次调用结果都是true。

Thread.interrupted: 测试当前线程是否被中断(检查中断标志),返回一个boolean并清除中断状态,第二次再调用时中断状态已经被清除,将返回一个false。

线程池关闭的正确姿势

根据线程的关闭,线程池是不是也提供了这样的操作方式呢?提供线程池的中断方法?

线程池给我们提供了两种关闭线程池的方法:shutdown() shutdownNow()

从命名对比可以看出来一个是关闭,一个是立刻关闭。确实如此

  • shutdown() :开始有序关闭线程池,与此同时,已提交的任务将继续执行,但不再接收新的任务

  • shutdownNow():尝试终止所有正在执行的任务,并停止处理等待队列中的的任务,最后将所有未执行的任务列表的形式返回。此方法会尽最大努力终止正在执行的任务。因为此方法底层实现是通过 Thread 类的interrupt()方法终止任务的,所以interrupt()未能终止的任务可能无法结束。

除了这两种方法,还提供了awaitTermination()方法。它的出现是为了解决:

shutdown() shutdownNow()不会等待正在执行的任务执行完毕,即调用后不会阻塞当前线程。如果需要等待当前线程池正在执行的任务完毕后再执行后续任务,就可以调用awaitTermination()方法阻塞等待。

最后

想更深入了解SynchronousQueue可参考:

深入理解 JUC:SynchronousQueue

newCachedThreadPool为什么使用SynchronousQueue

关闭线程池更详细的内容可参考:

关闭线程池的正确姿势

java线程池写的比较好的文章推荐:

Java线程池实现原理及其在美团业务中的实践