本文共 5792 字,大约阅读时间需要 19 分钟。
本文介绍java多线程最不可缺少的一环——线程池。
说到线程池,就不得不提池化思想了。池化思想的核心就是复用。
如果我们不用线程池,而是自己创建Thread对象来运行任务,系统就会不断的创建和回收Thread对象,而这其中必然会造成系统效率的下降和资源的浪费。
但是如果我们提前准备了一些空闲的Thread对象,需要的时候直接拿来用,用完了再把空闲的Thread对象存起来以备下次使用。这样就避免了系统临时创建线程和回收线程所产生的资源浪费。
这就是线程池的由来。
本文会从学习线程池所需要的前置知识入手,继而通过解剖线程池的内部结构和工作原理,来带动学习如何具体使用线程池。最后还会根据所学知识分析JDK提供的几种具体的线程池的特点。
要讲线程池的前置知识,让我们先预支下之后要用到的内容,来看看正常情况下是如何使用线程池的吧:
public static void main(String[] args) throws Exception { ExecutorService executor = Executors.newCachedThreadPool(); Futurefuture = executor.submit(new Callable () { @Override public Integer call() throws Exception { return 0; } }); future.get(); // 阻塞方法,直到future有结果 }
上面短短的几行代码却有好多需要引起我们注意的事情,先把它们一一列举出来,再对它们进行详细介绍。
代码中,executor
就是线程池本体了,它的类型是ExecutorService
。
通过调用线程池对象的submit(Callable)
方法,得到了一个Future
类型的结果。最后调用Future对象的get()
方法得到其中的结果。
接下来我们对上面出现的新知识点一一进行讲解。
ExecutorService
是线程池公用的一个接口,java中的线程池分为两种类型:ThreadPoolExecutor
和ForkJoinPool
。由于ThreadPoolExecutor类型的线程池在实际工作中使用的更多,这篇文章我就用ThreadPoolExecutor来演示了。
Executor的相关继承图谱如下图所示:
execute(Runnable command)
方法,传入一个Runnable类型的参数就可以运行,免去了我们自己创建thread的过程。 而下一层的ExecutorService中,提供了完善线程池生命周期的方法。如之前的实例代码所示,线程池具体实现类实际上是基于这个接口上。
除了提供了完整的生命周期函数,ExecutorService还提供了submit()方法,这个方法相当于线程池对外的接口,我们都是通过调用这个方法向线程池添加任务的。
下面我们来介绍一下submit()方法。
submit()方法是由接口ExecutorService提供的。作用是将某个任务提交给线程池,让线程池异步执行这个任务。
这个方法的签名如下:
Future<T> submit(Callable<T> task)
其中Callable
类型的参数可以理解成一个Runnable
,区别在于,Callable可以有返回值,而Runnable没有返回值。
也就是说,Callable中的代码是可以有返回值的代码,并且这个返回值放在Future对象中。那么Future是什么呢?
Future的本意的“未来”,在这里可以理解为一个占位符,可以把Future看成是未来的结果。Future中存放着Callable中代码执行完成后的结果,通过get()方法可以得到其中真正的结果。
注意,get()
方法是阻塞方法,直到能拿到Callable中的返回值才会解除阻塞。
我们来总结下submit()方法执行的流程。
首先将一个Callable对象提交给一个线程池,让线程池中的某个线程来运行这个Callable。
由于submit()方法是异步执行的,所以线程在调用完该方法并拿到Future对象后,就可以去执行其他操作了。
等什么时候想知道Callable中代码的最后结果,就调用Future对象的get()方法,当前线程才会被阻塞,直到Callable中的代码返回结果。
上面这些知识就让我们知道了线程池外部向线程池提交任务前后发生的细节。
下面我们就可以深入到线程池内部进行学习了。
要使用线程池,首先就要先创建一个线程池,而要只有理解了线程池的内部结构和工作原理,才明白构造函数中各个参数的意义。
所以在学习线程池的创建之前,先来了解一下线程池的内部结构。
线程池内部结构大致如图所示:
由上图所示,一个线程池由两部分组成,分别是线程的集合和任务的队列。
其中线程集合又分为 核心线程池和普通线程池,两者加起来是最大线程池。
当我们了解了线程池的内部结构后,就可以来理解线程池具体的工作流程了。
当线程池刚刚创建出来的时候,无论是线程集合还是任务队列都是空的。
当外界不断的通过submit()方法向线程池提交任务,会经过下面各个步骤:
现在我们已经知道线程池具体工作流程了,下面在创建线程池的时候,就能理解各个参数的实际含义了。
虽然在最开始的代码示例中,我们调用了Executors.newCachedThreadPool()
来创建一个线程池,但这个方法只是将实际创建线程池的代码包装起来了。
线程池的真正的构造函数是下面这个:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueueworkQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { // ... }
我们结合之前线程池的内部结构来分析一下每个参数的含义:
corePoolSize
: 定义核心线程池的大小,对应于内部结构图中的CorePool。
maximumPoolSize
:定义最大线程池的大小,对应于内部结构图中的MaximumPool。
keepAliveTime
:定义非核心线程的存活时间。这里要解释一下,如果没有特殊设置,默认情况下核心线程池中的线程创建出来就不会被回收,但是核心线程池之外的普通线程,一段时间内没有工作就会被回收,这个参数就是来指定多久不活跃这些非核心线程才会被回收。这种非核心线程类似于内部结构图中的thread4。
unit
:用来补充说明上一个参数的时间单位。
workQueue
:任务队列,用来存放任务的。注意这是一个BlockingQueue类型的参数,如果想了解更多细节可以阅读。对应于内部结构图中的WorkQueue。
threadFactory
:线程的工厂。当线程池需要新的线程的时候,会使用这个参数来生产一个线程。
handler
:拒绝策略。当任务队列满了并且最大线程池也满了之后,再接收到新任务,线程池就会执行这个策略。这个策略可以自定义,JDK也提供了四种默认的拒绝策略,分别是AbortPolicy
, DiscardPolicy
, DiscardOldestPolicy
, CallerRunsPolicy
。
包括实例代码中使用的CachedThreadPool
, JDK已经为我们封装好了几种常见的线程池实现,分别是FixedThreadPool
,SingleThreadExecutor
, CachedThreadPool
,。
归根结底,它们底层都是调用了上面介绍的线程池的构造函数,我们现在就可以通过观察它们传入构造函数的参数,来了解它们的特点。
FixedThreadPool
底层代码如下:
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());}
根据传进构造函数的方法,我们可以推断出FixedThreadPool类型的线程池有下面特点:
LinkedBlockingQueue
,任务队列中任务的最大个数是Integer.MAX_VALUE,也就是理论上没有上限,可以无限装任务。SingleThreadExecutor
底层代码如下:
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()));}
我们会发现SingleThreadExecutor类型的线程池跟FixedThreadPool类型的线程池很像,唯一的区别在于,FixedThreadPool中可以存在好几个线程,而SingleThreadExecutor只能存在一个线程。
拓展问题:一个单线程的线程池存在的意义为何?我们自己创建一个线程也能完成相同的任务啊。
但单线程的线程池和一个独立的线程最大的区别就在于任务队列。有这个任务队列,我们就可以一直提交任务,等待线程处理就好。但是如果是自己创建线程,就要每个任务创建一个线程。
CachedThreadPool
底层代码:
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue()); }
通过观察这种线程池我们也能大致了解这个线程池的特点:
核心线程池是0,而最大线程池是Integer.MAX_VALUE。也就是说线程池中所有的线程都是要被回收的。根据第三、四个参数,线程只要在60秒内没有活动,就会被回收
任务队列是SynchronousQueue
,根据的介绍,这个队列的容量为0,并且提交任务的线程会被阻塞住,直到任务被其他线程拿走。
也就是说,当一个线程向这个队列提交了一个任务,由于核心线程池的容量是0,任务一定会先进入这个SynchronousQueue,线程也会被阻塞住。直到线程池创建了一个非核心的线程,开始处理这个任务,原线程才会被唤醒执行其他操作。
上面就是对JDK提供的几种现成的线程池进行的分析,当我们了解了线程池的底层原理后,这种分析并不困难。
本文我们对线程池进行了初步的介绍,主要是介绍了线程池的内部结构以及工作原理,并且分析了几种常见的线程池的底层实现。
然而在工作中,我们几乎不用JDK提供的线程池实现,而是要自己通过压力测试选择最合适的线程池配置。
不过万变不离其宗,只要我们知道线程池的工作原理,剩下的只要不断优化参数就行了。
转载地址:http://ygky.baihongyu.com/