目录
二、Java中的ThreadPoolExecutor类的构造方法
6.1 为什么线程池的工作队列要是阻塞队列 BlockingQueue?
一、线程池的好处
(1)、降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
(2)、提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
(3)方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
ThreadPoolExecutor 位置在 java.util.concurrent.ThreadPoolExecutor 中,属于JUC。
这个类是Java线程池最核心的一个类,因此要透彻了解线程池,必先了解这个类。
二、Java中的ThreadPoolExecutor类的构造方法
在ThreadPoolExecutor中提供了四个构造方法:
public class ThreadPoolExecutor extends AbstractExecutorService {
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
}
可以看到,虽说有两个构造函数,其实实质上都是 对 ThreadPoolExecutor()方法的重载。(重载:同一个函数名,但是参数不同的方法)
接下来仔细介绍一下线程池构造方法的参数:
首先是4个构造方法共有的参数,有五个:corePoolSize、maximunPoolSize、keepAliveTime、unit、workQueue。这肯定是最重要的5个了
- corePoolSize(核心池大小):线程池的基本大小,即在没有任务需要执行的时候线程池的大小(并非线程池最大的线程数)。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
- maximunPoolSize(线程池最大容量):线程池中允许的最大线程数,线程池中的当前线程数目不会超过该值,它表示在线程池中最多能创建多少个线程。
- keepAliveTime(最长保持时间):表示线程没有任务执行时,最多保持多长时间就会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
- unit(keepAliveTime的时间单位):有七个取值,分别是:
TimeUnit.DAYS; //天 TimeUnit.HOURS; //小时 TimeUnit.MINUTES; //分钟 TimeUnit.SECONDS; //秒 TimeUnit.MILLISECONDS; //毫秒 TimeUnit.MICROSECONDS; //微妙 TimeUnit.NANOSECONDS; //纳秒
- workQueue(工作队列):一个阻塞队列,用来存储等待执行的任务。
非共有的参数:
- threadFactory(线程工厂):用于创建新线程。
- handler(线程饱和策略):当线程池和队列都满了,再加入线程会执行此策略。handler 有以下四种取值:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程) ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
三、线程池执行流程
1. 判断核心线程池是否已满(线程数是否超过corePoolSize),没满则创建一个工作线程执行任务,已满则执行2
2. 判断工作队列是否已满,没满则将提交的任务添加到工作队列,已满则执行3
3. 判断整个线程池是否已满(线程数是否超过maximunPoolSize),没满则创建一个新的线程执行任务,已满则执行饱和策略。
四、Java线程池的类型
JUC里的Executors类提供了4种不同的线程池:newCachedThreadPool、newFixedThreadPool、newScheduledThreadPool、newSingleThreadExecutor
1. newCachedThreadPool :用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。
corePoolSize为0,表明这个线程池是直接把提交的任务放到workQueue中的,且其keepAliveTime为60s,即线程执行完任务后,还要过60s看看有没有其他任务执行,如果没,才会终止。也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。
2. newFixedThreadPool:创建一个线程数目大小固定的线程池。保证线程数可控,不会造成线程过多,导致系统负载更为严重
3. newSingleThreadExecutor:创建一个单线程的线程池,适用于需要保证顺序执行各个任务。
4. newScheduledThreadPool:适用于执行延时或者周期性任务。
五、使用线程池代码例子
手动创建一个corePoolSize=5,maximumPoolSize=10,keepAliveTime=200ms的线程池,并用这个线程池执行15个任务,每个任务做的事情就是sleep 4秒。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<Runnable>(5));
for(int i=0;i<15;i++){
MyTask myTask = new MyTask(i);
executor.execute(myTask);
System.out.println("线程池中线程数目:"+executor.getPoolSize()+",队列中等待执行的任务数目:"+
executor.getQueue().size()+",已执行玩别的任务数目:"+executor.getCompletedTaskCount());
}
executor.shutdown();
}
}
class MyTask implements Runnable {
private int taskNum;
public MyTask(int num) {
this.taskNum = num;
}
@Override
public void run() {
System.out.println("正在执行task "+taskNum);
try {
Thread.currentThread().sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("task "+taskNum+"执行完毕");
}
}
但是一般不推荐手动设置线程池参数,因为JUC内已经配置好 newCachedThreadPool、newFixedThreadPool、newScheduledThreadPool、newSingleThreadExecutor 这四个线程池的参数,因此最好直接使用这四种线程池:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test {
public static void main(String[] args) {
/**
* 单线程化的线程池
*/
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
singleThreadExecutor.execute(new Runnable() {
@Override
public void run() {
Thread.currentThread().setName("Thread i = " + index);
System.out.println(Thread.currentThread().getName() + " index = " + index);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println("ssss");
}
}
});
}
singleThreadExecutor.shutdown();
System.out.println("on the main thread...");
}
}
总结一句:
newCachedThreadPool、newFixedThreadPool、newScheduledThreadPool、newSingleThreadExecutor 这四个线程池其实都是用ThreadPoolExecutor类创建的,只是他们4个传给ThreadPoolExecutor的参数不同而已:
下面的代码出自:java.util.concurrent.Executors;
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}
六、关于线程池相关疑问
6.1 为什么线程池的工作队列要是阻塞队列 BlockingQueue?
阻塞队列,顾名思义,首先它是一个队列,而一个队列在数据结构中所起的作用大致如下图所示:
多线程环境中,通过队列可以很容易实现数据共享,比如经典的“生产者”和“消费者”模型中,通过队列可以很便利地实现两者之间的数据共享。假设我们有若干生产者线程,另外又有若干个消费者线程。如果生产者线程需要把准备好的数据共享给消费者线程,利用队列的方式来传递数据,就可以很方便地解决他们之间的数据共享问题。但如果生产者和消费者在某个时间段内,万一发生数据处理速度不匹配的情况呢?理想情况下,如果生产者产出数据的速度大于消费者消费的速度,并且当生产出来的数据累积到一定程度的时候,那么生产者必须暂停等待一下(阻塞生产者线程),以便等待消费者线程把累积的数据处理完毕,反之亦然。然而,在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。好在此时,强大的concurrent包横空出世了,而他也给我们带来了强大的BlockingQueue。(在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒),下面两幅图演示了BlockingQueue的两个常见阻塞场景:
如上图所示:当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列。
如上图所示:当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。
这也是我们在多线程环境下,为什么需要BlockingQueue的原因。作为BlockingQueue的使用者,我们再也不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了。