线程池

线程池Executor框架

        Executor采用工厂模式提供了各种类型的线程池,是实际使用中我们就直接从Executor中获取我们想要的线程池,拿来直接使用即可。下面简单的介绍下Executor提供的五大类线程池。

        newFixedThreadPool()方法:该方法返回一个固定数量的线程池;

        newSingleThreadExecutor()方法:该方法返回只有一个线程的线程池;

        newCachedThreadPool()方法:该方法返回一个可根据实际情况调整线程数量的线程池;

        newScheduledThreadPool()方法:该方法返回一个ScheduleExecutorService对象,可以指定线程数量;

        newSingleThreadScheduledExecutor()方法:该方法返回一个ScheduleExecutorService对象,线程数量为1;

        上面线程池分两大类,一类是无计划的任务,提交就会执行,这类业务应用很广泛;另一类是有计划的任务,提交后会按照设定的规则去执行,这种应用的场景相对少一些,不过对有些业务是必须的,比如:我们系统晚上需要清空用户的状态、优惠券到期了自动提醒等等,用到的就是这类计划任务,常见的有spring task。下面分别举例演示两种线程池的使用。

1、newFixedThreadPool()固定大小的线程池

[java]  view plain  copy
  1. package concurrent.threadpool;  
  2.   
  3. import java.util.concurrent.ExecutorService;  
  4. import java.util.concurrent.Executors;  
  5.   
  6. public class FixedThreadDemo {  
  7.   
  8.     public static void main(String args[]) {  
  9.         // 创建任务对象  
  10.         MyTask task = new MyTask();  
  11.         // 创建固定数量线程池  
  12.         ExecutorService es = Executors.newFixedThreadPool(5);  
  13.         for(int i=0; i<10; i++) {  
  14.             // 向线程池里提交任务  
  15.             es.submit(task);  
  16.         }  
  17.     }  
  18.       
  19.     public static class MyTask implements Runnable {  
  20.         @Override  
  21.         public void run() {  
  22.             System.out.println(System.currentTimeMillis() + ": Thread ID: " + Thread.currentThread().getId());    
  23.             try {  
  24.                 Thread.sleep(1000);  
  25.             }  
  26.             catch (InterruptedException e) {  
  27.                 e.printStackTrace();  
  28.             }  
  29.         }  
  30.     }     
  31. }  

2、newScheduledThreadPool()计划任务

[java]  view plain  copy
  1. package concurrent.threadpool;  
  2.   
  3. import java.util.concurrent.Executors;  
  4. import java.util.concurrent.ScheduledExecutorService;  
  5. import java.util.concurrent.TimeUnit;  
  6.   
  7.   
  8. public class ScheduleThreadDemo {  
  9.   
  10.     public static void main(String args[]) {  
  11.         // 创建任务对象  
  12.         MyTask task = new MyTask();  
  13.         // 创建任务调度计划对象  
  14.         ScheduledExecutorService ses = Executors.newScheduledThreadPool(100);  
  15.         // 设置任务与执行计划  
  16.         ses.scheduleAtFixedRate(task, 01, TimeUnit.SECONDS);  
  17.     }  
  18.       
  19.     public static class MyTask implements Runnable {  
  20.         @Override  
  21.         public void run() {  
  22.             System.out.println(System.currentTimeMillis()/1000 + ": Thread ID: " + Thread.currentThread().getId());   
  23.             try {  
  24.                 Thread.sleep(1000);  
  25.             }  
  26.             catch (InterruptedException e) {  
  27.                 e.printStackTrace();  
  28.             }  
  29.         }  
  30.     }  
  31. }  
        大家可以看的出来这两段代码都非常简单,都是创建任务对象、获取Executors提供的线程池对象、将任务对象绑定到线程池对象上。通过Executors提供的不同策略的对象,就能快速实现我们对线程的控制。

线程池的内部实现

        Executor为我们提供了功能各异的线程池,其实其内部均是由ThreadPoolExecutor实现的,我们详细了解下ThreadPoolExecutor实现原理不但对我们使用理解Executor提供的线程池大有帮助,也让我们能根据实际情况自定义特定的线程池。

        首先介绍下ThreadPoolExecutor类最核心的构造方法,

