Java分布式应用学习笔记07线程池应用(又名:线程池与大排档)_JAVA_编程开发_程序员俱乐部

中国优秀的程序员网站程序员频道CXYCLUB技术地图
热搜:
更多>>
 
您所在的位置: 程序员俱乐部 > 编程开发 > JAVA > Java分布式应用学习笔记07线程池应用(又名:线程池与大排档)

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

 2011/9/9 8:14:45  suhuanzheng7784877  http://suhuanzheng7784877.iteye.com  我要评论(0)
  • 摘要:1.线程池是啥子一说到池子,大家都会想到数据库连接池那种对象池。本来嘛,现在倡导废物回收利用的节能环保绿色新社会嘛。其实线程池的初衷就是能将已经创建好了的线程对象重复利用。之前咱们说过对于一个分布式系统,离不开高并发、多线程的支持。那么无论是HTTP方式的,还是文件方式的,面对海量的客户端请求,作为服务端如果对于请求使用单线程阻塞方式显然是不可能的。JDK5之后为咱们提供了现成的线程池对象。我们用几个现成的JDK辅助类就能将线程对象池化。线程池实际上也是对象池的一种特例
  • 标签:笔记 学习 Java 应用 学习笔记

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

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

发表评论
用户名: 匿名