线程池的简介、应用和手动实现线程池

Java线程池原理与实践
本文通过实例对比,深入浅出地介绍了Java线程池的设计理念与实现方法,包括线程池提升程序性能的原因、自定义线程池的具体步骤及优化方向。

一 简介

java中接触到"池"这个概念的地方有不少。最开始是常量池、数据库连接池、线程池... 对于某些发散思维很强或者善于举例的人来说,可能会想方设法将java的某些概念和生活中的某些事物进行比拟,好加深理解。惭愧的是,关于生活中的"池",我最先想到的是水池、化粪池(我是农村银)... ... 

再后来,有了一些经验之后。才慢慢体会到。编程语言中的"池",侧重强调的不是作为容器的存储功能,而是容器中的元素能够重复利用的功能。

java中有线程池的概念,jdk也有相应的实现类。本文并非探讨JDK源码,大失所望者请勿浪费时间往下看。我一向不喜欢看别人的代码。不是狂妄。而是不希望自己的想法被别人的思路绑架。至少要等我自己的代码出现了很大的问题,严重碰壁的时候,才去学习借鉴别人的想法。

1 为什么要使用线程池呢?

答:线程池可以提高程序的性能。

2 使用了多线程本身不就是可以提高性能了吗?为什么线程池可以提高性能?

答前半个问题:多线程不一定提高性能,甚至反而降低了性能。多线程环境中,如果每个线程run方法中运行的代码耗时比较少,少于创建、启动该线程所消耗的时间。使用多线程就是一种浪费

场景一 :创建线程、开启线程、线程开始执行这段耗时  远远大于 线程的运行时间。

单线程搜索文件和多线程搜索文件性能对比:

单线程:

/**
 * 单线程搜索文件。
 * @author Administrator
 *
 */
public class SingleThreadSearchFile {
	public static void main(String[] args) {
		long t1 = System.currentTimeMillis();
		//在 G:\\代码库备份  这个目录中搜索包含"笔记"的文件夹或文件。并打印出来。
		searchFile(new File("G:\\代码库备份"), "笔记"); 
		long t2 = System.currentTimeMillis();
		System.out.println("耗时:"+(t2-t1)+"ms"); 
		 
	}
	public static void searchFile(File file,String keyword){
		if(file.isDirectory()){
			 File []files = file.listFiles();
			 if(files!=null){
				 isKeyWordContained(file, keyword);//搜索业务。
				 for(File f : files){
					 searchFile(f, keyword);
				 }
			 }
		}else{ 
			isKeyWordContained(file, keyword);//搜索业务。
		}
	}
	public static void isKeyWordContained(File file,String keyword){
		 int index = file.getName().indexOf(keyword);
		 if(index !=-1){
			 System.out.println(file.getAbsolutePath());
		 } 
	}
}

多线程:

/**
 * 
 * @author Administrator
 *
 */
public class MultiThreadSearchFile {
	public static void main(String[] args) throws InterruptedException {
		long t1 = System.currentTimeMillis();
		//在G:\\代码库备份  这个目录中搜索包含"笔记"的文件夹或文件。并打印出来。
		searchFile(new File("G:\\代码库备份"), "笔记"); 
		long t2 = System.currentTimeMillis();
		System.out.println("耗时:"+(t2-t1)+"ms"); 
	}
	public static void searchFile(File file,String keyword){
		if(file.isDirectory()){
			 File []files = file.listFiles();
			 if(files!=null){
				 isKeyWordContained(file, keyword);
				 //取出目录名,判断是否包含关键字。
				 for(File f : files){
					 searchFile(f, keyword);
				 }
			 }
		}else{//是否文件。
			isKeyWordContained(file, keyword);
		}
	}
	//被调用一次,则开一个线程来查找。所谓的查找其实仅仅是获取文件名,与关键字进行简单的比对。
	public static void isKeyWordContained(File file,String keyword){
		 Thread t = new Thread(){
			 public void run(){
				 int index = file.getName().indexOf(keyword);
				 if(index !=-1){
					 System.out.println(getName()+"  "+file.getAbsolutePath());
				 }
			 }
		 };
		 t.start();
		try { 
		  t.join();
		}catch (InterruptedException e) {  }
		 
	}
}

对比结果:相同的目录查找相同的结果。单线程60ms多线程220--400ms变动。单线程完胜。原因很简单:我们虽然使用了多线程,但是多线程里面要做的事情仅仅是几行简单的不耗时的代码。多线程同运行run方法带来的优势已经被创建线程、开启线程所需要的时间给抵消掉了。

场景二 创建线程、开启线程、线程开始执行这段耗时  远远 小于 线程的运行时间

多线程的应用场景在于,执行那些需要耗时的操作。例如,使用多线程 + Socket实现的Http服务器。每个连接都开启一个线程来处理请求响应。由于存在网络延时。因此多线程的优势体现出来。

再如,将上面的案例需求修改如下:搜索某个文件夹中,文件内容 包含指定关键字的.txt, .java , .js文件。

单线程的业务代码改为:

public static void isKeyWordContained(File file,String keyword){
		  if(file.getName().endsWith(".java") || file.getName().endsWith(".txt")||
				  file.getName().endsWith(".js")){
			  //创建流对文件内容进行读取,并且判断。
		  }
	}

多线程业务代码改为:

