本咸鱼不会看原码,只能和大家一起看看线程池的大致使用方法,相信有很多和我一样的咸鱼急于上手,不求甚解,看我的blog就对了
一、线程池的作用
最近接到个任务,开发一个小工具,用来获取现网设备的性能,现网3000多台设备,我一开始也没多想,直接一个主线程开跑,等到测试的时候发现有的设备连接超时,有的设备响应超时,从数据库中获取一台设备的ip也不只一个,需要对每个ip进行尝试。这就非常尴尬了,不知道要执行多久。后来SE说给我30个核用多线程跑,我就想到了线程池。
线程的创建和销毁的开销是巨大的,而通过线程池的重用大大减少了这些不必要的开销,当然既然少了这么多消费内存的开销,其线程执行速度也是突飞猛进的提升。
二、线程池
线程池,就是ThreadPoolExecutor,它有几个构造方法(其实都是调用的同一个构造方法),现在我们就看看最常用的构造方法
ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue)
- corePoolSize ,核心线程数;
- maximumPoolSize,最大线程数;
- keepAliveTime,线程执行结束后生存时间(线程可重用,可不用销毁线程,线程池自动分配任务);
- unit,keepAliveTime的单位,有7种取值:
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小时
TimeUnit.MINUTES; //分钟
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //纳秒
- workQueue,工作队列,有以下几种类型:
ArrayBlockingQueue;
LinkedBlockingQueue;
SynchronousQueue;
线程池初始化后调用ThreadPoolExecutor.execute(Runable)即可(Runable为实现了Runable接口的实例)
三、快速入手
Task.java:
package com.zr;
public class Task implements Runnable{
int workNum;
public Task(int workNum) { //传入要处理的数据
// TODO Auto-generated constructor stub
this.workNum=workNum;
}
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("正在执行任务"+workNum);
}
}
ThreadPoolTest.java:
package com.zr;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolTest {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 5, 2, TimeUnit.MINUTES, new LinkedBlockingQueue<>());
for (int i = 0; i < 10; i++) {
executor.execute(new Task(i));
}
}
}
执行结果:
可以看到所有任务都执行完了,但是程序并没有结束,这时我们需要调用ThreadPoolExecutor.shutdown()方法来结束线程池即在for循环后加上executor.shutdown(),实际开发的时候我们肯能需要让主线程挂起,当线程池任务执行完后再接着执行后面的代码,这时就需要在主进程中添加判断当ThreadPoolExecutor.isTerminated()为true时再执行后面的代码。修改过的代码如下:
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 5, 2, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
for (int i = 0; i < 10; i++) {
executor.execute(new Task(i));
}
executor.shutdown();
while(true) {
if(executor.isTerminated()) {
System.out.println("main执行完成");
break;
}
}
System.out.println("main真的执行完成");
}
执行结果:
也可以将判断逻辑改成这样:
while(!executor.isTerminated()) {
}
System.out.println("main执行完成");
System.out.println("main真的执行完成");
什么是workQueue
按字面意思就是工作队列,当我们调用ThreadPoolExecutor.execute(Runable)时就是向线程池添加了一个Runable任务,LinkedBlockingQueue中可以设置workQueue的容量,默认为Integer.MAX_VALUE,在上面的例子中for循环一次添加了10个任务,我们看看给LinkedBlockingQueue设置容量执行会出现什么结果
当workQueue容量设置为10时:
可以发现并没有什么变化
但我们把workQueue容量设置为4时就会出现异常:
可以发现一共执行了9个任务,最后一个并没有执行。
原理: 在workQueue没有满时,线程池只会创建corePoolSize个线程来跑任务,当workQueue满了,每加一个任务就会新创建一个线程来跑任务,但是不会超过maximumPoolSize,当线程数达到maximumPoolSize,还在往workQueue中添加任务时就会添加失败,抛出java.util.concurrent.RejectedExecutionException异常。上面的例子中我在Task执行的时候添加了Thread.sleep(500),可以保证在添加最后一个任务时workQueue已满(因为主线程for循环添加10个任务就是一瞬间的事,而不加sleep线程执行也是一瞬间的事,这样可能不会出现workQueue满了还在添加任务的情况)。workQueue的大小为4,核心线程数(corePoolSize)为3,前三个任务一进workQueue就被拿出来执行了,所以当添加第三个任务时workQueue还是空的,然后继续添加四个任务将workQueue填满,当添加第八个和第九个任务时创建额外的线程执行,这时线程数量达到最大值(maximumPoolSize),workQueue也满了,还有最后一个任务就会添加失败。
现在我们在添加最后一个任务之前让队列或线程空出来,这样就不会出现异常了
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 5, 2, TimeUnit.SECONDS, new LinkedBlockingQueue<>(4));
for (int i = 0; i < 10; i++) {
if (i == 9) {
try {
Thread.sleep(600); //Task里sleep(500),600ms后肯定有执行完的任务
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
executor.execute(new Task(i));
}
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("main执行完成");
System.out.println("main真的执行完成");
}
接下来同学们自己执行下就行了,结果就是全部执行成功。在我们工作时可以按照实际设备性能分配workQueue和线程数