spring boot

Java--线程池

2019-04-13  本文已影响14人  still_loving

线程池(Thread Pool):顾名思义,就是类似于一个充满了线程的池子,它其实是一种线程的使用模式,是一种池化技术的应用。因为频繁创建和销毁线程会导致线程调度效率降低,进而影响整体性能。所以为了降低这种影响,就出现了线程池。虽然线程的创建和销毁相对于进程来说已经很轻量化,但是仍然无法避免创建销毁以及切换带来的性能损耗。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务,当任务处理完线程不会销毁,会进入线程池,任务来临,直接从线程池中取线程去处理,从而省去了创建和销毁的性能损耗,提升了程序性能。

池化技术

池化技术:简单点来说,就是提前保存大量的资源,以备不时之需。在机器资源有限的情况下,使用池化技术可以大大的提高资源的利用率,提升性能等。但是同时它也需要一定的资源来维护这个“池”资源。

有非常多的典型应用场景:线程池、数据库连接时候用到的连接池、内存池和对象池等。

线程池简单使用

       在Java中,ThreadPool是通过ExecutorService来实现的,例如下面举例的一个最简单的线程池使用方式:

ExecutorService service = new ThreadPoolExecutor(1, 1, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));
service.execute(() -> System.out.println("Hello ThreadPool!"));
service.shutdown();

       上面这个例子很简单,就是创建一个内部只有一个线程的线程池,它的空闲时长为60秒,超过就会被回收,任务队列最多只能存储10条任务。调用serviceexecute()方法,传入一个实现了Runnable接口的类对象,这里我采用了Java8新增的Lambda表达式的方式,只是一种语法糖而已。
这里的shutdown方法我查阅了以下源码的注释,上面的解释是:

       调用该方法后,会启动有序关闭,其中先前提交的任务将被执行,但不会接受任何新任务。如果已经关闭,则没有其他影响。此方法不会等待先前提交的任务完成执行。

       它其实就是一个关闭线程池的操作,因为线程池如果没有特殊原因,它会一直都在运行,随时等候接收任务并处理。如果不对其进行释放,就会占用一部分系统资源,如果过多,可能会造成资源开销问题。调用shutdown只能保证其中的任务可以执行,但是并不保证是否能够执行完成。

       上面的使用示例中涉及到了一些参数传入,这些参数都有各自的概念,下面来解释一下线程池内部的一些名词概念。

名词概念

       如果把线程池比作一个公司。公司会有正式员工(核心线程数coreSize)处理正常业务,如果工作量大的话,会雇佣外包人员(maximumSize - coreSize)来工作。闲时就可以释放外包人员以减少公司管理开销。而且公司因为成本关系,雇佣的人员始终是有最大数(maximumSize)。如果这时候还有任务处理不过来,就走需求池排任务(任务队列(Queue))。

       那么上面这个比喻基本上就覆盖了线程池中一部分的基本概念,所谓线程池本质是一个hashSet。多余的任务会放在阻塞队列中。只有当阻塞队列满了后,才会触发非核心线程的创建。所以非核心线程只是临时过来打杂的。直到空闲了,然后自己关闭了。线程池提供了两个钩子(beforeExecuteafterExecute)给我们,我们继承线程池,在执行任务前后做一些事情。

任务队列

ThreadPoolExecutor

现在再回过头来看ThreadPoolExecutor类的源码,就能明白其中的一些概念了:

handler

       这里需要着重强调一下handler的策略问题,当队列和线程池都达到饱和状态,需要一个对应的饱和处理策略来处理后续涌入的线程任务,JDK主要提供了4种饱和策略供选择。4种策略都做为静态内部类在ThreadPoolExcutor中进行实现。

execute()

  • 首先得到线程池的当前线程数,如果线程数小于corePoolSize,则执行addWorker方法创建新的线程执行任务;
  • 然后判断线程池是否在运行,如果在,任务队列是否允许插入,插入成功再次验证线程池是否运行,如果不在运行,移除插入的任务,然后抛出拒绝策略。如果在运行,没有线程了,就启用一个线程。否则如果添加非核心线程失败,就直接拒绝了。

       简而言之就是:execute方法中传入的内容,会根据当前线程池的状态,来确定是直接加入到核心线程池中启动线程运行,还是加入到等待队列中等待运行,甚至是直接拒绝运行。

image.png

Java内置的四种常用线程池

newCachedThreadPool

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

       它的核心线程数为0,任务队列采用的是同步移交队列,要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接收这个元素。因此即便SynchronousQueue一开始为空且大小为1,第一个任务也无法放入其中,因为没有线程在等待从SynchronousQueue中取走元素。所以第一个任务到达时便会创建一个新线程执行该任务。

newFixedThreadPool

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

线程数量固定,使用无限大的队列。因为是无限大的任务队列,会有OOM的风险,慎重使用。

newScheduledThreadPool

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE,
              DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
              new DelayedWorkQueue());
}

       创建一个定长线程池,支持定时及周期性任务执行。这里有一个DelayedWorkQueue队列,它作为静态内部类就在ScheduledThreadPoolExecutor中进行了实现,实际上它是一个无界队列,能按一定的顺序对工作队列中的元素进行排列。

newSingleThreadExecutor

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

       创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。它实际上就是增强了ScheduledExecutorService(1)的功能,不仅确保只有一个线程顺序执行任务,也保证线程意外终止后会重新创建一个线程继续执行任务。

注意:在阿里巴巴Java开发手册中也明确指出,而且用的词是『不允许』使用Executors创建线程池。

image.png

何时建议使用线程池?

       何时可以使用线程池,这里有个简单的公式对比:

       假如T1为 创建线程时间,T2 为在线程中执行任务的时间,T3 为销毁线程时间。那么当T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。

       换句话说,如果出现了频繁的创建和销毁线程过程,但是每次线程执行的任务都比较简单快速,就强烈建议使用线程池,这种情况完美符合了线程池的使用场景,可以最大限度地提升程序的性能。

上一篇下一篇

猜你喜欢

热点阅读