并发编程之J.U.C - 线程池
1 java中的线程池
线程池是为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,通过对线程的复用减少了创建和销毁线程所需的时间,从而提高效率。
合理地使用线程池能够带来3个好处:
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
2 线程池体系结构
线程池的体系结构:
java.util.concurrent.Executor 负责线程的使用和调度的根接口
|--ExecutorService 子接口: 线程池的主要接口
|--ThreadPoolExecutor 线程池的实现类
|--ScheduledExceutorService 子接口: 负责线程的调度
|--ScheduledThreadPoolExecutor : 继承ThreadPoolExecutor,实现了ScheduledExecutorService
工具类 : Executors 提供快捷的创建线程池的方法
关键类或接口 | 含义 |
---|---|
Executor | 是一个接口,它是Executor框架的基础, 它将任务的提交与任务的执行分离开来 |
ExecutorService | 线程池的主要接口,是Executor的子接口 |
ThreadPoolExecutor | 是线程池的核心实现类,用来执行被提交的任务 |
ScheduledThreadPoolExecutor | 另一个关键实现类,可以进行延迟或者定期执行任务。ScheduledThreadPoolExecutor比Timer定时器更灵活, 功能更强大 |
Future接口与FutureTask实现类 | 代表异步计算的结果 |
Runnable接口和Callable接口的实现类 | 都可以被ThreadPoolExecutor或 ScheduledThreadPoolExecutor执行的任务 |
Executors | 线程池的工具类,可以快捷的创建线程池 |
Executor
线程池就是线程的集合,线程池集中管理线程,以实现线程的重用,降低资源消耗,提高响应速度等。线程用于执行异步任务,单个的线程既是工作单元也是执行机制,从JDK1.5开始,为了把工作单元与执行机制分离开,Executor框架诞生了,它是一个用于统一创建任务与运行任务的接口。框架就是异步执行任务的线程池框架。
ThreadPoolExecutor
Executor框架的最核心实现是ThreadPoolExecutor类,通过传入不同的参数,就可以构造出适用于不同应用场景下的线程池,那么它的底层原理是怎样实现的呢,下面就来介绍下ThreadPoolExecutor线程池的运行过程。
核心构造器参数
组件 | 含义 |
---|---|
int corePoolSize | 核心线程池的大小 |
int maximumPoolSize | 最大线程池的大小 |
BlockingQueue workQueue | 用来暂时保存任务的工作队列 |
RejectedExecutionHandler | 当ThreadPoolExecutor已经关闭或ThreadPoolExecutor已经饱和时(达到了最大线程池的大小且工作队列已满),execute()方法将要调用的Handler |
long keepAliveTime, | 表示空闲线程的存活时间。 |
TimeUnit | 表示keepAliveTime的单位。 |
ThreadFactory threadFactory | 指定创建线程的线程工厂 |
线程池的三种队列
1.SynchronousQueue
SynchronousQueue没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素。
使用SynchronousQueue阻塞队列一般要求maximumPoolSizes为无界,避免线程拒绝执行操作。
2.LinkedBlockingQueue
LinkedBlockingQueue是一个无界缓存等待队列。当前执行的线程数量达到corePoolSize的数量时,剩余的元素会在阻塞队列里等待。(所以在使用此阻塞队列时maximumPoolSizes就相当于无效了),每个线程完全独立于其他线程。生产者和消费者使用独立的锁来控制数据的同步,即在高并发的情况下可以并行操作队列中的数据。
3.ArrayBlockingQueue
ArrayBlockingQueue是一个有界缓存等待队列,可以指定缓存队列的大小,当正在执行的线程数等于corePoolSize时,多余的元素缓存在ArrayBlockingQueue队列中等待有空闲的线程时继续执行,当ArrayBlockingQueue已满时,加入ArrayBlockingQueue失败,会开启新的线程去执行,当线程数已经达到最大的maximumPoolSizes时,再有新的元素尝试加入ArrayBlockingQueue时会报错。
线程池的四种拒绝策略
1.DiscardPolicy
这种策略是当任务提交时直接将刚提交的任务丢弃,而且不会给与任何提示通知,所以这种策略使用要慎重,因为有一定的风险,对我们来说根本不知道提交的任务有没有被丢弃
2.DiscardOldestPolicy
这种策略和上面相似。不过它丢弃的是队列中的头节点,也就是存活时间最久的
3.AbortPolicy
这种策略在拒绝任务时,会直接抛出一个类型为RejectedExecutionException 的 RuntimeException,让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略
4.CallerRunsPolicy
这种策略算是最完善的相对于其他三个,当线程池无能力处理当前任务时,会将这个任务的执行权交予提交任务的线程来执行,也就是谁提交谁负责,这样的话提交的任务就不会被丢弃而造成业务损失,同时这种谁提交谁负责的策略必须让提交线程来负责执行,如果任务比较耗时,那么这段时间内提交任务的线程也会处于忙碌状态而无法继续提交任务,这样也就减缓了任务的提交速度,这相当于一个负反馈。也有利于线程池中的线程来消化任务
public static void main(String[] args) {
BlockingQueue<Integer> queue = new SynchronousQueue<>();
//生产者线程
Thread t1 = new Thread(()->{
for (int i=0;i<20;i++){
try {
System.out.println("装入数据:" + i);
queue.put(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//消费者线程
Thread t2 = new Thread(()->{
while (true){
try {
System.out.println("2秒后取数据");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
System.out.println(queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
3 线程池原理剖析
案例代码
/**
* 创建一个线程
* 通过对任务的执行
* 演示线程池的运行原理
* @作者 itcast
* @创建日期 2020/3/17 9:27
**/
public class ThreadPoolDemo05{
public static volatile boolean flag = true;
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2,
5,
10,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(10),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy());
for (int i = 0; i < 20; i++) {
try {
// 循环执行 20个任务
executor.execute(new MyRunnable("第"+(i+1)+"号线程"));
} catch (Throwable e) {
System.out.println("丢弃任务: " + (i+1) );
}
}
// 用于演示效果
new Thread(()->{
// 接收控制台参数
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String s = scanner.nextLine();
// 如果控制输入stop 将flag设置false, 所有任务都会执行完毕
if ("stop".equals(s)){
flag = false;
}
if("show".equals(s)){
System.out.println("活跃线程数量==>"+executor.getActiveCount());
}
}
}).start();
}
static class MyRunnable implements Runnable{
private String name;
public MyRunnable(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(name);
while (flag){
//flag是一个开关,为true时线程任务会一直执行让线程一直执行
}
}
}
}
提交一个任务到线程池中,线程池的处理流程如下:
流程1 判断核心线程数
判断正在运行的工作线程是否小于 设置的核心线程数,小于尝试创建一个
新的工作线程,如果不小于进入下一流程
流程2 判断任务队列
判断当前线程池的任务队列是否已满,未满的话将任务加入任务队列,如果满了,进入下一个流程
流程3 判断最大线程数
判断当前线程池的工作线程是否小于 设置的最大线程数,小于尝试创建一个新的临时工作线程,如果不小于进入下一流程
流程4 判断 饱和/拒绝 策略
到此流程,说明当前线程池已经饱和,需要进行拒绝策略,根据设置的拒绝策略进行处理
4 线程池源码解析
ThreadPoolExecutor源码分析
在线程池的实现中,Worker这个类是线程池的内部类,Worker对象是线程池实现的核心。在ThreadPoolExecutor中存放了一个
// 工作线程的集合
HashSet<Worker> workers = new HashSet<Worker>();
点进Worker类的源码中 发现Worker实现了Runnable接口,并且有两个属性一个线程对象,还有一个第一次要执行的任务
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
// 执行工作的线程对象
final Thread thread;
// Worker要执行的第一个任务
Runnable firstTask;
}
查看Worker的构造方法
Worker(Runnable firstTask) {
setState(-1);
// 传入worker第一次要执行的任务
this.firstTask = firstTask;
// 使用工厂对象创建线程, 并把worker本身传入
this.thread = getThreadFactory().newThread(this);
}
查看线程工厂对象的newThread类方法
public Thread newThread(Runnable r) {
// new 了一个新的线程对象 并且把worker对象作为线程任务传入
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
return t;
}
查看Worker类当中的run方法:
// 线程任务 run方法
public void run() {
runWorker(this);
}
// Worker的核心工作方法
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
// 第一次要执行的任务 赋值给task
Runnable task = w.firstTask;
...
try {
while (task != null || (task = getTask()) != null) {
w.lock();
...
// 执行任务的run方法
task.run();
...
} finally {
// 任务执行完毕后,清空任务判断是否包含下一个任务
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
查看getTask方法如何获取任务:
private Runnable getTask() {
boolean timedOut = false;
for (;;) {
...
try {
Runnable r = timed ?
// 下面两种方法都是在从 workQueue队列中获取任务
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
// 将取到的任务返回
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
小结:
通过上面的源码分析得出,Worker对象是线程池工作的核心,一个Worker对象代表一个工作线程, 只要Worker内的线程的start方法被调用后,我们的worker对象内的run方法被线程执行,而run方法中则不断的从任务队列中获取 任务,并调用任务的run方法来执行,这样就达到了线程复用的目的。
那么Worker什么时候会被创建呢?
接着分析线程池的execute执行任务的方法
execute的执行源码
// execute执行方法源码分析
public void execute(Runnable command) {
// 任务为空抛异常
if (command == null)
throw new NullPointerException();
// ctl 是 integer原子类 主要通过它记录两类信息,
// ctl作用: 1.记录线程池状态 2.记录线程池工作线程数量
// workerCountOf(c):判断worker数量
// isRunning(c): 判断线程池状态
// 1.如果当前worker数量小于corePoolSize 创建一个新的worker
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
// 创建worker 参数1:任务 参数2:添加核心还是临时线程
if (addWorker(command, true))
return;
c = ctl.get();
}
// 2.尝试向任务队列中添加任务,如果添加失败进入下移流程
if (isRunning(c) && workQueue.offer(command)) {
// 添加成功 复查线程池状态
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 3.参数2:true => 添加核心工作线程 false => 添加临时工作线程
else if (!addWorker(command, false))
// 4. 如果添加失败,执行拒绝策略
reject(command);
}
总结:通过execute方法的源码,我们就已经看到了执行流程
在查看addWorker的方法,看看是如何添加一个worker的
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
...
// wc是工作线程的数量
// core为true 判断是否大于核心线程数量
// core为false 判断是否大于最大线程数量
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
...
}
}
// 准备创建worker
Worker w = null;
try {
// 创建worker对象,构造器内会通过线程工厂创建一个线程 并且把worker对象作为任务传入
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
...
// 将worker对象 存入到workers集合
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
// 启动worker内的线程
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
小结:
可以看到,在execute方法中完全和我们前面将的流程一样,在addWorker方法中通过构造器创建了worker对象 并把它存入到了workers集合中,然后启动worker内线程的start方法,这样这个worker就会不断工作,不断的执行任务队列里面的任务
5 Executors线程池工具类
Executors是线程池的工具类,提供了四种快捷创建线程池的方法:
newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newCachedThreadPool
/**
* 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
*/
public void fun1(){
// 创建线程池
ExecutorService es = Executors.newCachedThreadPool();
// 会创建出10个线程 分别执行任务
for (int i = 0; i < 10; i++) {
es.execute(()->{
for (int j = 0; j < 10; j++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + j);
}
});
}
es.shutdown();
}
newFixedThreadPool
/**
* 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
*/
public void fun2(){
// 创建线程池
ExecutorService es = Executors.newFixedThreadPool(2);
// 会创建出10个线程 分别执行任务
for (int i = 0; i < 10; i++) {
es.execute(()->{
try {
Thread.sleep(400);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int j = 0; j < 10; j++) {
System.out.println(Thread.currentThread().getName() + ":" + j);
}
});
}
es.shutdown();
}
newSingleThreadExecutor
/**
* 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
*/
public void fun3(){
// 创建线程池
ExecutorService es = Executors.newSingleThreadExecutor();
// 会创建出10个线程 分别执行任务
for (int i = 0; i < 10; i++) {
es.execute(()->{
try {
Thread.sleep(400);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int j = 0; j < 10; j++) {
System.out.println(Thread.currentThread().getName() + ":" + j);
}
});
}
es.shutdown();
}
newScheduledThreadPool
/**
* 创建一个定长线程池,支持延迟及周期性任务执行。延迟执行示例代码如下
*/
public void fun4(){
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(5);
// 周期性执行任务(任务会执行多次)
// 参数1:任务 参数2:延迟时间 参数3:每隔长时间 参数4:时间单位
newScheduledThreadPool.scheduleAtFixedRate(()-> System.out.println("要执行的任务"), 3, 2, TimeUnit.SECONDS);
// 延迟执行任务(任务执行一次)
// 参数1:任务 参数2:延迟时间 参数3:时间单位
newScheduledThreadPool.schedule(()->System.out.println("要执行的任务"), 3,TimeUnit.SECONDS);
}