线程池概念
操作系统或者JVM创建一个线程以及销毁一个线程都需要消耗CPU资源,如果创建或者销毁线程的消耗源远远小于执行一个线程的消耗,则可以忽略不计,但是基本相等或者大于执行线程的消耗,而且需要创建大批量这种线程的话,CPU将资源将会大量消耗在创建线程和销毁线程上,这是不能接受的,因此我们需要一个集中管理线程的机制,那就是线程池。
线程池不仅仅可以预先批量创建线程,还可以管理和优化线程,一般来说,使用线程池有很多优点,
提前创建批量线程,减轻CPU负担
在必要情况下重用用过的线程,减少不必要的创建线程
管理,优化和调整线程的任务排队策略,任务拒绝策略。更科学地管理线程,节约资源
线程池管理是一个比较复杂的事情,这里暂不深入讨论,只对常见线程池做简单介绍。
JAVA创建线程需要使用工厂类Executors, 它提供了一些列的工厂方法来创建各种线程池
目前常见的线程池有,
- newFixedThreadPool 定长线程池
- newCachedThreadPool 可缓存线程池
- newSingleThreadExecutor 单线程化线程池
- newScheduledThreadPool 定时线程池
上面前三个工厂方法都是返回一个 ExecutorService类型的线程池,这种线程池会让线程尽快执行(有CPU资源就执行),第四个工厂方法返回一个ScheduledExecutorService类型的线程池,这种线程池会让线程在未来某个时间执行,时间可以设定,并且这种线程池还能让线程周期性地执行。
Executors中有两个执行线程的方法,一个是execute,提交线程对象表示将线程托管给线程池。还有一个是submit方法,其功能跟execute类似,不过submit方法可以获取线程返回值。
Executors还有两个结束线程的方法,一个是shutDown,可以停止创建新的线程,对于已经在执行的线程,会让任务执行完之后才结束线程,但不再创建新线程。
另一个就是shutDownNow,这是是自己结束线程。
下面看看这几种线程的解释和用法,
newFixedThreadPool - 定长线程池
这应该是最简单的线程池,维护固定数目的线程
下面演示用法, 我们定义一个容量为3的线程池,让希望提交5个线程进去,
1 package threads; 2 3 import java.util.concurrent.Callable; 4 import java.util.concurrent.ExecutorService; 5 import java.util.concurrent.Executors; 6 import java.util.concurrent.ScheduledExecutorService; 7 import java.util.concurrent.TimeUnit; 8 9 //定义一个线程类 10 class ThreadInPool implements Runnable { 11 private int index; 12 13 public int getIndex() { 14 return index; 15 } 16 17 public void setIndex(int index) { 18 this.index = index; 19 } 20 21 @Override 22 public void run() { 23 // TODO Auto-generated method stub 24 System.out.println(Thread.currentThread().getName()+" index = " + index); 25 } 26 27 } 28 29 public class ThreadPoolTest { 30 31 public static void fixedPool() { 32 ExecutorService pool = Executors.newFixedThreadPool(3); 33 ThreadInPool tp = new ThreadInPool(); 34 for (int i=0; i<5; i++) { 35 try { 36 Thread.sleep(i*1000); 37 } catch (InterruptedException e) { 38 e.printStackTrace(); 39 } 40 tp.setIndex(i); 41 pool.submit(tp); 42 } 43 pool.shutdown(); 44 } 45 46 public static void main(String[] args) { 47 ThreadPoolTest.cachedPool(); 48 } 49 50 }
执行结果,我们发现虽然我们依次提交5个线程进线程池,但是线程池只会创建三个线程,超过容量则会阻塞,等前面的线程完成任务之后
1 pool-1-thread-1 index = 0 2 pool-1-thread-2 index = 1 3 pool-1-thread-3 index = 2 4 pool-1-thread-1 index = 3 5 pool-1-thread-2 index = 4
newCachedThreadPool - 可缓存的线程池
这种线程池是只有在必要情况下(没有旧的可用线程)才会创建新线程,而且用过的线程还可以重复使用(任务已完成的情况下),但是不会限制线程池的容量。
下面演示这种线程池的使用, 在ThreadPoolTest类中添加一个静态方法用来创建newCachedThreadPool线程池,
1 public static void cachedPool() { 2 //ExecutorService类型的线程池 3 ExecutorService pool = Executors.newCachedThreadPool(); 4 ThreadInPool tp = new ThreadInPool(); 5 for (int i=0; i<5; i++) { 6 try { 7 Thread.sleep(i*1000); 8 } catch (InterruptedException e) { 9 e.printStackTrace(); 10 } 11 tp.setIndex(i); 12 pool.submit(tp); 13 } 14 pool.shutdown(); 15 }
特意在提交线程到线程池之前让main线程sleep一段时间,可以暂缓线程的提交时间,这样当第二个线程提交进线程池的时候,第一个线程就已经完成了,人为创造一个“线程重用”的条件,
下面是执行结果,可以看到,因此线程池每次都重用了上一次创建的线程,所以线程池中始终只有一个线程
1 pool-1-thread-1 index = 0 2 pool-1-thread-1 index = 1 3 pool-1-thread-1 index = 2 4 pool-1-thread-1 index = 3 5 pool-1-thread-1 index = 4
如果注释掉上面cachedPool方法第6到第10行,则线程之间完成时间都差不多,线程被重复使用的几率就小甚至没有了,
执行结果,可以看到创建了5个线程,
1 pool-1-thread-1 index = 2 2 pool-1-thread-4 index = 4 3 pool-1-thread-2 index = 2 4 pool-1-thread-3 index = 3 5 pool-1-thread-5 index = 4
newSingleThreadExecutor - 单线程化线程池
这种线程池始终只有一个线程能执行任务,任务按照指定顺序执行,演示如下,
在ThreadPoolTest中添加singlePool方法来创建一个newSingleThreadExecutor
1 private static void singlePool() { 2 ExecutorService pool = Executors.newSingleThreadExecutor(); 3 ThreadInPool tp = new ThreadInPool(); 4 for (int i=0; i<5; i++) { 5 tp.setIndex(i); 6 pool.submit(tp); 7 } 8 pool.shutdown(); 9 }
执行结果,可以看到线程池中只有一个线程,
pool-1-thread-1 index = 4 pool-1-thread-1 index = 4 pool-1-thread-1 index = 4 pool-1-thread-1 index = 4 pool-1-thread-1 index = 4
newScheduledThreadPool - 定时线程池
前面三种线程池里的线程在得到CPU资源的时候就会尽快执行,而newScheduledThreadPool会设定一个未来时间t,经过t时间后才开始执行,并且还支持周期性执行,
先来演示一下定时线程池,在ThreadPoolTest中添加scheuledPool方法如下,
设置线程池容量为2,同时提交10个线程进线程池托管,所有线程都设置3秒之后启动,
1 public static void scheuledPool() { 2 ScheduledExecutorService pool = Executors.newScheduledThreadPool(2); 3 ThreadInPool tp = new ThreadInPool(); 4 for(int i=0; i<10; i++) { 5 pool.schedule(tp, 3, TimeUnit.SECONDS); 6 } 7 pool.shutdown(); 8 }
执行结果,可以看到所有线程都在3秒之后才启动,虽然我们一次性启动10个线程,但是线程池中总共只有2个线程存在,这两个线程被重复使用了,
1 pool-1-thread-1 index = 0 2 pool-1-thread-1 index = 0 3 pool-1-thread-2 index = 0 4 pool-1-thread-2 index = 0 5 pool-1-thread-2 index = 0 6 pool-1-thread-2 index = 0 7 pool-1-thread-2 index = 0 8 pool-1-thread-2 index = 0 9 pool-1-thread-2 index = 0 10 pool-1-thread-1 index = 0
下面演示定时线程池周期性执行线程,修改scheduledPool如下,依然保持线程池的容量为2,设置线程1秒后启动,然后每隔100毫秒周期性执行,
1 public static void scheuledPool() { 2 ScheduledExecutorService pool = Executors.newScheduledThreadPool(2); 3 ThreadInPool tp = new ThreadInPool(); 4 pool.scheduleAtFixedRate(tp, 0, 100,TimeUnit.MILLISECONDS); 5 //pool.shutdown(); 6 }
执行结果,可以看到,只要不执行pool.shutdown(), 会周期性执行线程1和2,但是线程书目不会超过两个。
1 pool-1-thread-1 index = 0 2 pool-1-thread-1 index = 0 3 pool-1-thread-1 index = 0 4 pool-1-thread-1 index = 0 5 pool-1-thread-1 index = 0 6 pool-1-thread-2 index = 0 7 pool-1-thread-2 index = 0 8 pool-1-thread-2 index = 0 9 pool-1-thread-2 index = 0 10 pool-1-thread-2 index = 0 11 pool-1-thread-2 index = 0 12 pool-1-thread-2 index = 0 13 pool-1-thread-2 index = 0 14 pool-1-thread-1 index = 0 15 pool-1-thread-1 index = 0 16 pool-1-thread-1 index = 0 17 pool-1-thread-1 index = 0 18 pool-1-thread-1 index = 0
完整代码如下,
1 package threads; 2 3 import java.util.concurrent.Callable; 4 import java.util.concurrent.ExecutorService; 5 import java.util.concurrent.Executors; 6 import java.util.concurrent.ScheduledExecutorService; 7 import java.util.concurrent.TimeUnit; 8 9 //定义一个线程类 10 class ThreadInPool implements Runnable { 11 private int index; 12 13 public int getIndex() { 14 return index; 15 } 16 17 public void setIndex(int index) { 18 this.index = index; 19 } 20 21 @Override 22 public void run() { 23 // TODO Auto-generated method stub 24 System.out.println(Thread.currentThread().getName()+" index = " + index); 25 } 26 27 } 28 29 public class ThreadPoolTest { 30 public static void fixedPool() { 31 ExecutorService pool = Executors.newFixedThreadPool(3); 32 ThreadInPool tp = new ThreadInPool(); 33 for (int i=0; i<5; i++) { 34 try { 35 Thread.sleep(i*1000); 36 } catch (InterruptedException e) { 37 e.printStackTrace(); 38 } 39 tp.setIndex(i); 40 pool.submit(tp); 41 } 42 pool.shutdown(); 43 } 44 45 public static void cachedPool() { 46 //ExecutorService类型的线程池 47 ExecutorService pool = Executors.newCachedThreadPool(); 48 ThreadInPool tp = new ThreadInPool(); 49 for (int i=0; i<5; i++) { 50 /* 51 try { 52 Thread.sleep(i*1000); 53 } catch (InterruptedException e) { 54 e.printStackTrace(); 55 } 56 */ 57 tp.setIndex(i); 58 pool.submit(tp); 59 } 60 pool.shutdown(); 61 } 62 63 public static void scheuledPool() { 64 ScheduledExecutorService pool = Executors.newScheduledThreadPool(2); 65 ThreadInPool tp = new ThreadInPool(); 66 /* 67 for(int i=0; i<10; i++) { 68 pool.schedule(tp, 3, TimeUnit.SECONDS); 69 } 70 */ 71 pool.scheduleAtFixedRate(tp,1000, 100,TimeUnit.MILLISECONDS); 72 //pool.shutdown(); 73 } 74 75 private static void singlePool() { 76 ExecutorService pool = Executors.newSingleThreadExecutor(); 77 ThreadInPool tp = new ThreadInPool(); 78 for (int i=0; i<5; i++) { 79 tp.setIndex(i); 80 pool.submit(tp); 81 } 82 pool.shutdown(); 83 } 84 85 public static void main(String[] args) { 86 //ThreadPoolTest.fixedPool(); 87 //ThreadPoolTest.cachedPool(); 88 ThreadPoolTest.scheuledPool(); 89 //ThreadPoolTest.singlePool(); 90 } 91 92 93 }
对于线城池,阿里巴巴的Java开发规法有如下规定:
3. 【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资
源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者
“过度切换”的问题。
4. 【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor 的方式,这样
的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors返回的线程池对象的弊端如下:
1)FixedThreadPool和SingleThreadPool:
允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
2)CachedThreadPool和ScheduledThreadPool:
允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。