java 线程池
总阅读次
线程池:基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。
ExecutorService
通过上述分析,我们知道了通过new Thread().start()方式创建线程去处理任务的弊端,而为了解决这些问题,Java为我们提供了ExecutorService线程池来优化和管理线程的使用
使用线程池管理线程的优点
1、线程的创建和销毁由线程池维护,一个线程在完成任务后并不会立即销毁,而是由后续的任务复用这个线程,从而减少线程的创建和销毁,节约系统的开销
2、线程池旨在线程的复用,这就可以节约我们用以往的方式创建线程和销毁所消耗的时间,减少线程频繁调度的开销,从而节约系统资源,提高系统吞吐量
3、在执行大量异步任务时提高了性能
4、Java内置的一套ExecutorService线程池相关的api,可以更方便的控制线程的最大并发数、线程的定时任务、单线程的顺序执行等
ExecutorService简介
通常来说我们说到线程池第一时间想到的就是它:ExecutorService,它是一个接口,其实如果要从真正意义上来说,它可以叫做线程池的服务,因为它提供了众多接口api来控制线程池中的线程,而真正意义上的线程池就是:ThreadPoolExecutor,它实现了ExecutorService接口,并封装了一系列的api使得它具有线程池的特性,其中包括工作队列、核心线程数、最大线程数等。
线程池:ThreadPoolExecutor
既然线程池就是ThreadPoolExecutor,所以我们要创建一个线程池只需要new ThreadPoolExecutor(…);就可以创建一个线程池,而如果这样创建线程池的话,我们需要配置一堆东西,非常麻烦,我们可以看一下它的构造方法就知道了:
|
|
所以,官方也不推荐使用这种方法来创建线程池,而是推荐使用Executors的工厂方法来创建线程池,Executors类是官方提供的一个工厂类,它里面封装好了众多功能不一样的线程池,从而使得我们创建线程池非常的简便,主要提供了如下五种功能不一样的线程池:
1、newFixedThreadPool() :
作用:该方法返回一个固定线程数量的线程池,该线程池中的线程数量始终不变,即不会再创建新的线程,也不会销毁已经创建好的线程,自始自终都是那几个固定的线程在工作,所以该线程池可以控制线程的最大并发数。
栗子:假如有一个新任务提交时,线程池中如果有空闲的线程则立即使用空闲线程来处理任务,如果没有,则会把这个新任务存在一个任务队列中,一旦有线程空闲了,则按FIFO方式处理任务队列中的任务。2、newCachedThreadPool() :
作用:该方法返回一个可以根据实际情况调整线程池中线程的数量的线程池。即该线程池中的线程数量不确定,是根据实际情况动态调整的。
栗子:假如该线程池中的所有线程都正在工作,而此时有新任务提交,那么将会创建新的线程去处理该任务,而此时假如之前有一些线程完成了任务,现在又有新任务提交,那么将不会创建新线程去处理,而是复用空闲的线程去处理新任务。那么此时有人有疑问了,那这样来说该线程池的线程岂不是会越集越多?其实并不会,因为线程池中的线程都有一个“保持活动时间”的参数,通过配置它,如果线程池中的空闲线程的空闲时间超过该“保存活动时间”则立刻停止该线程,而该线程池默认的“保持活动时间”为60s。3、newSingleThreadExecutor() :
作用:该方法返回一个只有一个线程的线程池,即每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待这一个线程空闲,当这个线程空闲了再按FIFO方式顺序执行任务队列中的任务。4、newScheduledThreadPool() :
作用:该方法返回一个可以控制线程池内线程定时或周期性执行某任务的线程池。5、newSingleThreadScheduledExecutor() :
作用:该方法返回一个可以控制线程池内线程定时或周期性执行某任务的线程池。只不过和上面的区别是该线程池大小为1,而上面的可以指定线程池的大小。
好了,写了一堆来介绍这五种线程池的作用,接下来就是获取这五种线程池,通过Executors的工厂方法来获取:
|
|
Executors 提供四种线程池:
- 1)newCachedThreadPool 是一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute() 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。注意,可以使用 ThreadPoolExecutor 构造方法创建具有类似属性但细节不同(例如超时参数)的线程池。
- 2)newSingleThreadExecutor 创建是一个单线程池,也就是该线程池只有一个线程在工作,所有的任务是串行执行的,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
- 3)newFixedThreadPool 创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小,线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
- 4)newScheduledThreadPool 创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。
我们可以看到通过Executors的工厂方法来创建线程池极其简便,其实它的内部还是通过new ThreadPoolExecutor(…)的方式创建线程池的,我们看一下这些工厂方法的内部实现:
|
|
我们可以清楚的看到这些方法的内部实现都是通过创建一个ThreadPoolExecutor对象来创建的,正所谓万变不离其宗,所以我们要了解线程池还是得了解ThreadPoolExecutor这个线程池类,其中由于和定时任务相关的线程池比较特殊(newScheduledThreadPool()、newSingleThreadScheduledExecutor()),它们创建的线程池内部实现是由ScheduledThreadPoolExecutor这个类实现的,而ScheduledThreadPoolExecutor是继承于ThreadPoolExecutor扩展而成的,所以本质还是一样的,只不过多封装了一些定时任务相关的api
再撸一撸概念
Java里面线程池的顶级接口是 Executor,不过真正的线程池接口是 ExecutorService, ExecutorService 的默认实现是 ThreadPoolExecutor;普通类 Executors 里面调用的就是 ThreadPoolExecutor。
照例看一下各个接口的源码:
|
|
下面我创建的一个线程池:
|
|
通过 ThreadPoolExecutor 的构造函数,撸一撸线程池相关参数的概念:
|
|
- 1)corePoolSize:线程池的核心线程数,一般情况下不管有没有任务都会一直在线程池中一直存活,只有在 ThreadPoolExecutor 中的方法 allowCoreThreadTimeOut(boolean value) 设置为 true 时,闲置的核心线程会存在超时机制,如果在指定时间没有新任务来时,核心线程也会被终止,而这个时间间隔由第3个属性 keepAliveTime 指定。
- 2)maximumPoolSize:线程池所能容纳的最大线程数,当活动的线程数达到这个值后,后续的新任务将会被阻塞。
- 3)keepAliveTime:控制线程闲置时的超时时长,超过则终止该线程。一般情况下用于非核心线程,只有在 ThreadPoolExecutor 中的方法 allowCoreThreadTimeOut(boolean value) 设置为 true时,也作用于核心线程。
- 4)unit:用于指定 keepAliveTime 参数的时间单位,TimeUnit 是个 enum 枚举类型,常用的有:TimeUnit.HOURS(小时)、TimeUnit.MINUTES(分钟)、TimeUnit.SECONDS(秒) 和 TimeUnit.MILLISECONDS(毫秒)等。
- 5)workQueue:线程池的任务队列,通过线程池的 execute(Runnable command) 方法会将任务 Runnable 存储在队列中。
- 6)threadFactory:线程工厂,它是一个接口,用来为线程池创建新线程的。
上面的参数理解起来都比较简单,不过workQueue这个任务队列却要再次说明一下,它是一个BlockingQueue
对象,而泛型则限定它是用来存放Runnable对象的,刚刚上面讲了,不同的线程池它的任务队列实现肯定是不一样的,所以,保证不同线程池有着不同的功能的核心就是这个workQueue的实现了,细心的会发现在刚刚的用来创建线程池的工厂方法中,针对不同的线程池传入的workQueue也不一样,下面我总结一下这五种线程池分别用的是什么BlockingQueue:
1、newFixedThreadPool()—>LinkedBlockingQueue
2、newSingleThreadExecutor()—>LinkedBlockingQueue
3、newCachedThreadPool()—>SynchronousQueue
4、newScheduledThreadPool()—>DelayedWorkQueue
5、newSingleThreadScheduledExecutor()—>DelayedWorkQueue
这些队列分别表示:
LinkedBlockingQueue:无界的队列
SynchronousQueue:直接提交的队列
DelayedWorkQueue:等待队列
当然实现了BlockingQueue接口的队列还有:ArrayBlockingQueue(有界的队列)、PriorityBlockingQueue(优先级队列)。这些队列的详细作用就不多介绍了。
线程池的关闭
ThreadPoolExecutor 提供了两个方法,用于线程池的关闭,分别是 shutdown() 和 shutdownNow()。
shutdown():不会立即的终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务。
shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。
线程池ThreadPoolExecutor的使用
使用线程池,其中涉及到一个极其重要的方法,即:
|
|
该方法意为执行给定的任务,该任务处理可能在新的线程、已入池的线程或者正调用的线程,这由ThreadPoolExecutor的实现决定。
newFixedThreadPool
创建一个固定线程数量的线程池,示例为:
|
|
上述代码,我们创建了一个线程数为3的固定线程数量的线程池,同理该线程池支持的线程最大并发数也是3,而我模拟了10个任务让它处理,执行的情况则是首先执行前三个任务,后面7个则依次进入任务队列进行等待,执行完前三个任务后,再通过FIFO的方式从任务队列中取任务执行,直到最后任务都执行完毕。
为了体现出线程的复用,我特地在Log中加上了当前线程的名称,效果为:
newSingleThreadExecutor
创建一个只有一个线程的线程池,每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待线程处理完再依次处理任务队列中的任务,示例为:
|
|
代码还是差不多,只不过改了线程池的实现方式,效果我想大家都知道,即依次一个一个的处理任务,而且都是复用一个线程,效果为:
其实我们通过newSingleThreadExecutor()和newFixedThreadPool()的方法发现,创建一个singleThreadExecutorPool实际上就是创建一个核心线程数和最大线程数都为1的fixedThreadPool。
newCachedThreadPool
创建一个可以根据实际情况调整线程池中线程的数量的线程池,示例为:
|
|
为了体现该线程池可以自动根据实现情况进行线程的重用,而不是一味的创建新的线程去处理任务,我设置了每隔1s去提交一个新任务,这个新任务执行的时间也是动态变化的,所以,效果为:
newScheduledThreadPool
创建一个可以定时或者周期性执行任务的线程池,示例为:
|
|
newSingleThreadScheduledExecutor
创建一个可以定时或者周期性执行任务的线程池,该线程池的线程数为1,示例为:
|
|
实际上这个和上面的没什么太大区别,只不过是线程池内线程数量的不同,效果为:
每隔2秒就会执行一次该任务
自定义线程池ThreadPoolExecutor
Java内置只为我们提供了五种常用的线程池,一般来说这足够用了,不过有时候我们也可以根据需求来自定义我们自己的线程池,而要自定义不同功能的线程池,上面我们也说了线程池功能的不同归根到底还是内部的BlockingQueue实现不同,所以,我们要实现我们自己相要的线程池,就必须从BlockingQueue的实现上做手脚,而上面也说了BlockingQueue的实现类有多个,那么这次我们就选用PriorityBlockingQueue来实现一个功能是按任务的优先级来处理的线程池。
1、首先我们创建一个基于PriorityBlockingQueue实现的线程池,为了测试方便,我这里把核心线程数量设置为3,如下:
|
|
2、然后创建一个实现Runnable接口的类,并向外提供一个抽象方法供我们实现自定义功能,并实现Comparable接口,实现这个接口主要就是进行优先级的比较,代码如下:
|
|
3、使用我们自己的PriorityRunnable提交任务,整体代码如下:
|
|
测试效果
我们看下刚刚自定义的线程池是否达到了我们想要的功能,即根据任务的优先级进行优先处理任务,效果如下:
可以从执行结果中看出,由于核心线程数设置为3,刚开始时,系统有3个空闲线程,所以无须使用任务队列,而是直接运行前三个任务,而后面再提交任务时由于当前没有空闲线程所以加入任务队列中进行等待,此时,由于我们的任务队列实现是由PriorityBlockingQueue实现的,所以进行等待的任务会经过优先级判断,优先级高的放在队列前面先处理。从效果图中也可以看到后面的任务是先执行优先级高的任务,然后依次递减。
优先级线程池的优点
从上面我们可以得知,创建一个优先级线程池非常有用,它可以在线程池中线程数量不足或系统资源紧张时,优先处理我们想要先处理的任务,而优先级低的则放到后面再处理,这极大改善了系统默认线程池以FIFO方式处理任务的不灵活
扩展线程池ThreadPoolExecutor
除了内置的功能外,ThreadPoolExecutor也向外提供了三个接口供我们自己扩展满足我们需求的线程池,这三个接口分别是:
beforeExecute() - 任务执行前执行的方法
afterExecute() -任务执行结束后执行的方法
terminated() -线程池关闭后执行的方法
这三个方法在ThreadPoolExecutor内部都没有实现
前面两个方法我们可以在ThreadPoolExecutor内部的runWorker()方法中找到,而runWorker()是ThreadPoolExecutor的内部类Worker实现的方法,Worker它实现了Runnable接口,也正是线程池内处理任务的工作线程,而Worker.runWorker()方法则是处理我们所提交的任务的方法,它会同时被多个线程访问,所以我们看runWorker()方法的实现,由于涉及到多个线程的异步调用,必然是需要使用锁来处理,而这里使用的是Lock来实现的,我们来看看runWorker()方法内主要实现:
可以看到在task.run()之前和之后分别调用了beforeExecute和afterExecute方法,并传入了我们的任务Runnable对象
而terminated()则是在关闭线程池的方法中调用,而关闭线程池有两个方法,我贴其中一个:
所以,我们要扩展线程池,只需要重写这三个方法,并实现我们自己的功能即可,这三个方法分别都会在任务执行前调用、任务执行完成后调用、线程池关闭后调用。
这里我验证一下,继承自ThreadPoolExecutor 并实现那三个方法:
|
|
而运行后的结果则是,这正符合刚刚说的:
|
|
所以,在上面我们的优先级线程池的代码上,我们再扩展一个具有暂停功能的优先级线程池,代码如下:
具有暂时功能的线程池:
|
|
然后结合上面的优先级线程池的实现,创建具有暂停功能的优先级线程池:
|
|
这里我为了演示效果,把这个线程池设为只有一个线程,然后直接在TextView中显示当前执行的任务的优先级,然后设置个开关,控制线程池的暂停与开始:
|
|
效果为:
从效果上来看,该线程池和优先级线程一样,而且还多了一个暂停与开始的功能
优化线程池ThreadPoolExecutor
虽说线程池极大改善了系统的性能,不过创建线程池也是需要资源的,所以线程池内线程数量的大小也会影响系统的性能,大了反而浪费资源,小了反而影响系统的吞吐量,所以我们创建线程池需要把握一个度才能合理的发挥它的优点,通常来说我们要考虑的因素有CPU的数量、内存的大小、并发请求的数量等因素,按需调整。
通常核心线程数可以设为CPU数量+1,而最大线程数可以设为CPU的数量*2+1。
获取CPU数量的方法为:
|
|
shutdown()和shutdownNow()的区别
关于线程池的停止,ExecutorService为我们提供了两个方法:shutdown和shutdownNow,这两个方法各有不同,可以根据实际需求方便的运用,如下:
1、shutdown()方法在终止前允许执行以前提交的任务。
2、shutdownNow()方法则是阻止正在任务队列中等待任务的启动并试图停止当前正在执行的任务。
关于AsyncTask的实现
大家都知道AsyncTask内部实现其实就是Thread+Handler。其中Handler是为了处理线程之间的通信,而这个Thread到底是指什么呢?通过AsyncTask源码可以得知,其实这个Thread是线程池,AsyncTask内部实现了两个线程池,分别是:串行线程池和固定线程数量的线程池。而这个固定线程数量则是通过CPU的数量决定的。
在默认情况下,我们大都通过AsyncTask::execute()来执行任务的,
,而execute()内部则是调用executeOnExecutor(sDefaultExecutor, params)方法执行的,第一个参数就是指定处理该任务的线程池,而默认情况下AsyncTask是传入串行线程池(在这里不讲版本的变化),也就是任务只能单个的按顺序执行,而我们要是想让AsyncTask并行的处理任务,大家都知道调用AsyncTask::executeOnExecutor(sDefaultExecutor, params)方法传入这个参数即可:AsyncTask.THREAD_POOL_EXECUTOR。
而这个参数的意义在于为任务指定了一个固定线程数量的线程池去处理,从而达到了并行处理的功能,我们可以在源码中看到AsyncTask.THREAD_POOL_EXECUTOR这个参数就是一个固定线程数量的线程池:
|
|
面试题
1)什么是 Executor 框架?
Executor框架在Java 5中被引入,Executor 框架是一个根据一组执行策略调用、调度、执行和控制的异步任务的框架。
无限制的创建线程会引起应用程序内存溢出,所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。利用 Executor 框架可以非常方便的创建一个线程池。
2)Executors 类是什么?
Executors为Executor、ExecutorService、ScheduledExecutorService、ThreadFactory 和 Callable 类提供了一些工具方法。Executors 可以用于方便的创建线程池。
未经许可不得转载,转载请注明zilianliuxue的blog,本人保留所有版权。