JAVA线程池全解
来源:互联网 发布:迈远短信平台网页源码 编辑:程序博客网 时间:2024/06/04 20:13
我们都知道线程池有很多好处:
通过重复利用已经创建好的线程,可以减少创建线程时的资源消耗。
如果不限制线程的数量,不仅会大量消耗系统内存,还会照成系统的不稳定。
使用线程池,可以控制线程的数量,还可以对所有线程进行统一的管理,好处不言而喻。
一、BlockingQueue
先看一下阻塞队列的代码关系:interface BlockingQueue extends Queue
我们发现这是一个继承了Queue的接口。我们发现这是一个继承了Queue的接口。首先Queue接口有如下方法:
我们再看看BlockingQueue接口的方法
BlockingQueue有四种类型的插入,删除,和获取元素方法,归类如下:
我们可以很明显的看到BlockingQueue加入了阻塞等待的操作,可以理解成如果队列满了,插入任务就在门口等着,不抛出错误信息,直到有元素从队列中取出,队列有空位了,再进行插入操作,相应的你还可以加入等待超时机制,如果过时了,就不等了。
public class BlockingQueueTest { public static void main(String[] args) { //初始化队列长度只有3的队列 final BlockingQueue<String> blockingque = new ArrayBlockingQueue<String>(3); Thread Putter = new Thread(new Runnable(){ @Override public void run() { for(int i=1;i<10;i++){ try { blockingque.put("货物"+i); System.out.println("成功往队列中放入货物"+i); } catch (InterruptedException e) { e.printStackTrace(); } } } }); Putter.start(); Thread taker = new Thread(new Runnable(){ @Override public void run() { while(true){ try { //取得时间延长,模拟取得时间远大于放入时间 Thread.sleep(3000); String cargo = blockingque.take(); System.out.println("取出货物: "+cargo); } catch (InterruptedException e) { e.printStackTrace(); } } } }); taker.start(); }}
输出结果为:
成功往队列中放入货物1成功往队列中放入货物2成功往队列中放入货物3取出货物: 货物1成功往队列中放入货物4取出货物: 货物2成功往队列中放入货物5取出货物: 货物3成功往队列中放入货物6取出货物: 货物4成功往队列中放入货物7取出货物: 货物5成功往队列中放入货物8取出货物: 货物6成功往队列中放入货物9取出货物: 货物7取出货物: 货物8取出货物: 货物9
我们可以看到队列长度为3,队列放满3个后,put()方法就处于blocking的状态等待队列有位子
过了3秒后,getter开始从队列中取货物,一有空位,put()方法就得以继续执行。
二、实现一个简单的线程池
首先我们关注一个 “生产者消费者” 的情景。
生产者:不断产生新的任务,比如查询数据库,执行某些业务逻辑等。
消费者:完成任务。
那么把这两者连接起来的就要用到我们的BlockingQueue了。
生产者将不断产生的任务放入到队列中,如果队列满了,生产者等待。
消费者不断的从队列中取出任务解决,当队列空了,消费者等待新任务到来。
首先BlockQueue的长度我们要限制,不然如果解决者的解决能力跟不上生产者的,这个任务队列就会越来越多。
接着我们还需要限定消费者的个数,就是我们所谓的线程池中能同时运行的最多的线程数,如果线程数太多的话会严重影响系统的稳定性。
那么我们根据这两个参数写一个简单的线程池
线程池:
public class ThreadPool { //用blockingQueue创建一个任务队列,初始化长度为5 private BlockingQueue<Runnable> tasksQueue = new ArrayBlockingQueue<Runnable>(5); //定义线程池中消费者最大数量 private int consumers = 3; //这个方法提供给所有的任务生产者,产生新的任务插入 public void insertTask(Runnable task) throws InterruptedException{ tasksQueue.put(task); } //线程池的初始化 ThreadPool(){ //激活消费者,等待问题到来 for(int i=1;i<=consumers;i++){ Solver consumer = new Solver(tasksQueue,i); consumer.start(); } }}
接下来定义 消费者 逻辑:
public class Solver extends Thread{ //引用线程池的任务队列,消费者不断的从里面取得任务去解决 private BlockingQueue<Runnable> taskQueue = null; String name; Solver(BlockingQueue<Runnable> tasks,int name){ this.taskQueue = tasks; this.name = String.valueOf(name); } public void run(){ try { while(true){ //从队列中取出任务执行,注意这里用了take方法,所以如果队列空了,那么线程会等待,直到有任务来了,继续执行 Runnable task = taskQueue.take(); System.out.println("消费者"+name+"接收了一个任务"); task.run(); System.out.println("消费者"+name+"解决了一个任务"); } } catch (InterruptedException e) { e.printStackTrace(); } }}
我们在上面的例子可以看到。
这个线程池中最大的线程数是3,就是最多只能同时有3个消费者线程执行,消费者会监视线程池的任务队列,只要队列中有任务,就会取出来执行。
接下来我们定义 生产者 的逻辑:
public class ProblemCreater { public static void main(String[] args) throws Exception { //初始化线程池 ThreadPool threadPool = new ThreadPool(); //生成者不断产生任务 for(int i=1;i<10;i++){ //定义一个新的任务 Runnable task = new Runnable(){ public void run(){ Random random = new Random(); //随机一个数字模拟需要解决的时间 int randomTime = Math.abs(random.nextInt())%20; //System.out.println("这个任务需要解决时间为:"+randomTime); try { Thread.sleep(randomTime*1000); } catch (InterruptedException e) { e.printStackTrace(); } } }; //将问题插入到线程池任务队列中 threadPool.insertTask(task); System.out.println("插入新的任务"+i); } }}
到此我们已经实现好了一个非常简单的线程池,将线程的创建与执行过程分离开,而不是将线程的生命周期管理和任务的执行过程绑定在一起,如果只是想简单的丢一个任务进去执行,我们只需要将任务的执行过程封装到一个Runnable接口中就可以了。而对于那些需要返回结果的任务,我们可以将其封装到Callable接口里面。
三、ExecutorService接口以及ThreadPoolExecutor
我们来了解一下java为我们提供的线程池实现—— ExecutorService接口
它位于jdk的java.util.concurrent包下。
JDK提供了这么两个类来实现这个接口:
- ThreadPoolExecutor
- ScheduledThreadPoolExecutor
我们这篇文章只介绍一下ThreadPoolExecutor类(ScheduledThreadPoolExecutor类类似,多加入了计划任务功能。)
我们首先看看怎么用ThreadPoolExecutor类初始化一个线程池:
//初始化一个线程池 //核心线程数 int corePoolSize = 5; //最大线程数 int maxPoolSize = 10; //空闲线程最大存活时间 long keepAliveTime = 5000; //任务队列 BlockingQueue<Runnable> queue = new ArrayBlockingQueue<Runnable>(5); ExecutorService threadPoolExecutor = new ThreadPoolExecutor( corePoolSize, maxPoolSize, keepAliveTime, TimeUnit.MILLISECONDS, queue );
这个类完全实现了一个类似于我们上一篇文章中实现的线程池,它包含以下几个属性:
1. corePoolSize
核心线程数:即使没有任何任务过来,线程池里面也会有保持的最基本线程数。
2. maximumPoolSize
最大线程数(即使任务特别多,线程池里的线程数也不会超过它)
3. keepAliveTime
空闲线程最大存活时间
4. blockingQueue
任务队列,用来存放待处理的任务。可以选择以下几个阻塞队列。
ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。
SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue。
PriorityBlockingQueue:一个具有优先级得无限阻塞队列。
那么整个类的结构就如下图所示:
要使用这个线程池的话,可以使用它提供给我们的如下方法:
- execute(Runnable)
- submit(Runnable)
- submit(Callable)
- invokeAny(…)
- invokeAll(…)
execute()和submit()可以向这个线程池提交单个任务。他们的区别是:
使用execute提交任务,但是execute方法没有返回值,所以无法判断任务知否被线程池执行成功。
使用submit 方法来提交任务,它会返回一个future对象,那么我们可以通过这个future对象来判断任务是否执行成功
invokeAll可以直接把一个List类型的任务列表一次性的提交给线程池执行。
那么接下来我们就用 ThreadPoolExecutor 来创建一个线程池,改写一下我们上一篇文章的例子:
public class ProblemCreater { public static void main(String[] args) throws Exception { //初始化线程池 //核心线程数 int corePoolSize = 5; //最大线程数 int maxPoolSize = 10; long keepAliveTime = 5000; ExecutorService threadPoolExecutor = new ThreadPoolExecutor( corePoolSize, maxPoolSize, keepAliveTime, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(5) ); //生成者不断产生任务 for(int i=1;i<10;i++){ //定义一个新的任务 Runnable task = new Runnable(){ public void run(){ Random random = new Random(); //随机一个数字模拟需要解决的时间 int randomTime = Math.abs(random.nextInt())%20; try { Thread.sleep(randomTime*1000); System.out.println("任务完成,花费时间为:"+randomTime); } catch (InterruptedException e) { e.printStackTrace(); } } }; //将问题插入到线程池任务队列中 threadPoolExecutor.execute(task); System.out.println("插入新的任务"+i); } } }
我们再回头看看这个 ThreadPoolExecutor 的初始化,我们需要给它传递5个参数。核心线程数,最大线程数,线程存活时间,时间单位,还有阻塞队列的类型。
这个过程还是比较繁琐的。其实Java帮我们简化了这个过程,我们可以根据不同的情景,直接用一行代码创建一个合适的线程池。
实现这个功能的就是 java.util.concurrent.Executors类。
四、Executors类
上一章我们介绍了ExecutorService接口,以及它的实现类ThreadPoolExecutor。
那么这里我们将介绍Executors类,它可以更进一步的简化我们的工作,直接创建一些预定义过的线程池
这个类也在java.util.concurrent包下。它有如下的几个比较常用的创建线程池的方法:
一:newFixedThreadPool
创建一个线程可重用的并且线程数固定的线程池。
二:newCachedThreadPool
创建一个可根据实际情况动态维持线程数的线程池,当任务到来时,如果有已经构造好的空闲线程将重用它们,不创建新的线程。
如果没有可用的空闲线程,则创建一个新线程并添加到池中。并且会终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。
三:newSingleThreadExecutor
创建一个使用单个线程的 ExecutorService,以无界队列方式来运行该线程。
关于这3类的线程池我引用一下《thinking in java》对它们的描述:
使用FixedThreadPool,你可以一次性的预先执行代价高昂的线程分配,因而也就可以限制线程数的数量,这可以节省时间,因为你不用为每一个任务都固定的付出创建线程的开销。在事件驱动的系统中,需要线程的事件处理器,通过直接从池中获取线程,也可以如你所愿地尽快得到服务。你不会滥用可获得的资源,因为FixedThreadPool使用的Thread对象的数量是有界的。
对于CachedThreadPool,它在程序的执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程,因此它是合理的ExecutorService的首选。只有当这种方式会引发问题时,你才需要切换到FixedThreadPool。
SingleThreadExecutor就是线程数量为1的FixedThreadPool。这对于你希望在另一个线程中连续运行的任何事物(长期存活的任务)来说,都是非常有用的,例如监听进入套接字连接的任务。
我们再来看一下它们的源码,比如FixedThreadPool:
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
所以在本质上,就是创建了一个我们上一篇文章介绍过的ThreadPoolExecutor类。
接下来我们写一个完整的,使用它们的例子:
下面给出了一个网络服务的简单结构,这里线程池中的线程作为传入的请求。它使用了预先配置的 Executors.newFixedThreadPool(int) 方法创建线程池:
class NetworkService implements Runnable { private final ServerSocket serverSocket; private final ExecutorService pool; public NetworkService(int port, int poolSize) throws IOException { serverSocket = new ServerSocket(port); pool = Executors.newFixedThreadPool(poolSize); } public void run() { // run the service try { for (;;) { pool.execute(new Handler(serverSocket.accept())); } } catch (IOException ex) { pool.shutdown(); } } } class Handler implements Runnable { private final Socket socket; Handler(Socket socket) { this.socket = socket; } public void run() { // read and service request on socket } }
五、ThreadPoolExecutor源码分析
通过前面的章节,我们学习了可以通过ThreadPoolExecutor来创建一个线程池。
那么接下来我们分析一下ThreadPoolExecutor的源码,看看它具体是如何工作的。
我们看一下使用execute(Runnable task)执行一个任务的时候,到底发生了什么(代码进过简化):
先简单描述一下当我们提交一个任务到线程池中后发生了什么:
public void execute(Runnable task) { //取出当前线程池活跃的线程数。 //ctl是一个原子类型的对象(final AtomicInteger ctl),用来保存当前线程池的线程数以及线程池的状态。 int c = ctl.get(); //如果当前的活跃线程数小于核心线程数,即使现在有空闲线程,也创建一个新的线程,去执行这个任务 if (workerCountOf(c) < corePoolSize) { //创建一个新的线程,去执行这个任务。 if (addWorker(task, true)) return; //如果执行到这一句说明任务没有分配成功。 //所以获得当前线程池状态值,为后面的检查做准备。 c = ctl.get(); } //如果大于核心线程数,检查一下线程池是否还处于运行状态,并尝试把任务放入到blockingQueue任务队列中。 if (isRunning(c) && workQueue.offer(task)) { //这里再次检查一下线程池的状态 int recheck = ctl.get(); if (! isRunning(recheck) && remove(task)) //如果线程池不处于运行状态的话,就把我们刚才添加进任务队列中的任务移出,并拒绝这个任务。 reject(task); //检查如果当前线程池中的线程数,如果为0了,就为线程池创建新线程(因为有可能之前存活的线程在上一次检查过后死亡了) else if (workerCountOf(recheck) == 0) addWorker(null, false); } //执行到这一句,说明队列满了。这时,如果当前线程池中的线程数还没有超过最大线程数,就创建一个新的线程去执行这个任务,如果失败就拒绝这个任务。 else if (!addWorker(task, false)) reject(task); }
六、可以返回结果的“Runnable”
我们知道ExecutorService框架使用Runnble作为其基本的任务表示形式。但是它有很大的局限性就是它不能返回一个值,或者抛出一个受检查的异常
实际上许多需要知道执行结果的任务都需要一定的执行时间的,比如执行数据库的查询,或者从网络上获取一些资源,更或者进行一些比较复杂的计算。对于这些任务Callable是一种更好的抽象。你可以把它当成有返回值的“Runnable”,它的call()方法就相当于Runnable的run()方法。但关键是它的call()方法将返回一个值,并可能抛出一个异常。
那么怎么在ExecutorService框架中很好的使用Callable呢。这里就需要使用到Feture接口。
ExecutorService中的所有submit方法都将返回一个Future对象,从而可以将Callable提交给ExecutorService,并得到一个Future用来获得任务的执行结果或者取消任务。
public class Test { private final ExecutorService executor = Executors.newFixedThreadPool(3); public void runTheTask(){ Future<String> future = executor.submit(new Callable<String>(){ @Override public String call() throws Exception { Thread.sleep(3000); return "result"; } }); try { System.out.println( future.get()); } catch (InterruptedException | ExecutionException e) { } } public static void main(String args[]){ Test t = new Test(); t.runTheTask(); } }
运行后在future.get()步会阻塞 直到3秒后 返回结果“result”
上面的线程只执行了一个Callable任务。
但有某些情景下需要我们执行好几个Callable任务,并且要获得它们的返回结果,代码就变得很不好控制了
public class Test { private final ExecutorService executor = Executors.newFixedThreadPool(3); public void runTheTask(){ Future<String> future1 = executor.submit(new Callable<String>(){ @Override public String call() throws Exception { Thread.sleep(3000); return "result1"; } }); Future<String> future2 = executor.submit(new Callable<String>(){ @Override public String call() throws Exception { Thread.sleep(3000); return "result2"; } }); Future<String> future3 = executor.submit(new Callable<String>(){ @Override public String call() throws Exception { Thread.sleep(3000); return "result3"; } }); try { System.out.println( future1.get()); System.out.println( future2.get()); System.out.println( future3.get()); } catch (InterruptedException | ExecutionException e) { } } public static void main(String args[]){ Test t = new Test(); t.runTheTask(); } }
可以看到 上面的代码非常难看而且不好控制。
幸运的是,在这种情况下我们可以使用CompletionService来实现
CompletionService将ExecutorService和BlockingQueue功能融合在了一起。
你可以将Callable任务提交给它来执行,它执行完返回的Future结果会放进BlockingQueue中。
你再用类似于队列操作的take和poll方法来获得已完成的结果。
public class Test2 { private final ExecutorService executor = Executors.newFixedThreadPool(3); public void runTasks(){ CompletionService<String> completionService = new ExecutorCompletionService<String>(executor); completionService.submit(new Callable<String>(){ @Override public String call() throws Exception { return "result1"; } }); completionService.submit(new Callable<String>(){ @Override public String call() throws Exception { return "result2"; } }); completionService.submit(new Callable<String>(){ @Override public String call() throws Exception { return "result3"; } }); for(int i=0;i<3;i++){ try { Future<String> f = completionService.take(); System.out.println(f.get()); } catch (InterruptedException e) { } catch (ExecutionException e) { } } } public static void main(String[] args) { Test2 t = new Test2(); t.runTasks(); } }
我们可以看到我们使用了一个ExecutorService作为参数来初始化CompletionService。多个CompletionService可以共享一个ExecutorService。因此可以创建一个对于特定计算私有,又能共享一个ExecutorService的应用。
七、线程池的关闭——shutdown()和shutdownNow()方法。
我们可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池,但是它们的实现原理不同,shutdown的原理是只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。对shutdown()方法的调用可以防止新的任务被提交给这个线程池,当前线程将继续运行在shutdown()被调用之前提交的所有任务
shutdownNow的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。shutdownNow会首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。
只要调用了这两个关闭方法的其中一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于我们应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow。
我们加入shutdown()和shutdownNow()方法来完善一下上面这个例子:
void shutdownAndAwaitTermination(ExecutorService pool) { pool.shutdown(); // 防止新的任务被提交上来 try { // 等待当前已经存在的任务执行完 if (!pool.awaitTermination(60, TimeUnit.SECONDS)) { pool.shutdownNow(); // 如果过了指定时间还有任务没有完成,立马停止它们 // 等待任务响应取消命令 if (!pool.awaitTermination(60, TimeUnit.SECONDS)) System.err.println("Pool did not terminate"); } } catch (InterruptedException ie) { // (Re-)Cancel if current thread also interrupted pool.shutdownNow(); // Preserve interrupt status Thread.currentThread().interrupt(); } }
八、线程池的合理配置
一、确定线程数
在工作中,为了加快程序的处理速度,我们需要将问题分解成若干个并发执行的任务。接着我们将这些任务委派给线程,以便使它们可以并发的执行。但是需要注意的是,由于资源所限,我们不能创建过多的线程。
这就涉及到一个 确定创建多少线程数才是合理 的问题。
《java虚拟机并发编程》一书中,对这个问题有详尽的解答,本人在此摘取归纳如下:
1.我们可以先获取到系统可用的处理器核心数:
Runtime.getRuntime().availableProcessors()
2.确定任务的类型:
如果所有任务都是计算密集型的,则创建处理器可用核心数那么多的线程数就可以了。
在这种情况下,创建更多的线程对程序的性能而言反而是不利的。因为当有多个任务处于就绪状态时,处理器核心需要在线程间频繁进行上下文切换,而这种切换对程序性能损耗较大。
如果任务都是IO密集型的,那么我们需要开更多的线程来提高性能。
当一个任务执行IO操作时,其线程被阻塞,于是处理器可以立即进行上下文切换以便处理其他就绪线程。如果我们只有处理器可用核心数那么多线程的话,则即使有待执行的任务也无法处理,因为我们已经拿不出更多的线程供处理器调度了。
3.计算出程序所需的线程数:
首先我们要明白一个概念叫 阻塞系数
如果任务有50%的时间处于阻塞状态,则阻塞系数为0.5。则程序所需的线程数为处理器可用核心数的两倍。如果任务被阻塞的时间少于50%,即这些任务是计算密集型的,则程序所需线程数将随之减少,但最少也不应该低于处理器的核心数。如果任务被阻塞的时间大于执行时间,即该任务是IO密集型的,我们就需要创建比处理器核心数大几倍数量的线程。
我们可以计算出程序所需线程的总数,总结如下:
线程数 = CPU可用核心数/(1 - 阻塞系数),其中阻塞系数的取值在0和1之间。
计算密集型人物的阻塞系数为0,而IO密集型任务的阻塞系数则接近1。
二、线程池的监控:
我们可以通过线程池提供的参数进行监控。线程池里有一些属性在监控线程池的时候可以使用
taskCount:线程池需要执行的任务数量。
completedTaskCount:线程池在运行过程中已完成的任务数量。小于或等于taskCount。
largestPoolSize:线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。
getPoolSize:线程池的线程数量。如果线程池不销毁的话,池里的线程不会自动销毁,所以这个大小只增不减。
getActiveCount:获取活动的线程数。
通过扩展线程池进行监控。通过继承线程池并重写线程池的beforeExecute,afterExecute和terminated方法,我们可以在任务执行前,执行后和线程池关闭前干一些事情。如监控任务的平均执行时间,最大执行时间和最小执行时间等。这几个方法在线程池里是空方法。如:
protected void beforeExecute(Thread t, Runnable r) { }
- JAVA线程池全解
- Java线程:什么是线程
- Java线程:线程池
- java线程--线程退出
- JAVA-线程/线程锁
- Java线程:什么是线程
- Java线程:线程中断
- Java线程:线程状态
- Java线程: 线程调度
- Java线程:线程交互
- java--线程--线程池
- java 线程
- Java线程
- java线程
- java线程
- Java线程
- Java线程
- java线程
- Servlet深入学习,规范,理解和实现(上)
- 搜索引擎常用技巧
- Swift初体验
- 数据结构_8:查找:线性查找
- 编程题#4: 字符串操作(C++程序设计第9周)
- JAVA线程池全解
- android的IP拨号器
- 【微信你妹】中间人攻击截获微信数据
- mysql之控制语句【整理】
- Allegro铜皮倒角技巧-shape倒角
- Java的身份证号码工具类
- 程序员之路的开始
- BZOJ 3144 HNOI 2013 切糕
- BZOJ 1086 裸-树分块