[java]  view plain  copy
  1. public ThreadPoolExecutor(  
  2.     int corePoolSize,       // 指定线程池中线程的数量(总线程量可大于等于这个值)  
  3.     int maximumPoolSize,    // 指定线程池中最大线程数量(总线程量不可能超越这个数值)  
  4.     long keepAliveTime,     // 超过corePoolSize数量的空闲线程,存活的时间  
  5.     TimeUtil unit,          // keepAliveTime的单位  
  6.     BlockingQueue<Runnable> workQueue,    // 任务队列,被提交但为执行的任务  
  7.     ThreadFactory threadFactory,          // 线程工厂,用于创建线程  
  8.     RejectedExecutionHandler handler      // 当workQueue队列满的时候的拒绝策略  
  9.     )  
        看到corePoolSize和maximumPoolSize的含义,应该很容易通过设置参数的不同,得到Executors提供的线程池对象。该方法一共七个参数,前四个很简单,我们都会使用,第六个一般使用的是JDK默认提供的,剩下的就只有workQueue和handler了。

        workQueue:存放的是提交的任务,例如:es.submit(task);在样例中提交了10次task,但线程只有5个,于是就有5个提交但没开始执行的任务存到了workQueue里啦。既然是一个存放任务的队列,我们知道实现队列的方式有多种,比如:ArrayBlockQueue、LinkedBlockQueue、PriorityBlockQueue等等,选择不同的队列就会带来不同的问题。ArrayBlockQueue,存在一个任务过多超出队列长度;LinkedBlockQueue,接受过多的任务可能会占用太多内存,造成内存崩溃等等。这里介绍下,newFixedThreadPool和newSingleFixedThreadPool使用的都是LinkedBlockQueue,newCacheThreadExecutor使用的SynchronousQueue队列。关于队列的选择是要根据实际情况来确定,这也是自定义线程池的核心。

        handler:拒绝策略,实际上是一种补救措施,就是当超出了workQueue临界值了,我们怎么让我们的系统不至于崩溃。JDK内置的处理方法有AbortPolicy,抛出异常阻止程序(除非是安全性要求极高,否则在大并发情况下使用这种做法不是很明智);DiscardPolicy,丢弃无法处理的任务(如果允许丢弃,这是不错方案);DiscardOledesPolicy:也是丢弃任务,只不过丢弃的是队列最前的一个任务。由于上面策略都是实现RejectExecutionHandler接口,我们也可以实现该接口自定义拒绝策略。


自定义线程创建

[java]  view plain  copy
  1. package concurrent.threadpool;  
  2.   
  3. import java.util.concurrent.ExecutorService;  
  4. import java.util.concurrent.Executors;  
  5. import java.util.concurrent.LinkedBlockingQueue;  
  6. import java.util.concurrent.RejectedExecutionHandler;  
  7. import java.util.concurrent.ThreadPoolExecutor;  
  8. import java.util.concurrent.TimeUnit;  
  9.   
  10. public class ThreadPoolDemo {  
  11.   
  12.     public static void main(String args[]) {  
  13.         // 创建任务对象  
  14.         MyTask task = new MyTask();  
  15.         // 获取自定义线程池  
  16.         ExecutorService es = getMyThreadPool();  
  17.         for(int i=0; i<20; i++) {  
  18.             // 向线程池提交任务  
  19.             es.submit(task);  
  20.         }  
  21.     }  
  22.       
  23.     // 自定义线程池,我们创建一个线程数固定的线程池  
  24.     public static ExecutorService getMyThreadPool() {  
  25.         ExecutorService es = new ThreadPoolExecutor(  
  26.             // 设置线程池大小  
  27.             55, 0L, TimeUnit.MILLISECONDS,   
  28.             // 设置缓存队列  
  29.             new LinkedBlockingQueue<Runnable>(5),  
  30.             // 设置线程工厂  
  31.             Executors.defaultThreadFactory(),  
  32.             // 设置拒绝策略  
  33.             new RejectedExecutionHandler() {  
  34.                 @Override  
  35.                 public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {  
  36.                     System.out.println(r.toString() + " is discard! ");    // 输出日志后直接丢弃任务  
  37.                 }  
  38.             });  
  39.         return es;  
  40.     }  
  41.       
  42.     public static class MyTask implements Runnable {  
  43.         @Override  
  44.         public void run() {  
  45.             System.out.println(System.currentTimeMillis() + ": Thread ID: " + Thread.currentThread().getId());    
  46.             try {  
  47.                 Thread.sleep(1000);  
  48.             }  
  49.             catch (InterruptedException e) {  
  50.                 e.printStackTrace();  
  51.             }  
  52.         }  
  53.     }     
  54. }  
        从上面程序的运行结果我们可以看到,10任务被执行(因为线程池有5个线程,缓存队列也能缓存5个),10任务被丢弃,符合我们预期。这个样例十分简单,只是通过这个样例展示怎么去自定义一个线程池,具体的线程池定义,我们要根据实际情况,设置传入的参数即可。