public static void isKeyWordContained(File file,String keyword){
		 Thread t = new Thread(){
			 public void run(){
				if(file.getName().endsWith(".java") || file.getName().endsWith(".txt")||
				  file.getName().endsWith(".js")){
			       //创建流对文件内容进行读取,并且判断。
		        }
			 }
		 };
		 t.start();
		try { 
		  t.join();
		}catch (InterruptedException e) {  }
	}

由于使用IO读取文件是个相对耗时的操作,此时多线程的优势展示出来了。

答后半个问题:经过上述分析,已经得出了个结论。如果不使用线程池,由于创建线程、开启线程过程中,JVM要为每个线程分配内存空间,相对耗时,所以不一定提高性能。如果能将线程放入一个池里,不需要每次都开启和运行。就在一定程度上提高性能

二 想法

如果实现线程池?回想线程池的作用:

①首先得是个容器,容器里面装有线程。可以是Thread[]或者List<Thread>

其次容器中的线程还得重复利用,意味着线程不死,如何不死?最粗暴的方法是run方法中死循环(一定条件下可调出)

矛盾:那如何让线程执行自定义的代码,自定义的代码放在哪里?

答案是:任务。

这里引出任务的概念,这里的任务,是我们自定义个一个接口。

public interface  MyTask {
	//被调用。被线程池中的线程调用。
	public  void execute()throws Exception;
}

我们的代码放在哪里呢?就放在子类的execute方法里。

即,我们创建一个实现类,把代码放在execute方法中。然后创建该类的对象,一个类就表示一个任务。

好啦,到这里,整理一下我们线程池的所有流程:

应该要有一个线程池管理类,里面包含着任务列表,MyTask[]或者List<MyTask>,也可以用阻塞队列。

也包含了线程列表。并且线程可以运行状态。

使用线程池时,只需要创建任务对象,把代码写好,把对象丢进线程池。

理想的编程方式如下:

//创建线程池。
//创建任务1 ,并加入线程池。
//创建任务2,并加入线程池
//创建任务3,并加入线程池
...

       伪代码如下:
       //1 创建线程池。指定池里有5个正在运行的线程(活跃)
		MyThreadPools pools  = new MyThreadPools(5);
		//2 创建任务列表。并加入线程池。
		Random r = new Random();
		for(int i=0;i<20;i++){
			MyTask t = new MyTask() {
				@Override
				public void execute() throws Exception{
					 System.out.println("产生随机数:"+r.nextInt());
				}
			};
			pools.submit(t);
		}

三 代码

完整代码,2个类和一个接口。

MyTask.java代码如下

public interface  MyTask {
	//被调用。被线程池中的线程调用。
	public  void execute()throws Exception;
}

MyThreadPools.java代码如下

/**
 * 线程池。管理类。
 * @author Administrator
 *
 */
public class MyThreadPools {
    //实际应用中,可将以下两个换成阻塞队列。这样就避免在run方法中手动使用synchronized关键字来同步。
	private  List<Thread> threadList = new ArrayList<Thread>();//线程池的线程列表。
	private  List<MyTask> mytasklist = new LinkedList<MyTask>();//任务列表。
	//num表示线程池中线程的总数。
	public MyThreadPools(int num){
		for(int i=0;i<num;i++){
			Thread t = new MyThread();
			threadList.add(t);
			t.start();
		}
	}
	/**
	 *  添加任务列表。
	 * @param task
	 */
	public void submit(MyTask task){
		mytasklist.add(task);
	}
	private static void delay(){
		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
	private class MyThread extends Thread{
		public void run(){
			delay();//一定的延时。
			while(true){
				//退出条件。再考虑。
				//每个线程。从任务列表(mytasklist)取出第一个任务。不放回。
				//说明线程多。任务少。当前线程没事干。
				MyTask task = null;
				System.out.println(getName()+"线程从任务列表中等待取出一个任务");
				synchronized (mytasklist) {
					if(mytasklist.size()>0)task = mytasklist.remove(0);
					else break;//一定条件下推出。也可以不退出。
				}
				if(task!=null  ){
					System.out.println(getName()+"线程执行任务");
					try {
						task.execute();
					} catch (Exception e) {
						System.out.println("有任务有异常。执行失败。");
					}
					System.out.println(getName()+"线程执行完毕");
				}
			}
			System.out.println("线程"+getName()+"退出");
		}
	}
}

TestMain.java代码如下

public class TestMain {
	public static void main(String[] args) {
		//1 创建线程池。
		MyThreadPools pools  = new MyThreadPools(5);
		//2 创建任务列表。并加入线程池。
		Random r = new Random();
		for(int i=0;i<20;i++){
			MyTask t = new MyTask() {
				@Override
				public void execute() throws Exception{
					 System.out.println("产生随机数:"+r.nextInt());
				}
			};
			pools.submit(t);
		}
	}
}

四 优化

这就完了吗?肯定不会。我们这里的线程池,仅仅实现的是固定数目的线程池,并且线程没事干的时候就退出,这个机制有待商榷。更多的时候,我们需要根据任务的数量来决定线程中最小活跃的线程数量、最大线程数量等。因此还可以做得更加完善。但至少以上已经实现了线程池的根本模型。相信再开发自己的线程池,也就不是难事了。

有一点要说明:JDK提供的线程池,并没有新定义一个类来表示任务。它使用了Runnable接口作为任务。因此,我们的自定义逻辑就写在run方法中。线程池中的线程会调用任务的run方法。

085257_vYcY_3866423.png

 

 

 

转载于:https://my.oschina.net/lightled/blog/1821547

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值