一 简介
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方法。