Fork/Join框架

        上面我们详细介绍了线程池的原理,还是那句话,学底层原理是拿来做设计,并不是让直接去使用。Fork/Join在线程池的基础上,做了更近一步的封装,对线程的开启、分发做了优化,使系统更稳定。另外补充下,Fork/Join还涉及到关于多线程的一个重要思想:“分而治之”,通俗的将就是将复杂的问题拆分成多个简单问题,分开处理。下面通过一段样例了解下Fork/Join。

[java]  view plain  copy
  1. package concurrent.threadpool;  
  2.   
  3. import java.util.ArrayList;  
  4. import java.util.concurrent.ExecutionException;  
  5. import java.util.concurrent.ForkJoinPool;  
  6. import java.util.concurrent.ForkJoinTask;  
  7. import java.util.concurrent.RecursiveTask;  
  8.   
  9. public class ForkJoinDemo extends RecursiveTask<Long> {  
  10.       
  11.     private static final int THRESHOLD = 10000;    // 门阀值,当大于这个值才进行拆分处理  
  12.     private long start;                             // 数列的起始值  
  13.     private long end;                               // 数列的结束值  
  14.   
  15.     public ForkJoinDemo(long start, long end) {  
  16.         this.start = start;  
  17.         this.end = end;  
  18.     }  
  19.       
  20.     // 对数列进行求和  
  21.     @Override  
  22.     protected Long compute() {  
  23.         // 定义求和对象  
  24.         long sum = 0;  
  25.         if((end - start) < THRESHOLD) {  
  26.             // 当求和数列的数量小于门阀值,则直接计算不需要分拆  
  27.             for(long i=start; i<=end; i++) {  
  28.                 sum += i;  
  29.             }  
  30.         }  
  31.         else {  
  32.             // 当求和数列的数量大于门阀值,则拆分成100个小任务  
  33.             ArrayList<ForkJoinDemo> subTasks = new ArrayList<ForkJoinDemo>();   
  34.             long step = (start + end) / 100;    // 计算每个小任务的数列的数量  
  35.             long firstOne = start;              // 动态记录小任务数列的起始值  
  36.             // 将任务拆分,并提交给框架  
  37.             for(int i=0; i<100; i++) {  
  38.                 long lastOne = firstOne + step; // 动态记录小任务数列的结束值  
  39.                 if(lastOne > end) {  
  40.                     // 当队列结束值大于end,需要将lastOne设置为end  
  41.                     lastOne = end;  
  42.                 }  
  43.                 ForkJoinDemo subTask = new ForkJoinDemo(firstOne, lastOne);  
  44.                 firstOne = firstOne + step + 1;  
  45.                 // 将子任务添加到数组中  
  46.                 subTasks.add(subTask);  
  47.                 // 执行子任务,这里是将子任务交个Fork/Join框架,什么时候开始执行由框架自己决定  
  48.                 subTask.fork();  
  49.             }  
  50.             // 将子任务的计算结果汇总  
  51.             for(ForkJoinDemo st : subTasks) {  
  52.                 sum += st.join();  
  53.             }  
  54.         }  
  55.         return sum;  
  56.     }  
  57.       
  58.     public static void main(String args[]) {  
  59.         // 创建任务对象  
  60.         ForkJoinDemo task = new ForkJoinDemo(0, 500000L);  
  61.         // 创建Fork/Join线程池  
  62.         ForkJoinPool forkJoinPool = new ForkJoinPool();  
  63.         // 将任务提交给线程池  
  64.         ForkJoinTask<Long> result = forkJoinPool.submit(task);  
  65.         try {  
  66.             // 取出运算结果  
  67.             long sum = result.get();  
  68.             System.out.println("sum: " + sum);  
  69.         } catch (InterruptedException | ExecutionException e) {  
  70.             e.printStackTrace();  
  71.         }  
  72.     }     
  73. }  
       
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值