`
suhuanzheng7784877
  • 浏览: 691786 次
  • 性别: Icon_minigender_1
  • 来自: 北京
博客专栏
Ff8d036b-05a9-33b5-828a-2633bb68b7e6
读金庸故事,品程序人生
浏览量:47239
社区版块
存档分类
最新评论

Java分布式应用学习笔记07线程池应用(又名:线程池与大排档)

阅读更多

1.  线程池是啥子

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

2.  为什么用这玩意

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

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

3.  怎么用这玩意

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

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.首先咱们就上面那个程序的例子,上面那个程序创建线程池是如下语句

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

 

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

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

 

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

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

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

 

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

 

底层为

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

 细心地读者发现了,这个和调用

ExecutorService exec = Executors.newFixedThreadPool(1)

 

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

3.还有就是缓存线程池,创建方式如下

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

 

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

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

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

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

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

58
4
分享到:
评论
19 楼 yuediaoyuan0809 2011-09-13  
搞笑撒      
18 楼 sunshine09120 2011-09-10  
之前写过一个thread pool,后来才知道JDK里面自带,悲剧了。
17 楼 tina_soqal4891 2011-09-09  
suhuanzheng7784877 写道
hufei023090 写道
这么好的文章都踩,谁那么无聊

分享的目的达到了就好了,我也是想让大家挑错的。至于顶还是踩,我不在乎。说真的,我真希望我没那么排名靠前,烦恼啊……。达到共同进步的目的就好喽。

楼主不必过谦,我一直学习你的博客内容,希望你能及时更新,对我工作挺有促进作用的。
16 楼 suhuanzheng7784877 2011-09-09  
hufei023090 写道
这么好的文章都踩,谁那么无聊

分享的目的达到了就好了,我也是想让大家挑错的。至于顶还是踩,我不在乎。说真的,我真希望我没那么排名靠前,烦恼啊……。达到共同进步的目的就好喽。
15 楼 hufei023090 2011-09-09  
这么好的文章都踩,谁那么无聊
14 楼 suhuanzheng7784877 2011-09-09  
ma_xuezhi1987 写道
jiang_yun198701 写道
引用
在厕所里面方面……

是方便吧大哥。

哈哈,lz又来了,继续力挺你的搞笑风格

ma_xuezhi1987 写道
jiang_yun198701 写道
引用
在厕所里面方面……

是方便吧大哥。

哈哈,lz又来了,继续力挺你的搞笑风格

谢谢,希望兄弟你也能提出质疑与批评。
13 楼 ma_xuezhi1987 2011-09-09  
jiang_yun198701 写道
引用
在厕所里面方面……

是方便吧大哥。

哈哈,lz又来了,继续力挺你的搞笑风格
12 楼 suhuanzheng7784877 2011-09-08  
你若无情我便休 写道
lz的技术文章有日子没更新了啊。虽然看不懂,不过还是顶一个。

哦,sorry,这位兄弟没有看懂吗?看来我表达上还很欠缺,有待加强。
11 楼 suhuanzheng7784877 2011-09-07  
现已修正如下:

引用
也就是说之前露天的大排档吃完的地方在60s内还可以重复利用。服务员在60s后发现依然没人占用地方。ok,回收之。但是,如果此时线程池依然很忙碌,没有可复用的线程资源,那没办法,只能重新创建一个新线程并添加到池中。
10 楼 suhuanzheng7784877 2011-09-07  
tag13346 写道
newCachedThreadPool关于60s的说明貌似有偏差,以下引自jdk1.6的API。
public static ExecutorService newCachedThreadPool()
如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。

按照LZ的解释应该是,空闲60s的桌子被收回,而不是坐了60s不管有没吃完都要收回。

太感谢你了,不错,是我理解得不对~
重新解释一下,就是那些60s内没有人坐的桌椅回收。如果在规定时间内——60s,空闲线程是可以被复用的。太感谢了。哈哈,这就是笔者写出来的目的啊!!太感谢了。
9 楼 tag13346 2011-09-07  
newCachedThreadPool关于60s的说明貌似有偏差,以下引自jdk1.6的API。
public static ExecutorService newCachedThreadPool()
如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。

按照LZ的解释应该是,空闲60s的桌子被收回,而不是坐了60s不管有没吃完都要收回。
8 楼 你若无情我便休 2011-09-07  
lz的技术文章有日子没更新了啊。虽然看不懂,不过还是顶一个。
7 楼 heavensay 2011-09-07  
suhuanzheng7784877 写道
jiang_yun198701 写道
引用
在厕所里面方面……

是方便吧大哥。

哦,谢谢,不好意思,键盘不好使了,打字快了点……

     其实楼主的意思是 吃方便面
6 楼 qijinha_toni 2011-09-07  
引用
带着老婆,唱着歌,吃着火锅然后服务员突然跟您说:“对不起,先生,您time out了。”
我怎么觉得这句话那么耳熟啊。
5 楼 xun_nieONE 2011-09-07  
很好士大夫撒大法师打发
4 楼 suhuanzheng7784877 2011-09-07  
zhanghaiyangruijie 写道
哥哥,你总结的真好。小弟收益匪浅。谢谢了。

啊?不能吧~~这个仅仅是介绍而已,并没太复杂的技术深入。
3 楼 zhanghaiyangruijie 2011-09-07  
哥哥,你总结的真好。小弟收益匪浅。谢谢了。
2 楼 suhuanzheng7784877 2011-09-07  
jiang_yun198701 写道
引用
在厕所里面方面……

是方便吧大哥。

哦,谢谢,不好意思,键盘不好使了,打字快了点……
1 楼 jiang_yun198701 2011-09-07  
引用
在厕所里面方面……

是方便吧大哥。

相关推荐

Global site tag (gtag.js) - Google Analytics