线程池的使用
上一篇看这里:JAVA并发编程-7-并发容器
一、为什么要使用线程池
1、降低资源的消耗。降低线程创建和销毁的资源消耗;
2、提高响应速度:线程的创建时间为T1,执行时间T2,销毁时间T3,免去T1和T3的时间
3、提高线程的可管理性。
二、手动实现一个线程池
如何实现线程呢?有2个关键点
1、线程必须在池子已经创建好了,并且可以保持住,要有容器保存多个线程;
2、线程还要能够接受外部的任务,运行这个任务。容器保持这个来不及运行的任务.
/**
* 类说明:自己线程池的实现
*/
public class MyThreadPool2 {
// 线程池中默认线程的个数为5
private static int WORK_NUM = 5;
// 队列默认任务个数为100
private static int TASK_COUNT = 100;
// 工作线程组
private WorkThread[] workThreads;
// 任务队列,作为一个缓冲
private final BlockingQueue<Runnable> taskQueue;
private final int worker_num;//用户在构造这个池,希望的启动的线程数
// 创建具有默认线程个数的线程池
public MyThreadPool2() {
this(WORK_NUM, TASK_COUNT);
}
// 创建线程池,worker_num为线程池中工/作线程的个数
public MyThreadPool2(int worker_num, int taskCount) {
if (worker_num <= 0) worker_num = WORK_NUM;
if (taskCount <= 0) taskCount = TASK_COUNT;
this.worker_num = worker_num;
taskQueue = new ArrayBlockingQueue<>(taskCount);
workThreads = new WorkThread[worker_num];
for (int i = 0; i < worker_num; i++) {
workThreads[i] = new WorkThread();
workThreads[i].start();
}
}
// 执行任务,其实只是把任务加入任务队列,什么时候执行有线程池管理器决定
public void execute(Runnable task) {
try {
taskQueue.put(task);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 销毁线程池,该方法保证在所有任务都完成的情况下才销毁所有线程,否则等待任务完成才销毁
public void destroy() {
// 工作线程停止工作,且置为null
System.out.println("ready close pool.....");
for (int i = 0; i < worker_num; i++) {
workThreads[i].stopWorker();
workThreads[i] = null;//help gc
}
taskQueue.clear();// 清空任务队列
}
// 覆盖toString方法,返回线程池信息:工作线程个数和已完成任务个数
@Override
public String toString() {
return "WorkThread number:" + worker_num
+ " wait task number:" + taskQueue.size();
}
/**
* 内部类,工作线程
*/
private class WorkThread extends Thread {
@Override
public void run() {
Runnable r = null;
try {
while (!isInterrupted()) {
r = taskQueue.take();
if (r != null) {
System.out.println(getId() + " ready exec :" + r);
r.run();
}
r = null;//help gc;
}
} catch (Exception e) {
// TODO: handle exception
}
}
public void stopWorker() {
interrupt();
}
}
}
上面类中,在构造方法中,定义好线程容器,并且在其中创建指定数量的线程,定义了任务队列来存放任务。
execute方法就是向任务队列中放入需要执行的任务。
定义了WorkThread作为单个工作线程,它不断的从taskQueue任务队列中去取得任务,然后去运行它。
public class TestMyThreadPool {
public static void main(String[] args) throws InterruptedException {
// 创建3个线程的线程池
MyThreadPool2 t = new MyThreadPool2(3,0);
t.execute(new MyTask("testA"));
t.execute(new MyTask("testB"));
t.execute(new MyTask("testC"));
t.execute(new MyTask("testD"));
t.execute(new MyTask("testE"));
System.out.println(t);
Thread.sleep(10000);
t.destroy();// 所有线程都执行完成才destory
System.out.println(t);
}
// 任务类
static class MyTask implements Runnable {
private String name;
private Random r = new Random();
public MyTask(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public void run() {// 执行任务
try {
Thread.sleep(r.nextInt(1000)+2000);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getId()+" sleep InterruptedException:"
+Thread.currentThread().isInterrupted());
}
System.out.println("任务 " + name + " 完成");
}
}
}
我们自己实现的线程池还有很多缺点:
1,线程池在创建时,就创建启动好了线程,如果任务数量小于线程数的时候,会导致线程无意义的等待。我们不能很好的控制线程的数量
2,阻塞队列满了的时候,queue只能被阻塞,线程池应用速度变慢,没有一个有效的机制来处理这种情况
三、JDK中的线程池和工作机制
1、线程池的创建及参数
线程池的创建主要依赖ThreadPoolExecutor类,它是jdk中所有线程池的父类。它的构造参数如下:
- int corePoolSize :线程池中核心线程数,< corePoolSize ,就会创建新线程,= corePoolSize ,这个任务就会保存到BlockingQueue,如果调用prestartAllCoreThreads()方法就会一次性的启动corePoolSize 个数的线程。
- int maximumPoolSize, 允许的最大线程数,BlockingQueue也满了,< maximumPoolSize时候就会再次创建新的线程
- long keepAliveTime, 线程空闲下来后,存活的时间,这个参数只在> corePoolSize才有用
- TimeUnit unit, 存活时间的单位值
- BlockingQueue workQueue, 保存任务的阻塞队列
- ThreadFactory threadFactory, 创建线程的工厂,给新建的线程赋予名字
- RejectedExecutionHandler handler :饱和策略,包括下面几种
AbortPolicy :直接抛出异常,默认;
CallerRunsPolicy:用调用者所在的线程来执行任务
DiscardOldestPolicy:丢弃阻塞队列里最老的任务,队列里最靠前的任务
DiscardPolicy :当前任务直接丢弃
实现自己的饱和策略,实现RejectedExecutionHandler接口即可
2、提交任务的方法
execute(Runnable command) 不需要返回值
Future submit(Callable task) 需要返回值
3、关闭线程池的方法
shutdownNow():设置线程池的状态,还会尝试停止正在运行或者暂停任务的线程
shutdown()设置线程池的状态,只会中断所有没有执行任务的线程
4、工作机制
来看一段重要的源码:
如果小于核心线程数,就直接增加任务。否则就尝试将任务放到等待队列中,如果放失败了,就在小于最大线程数的条件下进行新线程创建,如果仍然没有成功,就拒绝任务。
四、合理配置线程池
线程池的使用也有它的合理性,并不是线程越多就越好,需要根据不同类型的任务来合理的配置线程池。
根据任务的性质来:
计算密集型(CPU),IO密集型,混合型
- 计算密集型:这类的主要任务有 加密,大数分解,正则……., 这类任务是计算量比较大,比较耗费cpu,应该尽量减少cpu在线程之间的轮转,所以线程数适当小一点,最大推荐:机器的Cpu核心数+1,为什么+1,防止页缺失,这是指当某个线程因为调度原因不得不去等待下一个时间片时,+1的线程可以被执行 (机器的Cpu核心=Runtime.getRuntime().availableProcessors()😉
- IO密集型:读取文件,数据库连接,网络通讯, 这类任务的特点是线程可能需要长时间等待。线程数适当大一点,可以充分的利用cpu,配置的线程数量可以参考机器的Cpu核心数*2
- 混合型:尽量拆分,拆分成一个IO密集型和一个计算密集型,但是当IO密集型需要的时间>>计算密集型需要的时间,拆分意义不大。当IO密集型所需时间和计算密集型差不多的时候,是非常有意义的。
队列的选择上,应该使用有界,无界队列可能会导致内存溢出,OOM
五、预定义的线程池
jdk中给我们预定义了一些线程池,来方便特殊情况下的使用。
- FixedThreadPool
创建固定线程数量的,适用于负载较重的服务器,使用了无界队列 - SingleThreadExecutor
创建单个线程,需要顺序保证执行任务,不会有多个线程活动,使用了无界队列 - CachedThreadPool
会根据需要来创建新线程的,执行很多短期异步任务的程序,使用了SynchronousQueue - WorkStealingPool(JDK7以后)
基于ForkJoinPool实现 - ScheduledThreadPoolExecutor
需要定期执行周期任务,Timer不建议使用了。
newSingleThreadScheduledExecutor:只包含一个线程,只需要单个线程执行周期任务,保证顺序的执行各个任务
newScheduledThreadPool 可以包含多个线程的,线程执行周期任务,适度控制后台线程数量的时候
方法说明:
schedule:只执行一次,任务还可以延时执行
scheduleAtFixedRate:提交固定时间间隔的任务
scheduleWithFixedDelay:提交固定延时间隔执行的任务
六、Executor框架
Executor的继承关系大体如下:
Executor框架的基本使用流程:
下一篇:JAVA并发编程-9-并发安全