`

池链接

 
阅读更多
1.  线程池是啥子

一说到池子,大家都会想到数据库连接池那种对象池。本来嘛,现在倡导废物回收利用的节能环保绿色新社会嘛。其实线程池的初衷就是能将已经创建好了的线程对象重复利用。之前咱们说过对于一个分布式系统,离不开高并发、多线程的支持。那么无论是HTTP方式的,还是文件方式的,面对海量的客户端请求,作为服务端如果对于请求使用单线程阻塞方式显然是不可能的。JDK5之后为咱们提供了现成的线程池对象。我们用几个现成的JDK辅助类就能将线程对象池化。线程池实际上也是对象池的一种特例,对象池主要是池化那些创建比较费劲、耗费资源比较大的大对象,比如你的重量级UI组件,比如你的IO文件对象。当然了,如果您的企业不差钱,买得起优越的服务器,仅仅针对一个不到100并发场景的小场景,每个数据服务终端都是当时很顶级的服务器,50w的IBM服务器,那另当别论,您愿意怎么折腾就怎么折腾!别以为笔者在这里开玩笑,确实有这样的现象。某政府的电子政务系统,花的纳税人的钱,买的都是高档服务器,配置的集群环境。最后的使用量,并发量……唉,花纳税人的钱就是不痛不痒啊,不提了,那个面子工程……

2.  为什么用这玩意

言归正传,我们来看为什么要用线程池这个东西。上面我们提到了对象池,对象池的目的就是为了重复利用创建起来比较复杂的大对象的。那么线程呢?线程对象不大的话也要用池子管理它吗?实际上线程池适合于短时间运行的、并发量比较大的线程对象场景。就好比说你去快餐店吃东西,快餐店不可能为每一个顾客都布置一个桌子,一把椅子,固定就那么10几个座位。您买完了一个汉堡,一杯可乐,在座位上吧唧吧唧2口,喝口水,ok,擦擦嘴,抬屁股走人即可。从买餐到坐下吃饭,不到5分钟。因为这家快餐店是知名品牌,有很高的客户群体。没办法,对于每个客户而言就那么珍贵的5分钟啊。轻轻的您走了,正如您轻轻地来。线程池就是适用于以上这种场景,并发量非常巨大,但是每个访问的时间又是极短的。如果是每次访问需要的时间比较多的情况下就不适合这种场景,就适合建立新线程对象的场景。就好比说您现在关心吃饭的质量,吃饭喜欢细嚼慢咽,带着老婆,唱着歌,吃着火锅然后服务员突然跟您说:“对不起,先生,您time out了。”您怎么想。这种场景就适合大排档,所有客户都是露天桌椅,如果来了新客户就应该在外面搭个桌子(new一个新线程),招呼客户去外面吃大排档了。

如果线程对象请求的时间过长,那么很多新线程对象都在线程等待队列中等着。等待的队伍越来越长不说,客户端也一直处于等待状态,总有一个时间,客户端会没有耐心的。那么我们在开发中,大概在什么情况下使用线程池来维护线程对象呢。

3.  怎么用这玩意

讲了线程池是什么,又描述了线程池的使用场景,那么如何使用线程池呢。咱们先来看一个实例
Java代码  收藏代码

   1. package threadPool; 
   2.  
   3. import java.util.concurrent.ExecutorService; 
   4. import java.util.concurrent.Executors; 
   5.  
   6. public class TestThreadPool { 
   7.  
   8.     /**
   9.      * @param args
  10.      */ 
  11.     public static void main(String[] args) { 
  12.  
  13.         // 创建线程池 
  14.         ExecutorService exec = Executors.newFixedThreadPool(3); 
  15.  
  16.         // 建立100个线程 
  17.         for (int index = 0; index < 100; index++) { 
  18.             Runnable run = new Runnable() { 
  19.                 public void run() { 
  20.  
  21.                     // 随机毫秒 
  22.                     long time = (long) (Math.random() * 500); 
  23.                     System.out.println("Sleeping " 
  24.                             + Thread.currentThread().getName() + ":" + time 
  25.                             + "ms"); 
  26.                     try { 
  27.                         Thread.sleep(time); 
  28.  
  29.                     } catch (InterruptedException e) { 
  30.                         e.printStackTrace(); 
  31.                     } 
  32.  
  33.                 } 
  34.  
  35.             }; 
  36.              
  37.             //执行 
  38.             exec.execute(run); 
  39.         } 
  40.          
  41.         //关闭 
  42.         exec.shutdown(); 
  43.  
  44.     } 
  45. } 

package threadPool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestThreadPool {

/**
* @param args
*/
public static void main(String[] args) {

// 创建线程池
ExecutorService exec = Executors.newFixedThreadPool(3);

// 建立100个线程
for (int index = 0; index < 100; index++) {
Runnable run = new Runnable() {
public void run() {

// 随机毫秒
long time = (long) (Math.random() * 500);
System.out.println("Sleeping "
+ Thread.currentThread().getName() + ":" + time
+ "ms");
try {
Thread.sleep(time);

} catch (InterruptedException e) {
e.printStackTrace();
}

}

};

//执行
exec.execute(run);
}

//关闭
exec.shutdown();

}
}



以上程序先创建了一个线程池,Executors可以创建各式各样的线程池,这个我们后面再说,先不去管它,就知道先创建了个线程池即可。在构造参数里限定线程池的容量是3个,也就是说线程池允许的活动状态线程个数是3个(这家快餐馆也太小了,就给开了3个座位,不够看美女的啊~),在同一时间内,3个线程任务争抢CPU资源执行任务。这里每一个Runnable对象相当于一个要吃饭的人,他们必须有桌子才能吃饭,而线程池为他们提供了桌子、椅子,Runnable们有了桌子、椅子才能吃东西。其他为活动的线程都在线程队列中等待着。最后调用关闭线程池的方法shutdown();,如果不调用该方法那么线程池还可以继续运行线程对象的任务,如果调用了再执行线程对象会抛出java.util.concurrent.RejectedExecutionException。拒绝执行线程任务。

4.  几个扩展线程池

从以上程序执行情况来看实际上是ThreadPoolExecutor对象负责执行,代码段Executors.newFixedThreadPool(3)是创建ThreadPoolExecutor对象的。只是ThreadPoolExecutor对象有很多有参的构造函数,合理的利用ThreadPoolExecutor的构造函数成为了使用线程池,创建线程池的关键点。Executors提供了很多的静态方法来创建ThreadPoolExecutor对象,其底层实质上就是调用ThreadPoolExecutor不同的构造函数,或者在相同构造函数上使用不同的实参来对外进行服务。这样对于客户端调用者来说不必记住那些复杂的参数含义,按需调用Executors静态方法就能获得自己想要的线程池执行对象,大大简化了创建线程池的难度。这也是装饰器模式的体现,把复杂的东西对客户端以一种简单的方式呈现出来。客户端不必被那些繁琐的参数、创建过程吓到。

1.首先咱们就上面那个程序的例子,上面那个程序创建线程池是如下语句
Java代码  收藏代码

   1. // 创建线程池 
   2. ExecutorService exec = Executors.newFixedThreadPool(3); 

// 创建线程池
ExecutorService exec = Executors.newFixedThreadPool(3);



它实际上是调用了如下代码
Java代码  收藏代码

   1. public static ExecutorService newFixedThreadPool(int nThreads) { 
   2.   return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); 
   3. } 

public static ExecutorService newFixedThreadPool(int nThreads) {
  return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}



大家看到了,客户端调用的时候仅仅传入了一个参数——活动线程的个数。

之前使用newFixedThreadPool的效果大家看到了,在一个时刻,一直是3个线程任务再争抢资源,如果这个时候来新的线程任务了,那么会将其放到LinkedBlockingQueue中放着。LinkedBlockingQueue的大小是整型的最大数——2147483647。这也是最普通的使用场景。限制线程的执行数量,也减少了高并发下多线程之间的资源切换争抢时间。

2.下面是一个仅单线程池的创建例子


Java代码  收藏代码

   1. // 创建线程池 
   2. ExecutorService exec = Executors.newSingleThreadExecutor(); 

// 创建线程池
ExecutorService exec = Executors.newSingleThreadExecutor();



底层为
Java代码  收藏代码

   1.  public static ExecutorService newSingleThreadExecutor() { 
   2.     return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); 
   3. } 

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()));
}

细心地读者发现了,这个和调用
Java代码  收藏代码

   1. ExecutorService exec = Executors.newFixedThreadPool(1) 

ExecutorService exec = Executors.newFixedThreadPool(1)



没什么区别,这里需要说明的是它的使用场景,这个是单线程的线程池,在一个时刻只能一个线程任务去执行,这个具体的场景在哪里?其实根据实际情况来看,它更适合于做多任务的汇总,这个多任务之间没有任何层次关系,都是并列的。比如云计算节点单机A、节点B、节点C月底凌晨要做流量汇总,运维的时候是要按需收费的!这个时候就需要每个节点机器的用户appid、流量、吞吐汇报上来做报表汇总。此时数据上报是多线程任务的,从每个节点接收成功后启动计算功能的线程,之后将计算线程逐一放到这个单一的线程池进行计算、入本地库、记日志、单节点的成本BI计算等等操作完成后,下一个node的线程再进行汇总。使用单一线程考虑到2方面,第一每条数据都要严格的进行log记录,以便进行智能的统计BI分析,第二就是在统计的主线程中,需要用到种种局部临时变量,所以这种情况是最适合不过了。当然了,读者要是问我有其他方式解决吗?当然有其他方式。我们这里仅仅讨论newSingleThreadExecutor的适用场景,至少在此场景下newSingleThreadExecutor是解决方案之一。抱歉的是,因为一些特殊的原因,笔者不能将这部分实例代码分享出来,涉及到一些其他利益,大家懂得的,呵呵。

3.还有就是缓存线程池,创建方式如下
Java代码  收藏代码

   1. // 创建线程池 
   2. ExecutorService exec = Executors.newCachedThreadPool(); 

// 创建线程池
ExecutorService exec = Executors.newCachedThreadPool();



当放入池中的新线程发现没有可复用的池对象的时候,那么就创建一个新的线程对象放入池中。在一定的时间,默认是60秒后,线程池会自动将其赶出池子,也就是说之前露天的大排档吃完的地方在60s内还可以重复利用。服务员在60s后发现依然没人占用地方。ok,回收之。但是,如果此时线程池依然很忙碌,没有可复用的线程资源,那没办法,只能重新创建一个新线程并添加到池中。

相比较咱们之前的newFixedThreadPool方法,它最大的特点就是回收资源、清除超时(60s)、闲置的线程资源,因此它占用系统资源方面不是很大。而newFixedThreadPool方法,只要线程池不关闭,池子里面的资源不会回收。也就是,您哪怕是买一杯麦当劳的咖啡,也可以在里面占一个座位,喝上一整天,看一整天的书,没办法,谁让咱这个线程执行得慢呢。

4.最后一个要介绍的是类似于Timer类的ScheduledExecutorService,这个就是属于使用周期性任务的线程了,大家对Timer或者任务调度已经很明白了,不再赘述了,这里就将网友的一段代码示例给大家即可
Java代码  收藏代码

   1. ScheduledExecutorService executor = Executors 
   2.         .newScheduledThreadPool(10); 
   3.  
   4. Runnable task = new Runnable() { 
   5.  
   6.     @Override 
   7.     public void run() { 
   8.  
   9.         System.out.println("task over"); 
  10.  
  11.     } 
  12.  
  13. }; 
  14.  
  15. executor.scheduleAtFixedRate(task, 10, 2, TimeUnit.SECONDS); 

ScheduledExecutorService executor = Executors
.newScheduledThreadPool(10);

Runnable task = new Runnable() {

@Override
public void run() {

System.out.println("task over");

}

};

executor.scheduleAtFixedRate(task, 10, 2, TimeUnit.SECONDS);



executor.scheduleAtFixedRate(task, 10, 2, TimeUnit.SECONDS);的意思就是线程放到周期线程池中,第一次延迟10秒执行,每过2秒则执行一次任务。

5.  总结

线程池的使用其实还可以深入,如果自己感兴趣也可以造造这个轮子。笔者觉得比较难的是创建线程池的时候的创建参数,上面集中线程池是已有的,适合大多数线程池场景的创建方法,如果在特殊情况下需要自己手工new线程池,参数的选择,直接影响了线程池的效率以及功能。最后还是唠叨一下,线程池适合执行短时间连接请求的线程任务,并且访问量比较巨大的情况。对于消耗时间比较长的连接,您最好还是用别的办法。



PS:时间长短是相对来说的,一个笑话:“您觉得一分钟是长还是短?那得看您是在厕所外面排队还是在厕所里面方便……”

各位网友原谅我将其放到了“分布式”的大标题下面,笔者想将其弄成一个系列而已。
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics