BlockingQueue接口
BlocKingQueue是并发容器的一种,在J.U.C包路径下
BlockingQueue是线程安全的版本实现,是一个阻塞队列实现,该接口提供两个新的操作put/take
接口特点:
• BlockingQueue不接受null值,进行add/put/offer如果是一个null值,则会抛出NullPointerException
• BlockingQueue可以是指定容量的,如果超过了给定的容量就会阻塞
• BlockingQueue实现的是线程安全的,视线里内容可以使用内部锁或者是其他形式的并发控制来自动达到线程安全的目的
三种实现类
ArrayBlockingQueue:基于数组实现的有界阻塞队列 LinkedBlockingQueue:基于链表实现的无界阻塞队列
SynchronousQueue:同步阻塞队列
ArrayBlockingQueue
ArrayBlockingQueue底层实现是数组,数组的大小是固定的,所以ArrayBlockingQueue需要制定大小的
属性和默认值:
//items属性是用来存储数据的,ArrayBlockingQueue是底层数据结构数组(环形数组)
final Object[] items;
//读取操作的位置
int takeIndex;
//写操作的位置
int putIndex;
//队列中元素的个数
int count;
//锁相关操作
final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;
构造函数:
//指定容量的阻塞队列
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
//指定阻塞队列的初始容量大小,fair设置锁是公平性锁和非公平性锁
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
//指定容量大小,公平锁或非公平锁 ,给定的集合实例来创建队列
public ArrayBlockingQueue(int capacity, boolean fair,Collection<? extends E> c) {
this(capacity, fair);
final ReentrantLock lock = this.lock;
//加锁
lock.lock(); // Lock only for visibility, not mutual exclusion
try {
int i = 0;
try {
for (E e : c) {
//将原集合中的每一个元素都安全的存放到阻塞队列中
checkNotNull(e);
items[i++] = e;
}
} catch (ArrayIndexOutOfBoundsException ex) {
throw new IllegalArgumentException();
}
count = i;
putIndex = (i == capacity) ? 0 : i;
} finally {
//释放锁
lock.unlock();
}
}
put()操作
public void put(E e) throws InterruptedException {
//检测存储的元素不能为null
checkNotNull(e);
final ReentrantLock lock = this.lock;
//加可中断锁
lock.lockInterruptibly();
try {
while (count == items.length)
//若队列满则会阻塞
notFull.await();
insert(e);
} finally {
//释放锁
lock.unlock();
}
}
private void insert(E x) {
items[putIndex] = x;
putIndex = inc(putIndex);
++count;
notEmpty.signal();
}
//通过索引位置的处理(takeIndex和putIndex)来实现环形数组
final int inc(int i) {
return (++i == items.length) ? 0 : i;
}
take()操作
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
//当队列为空时,会产生阻塞
notEmpty.await();
return extract();
} finally {
lock.unlock();
}
}
private E extract() {
final Object[] items = this.items;
E x = this.<E>cast(items[takeIndex]);
items[takeIndex] = null;
takeIndex = inc(takeIndex);
--count;
notFull.signal();
return x;
}
ArrayBlockingQueue的阻塞队列实现是基于数组实现,内部使用个一个ReentrantkLock锁和两个Condition实例,通过ReentrantkLock实现线程安全的操作,两个Condition用来进行添加/删除的则塞和唤醒的通知
LinkedBlockingQueue
LinkedBlockingQueue是基于链表实现的无界阻塞队列
private final int capacity;
//用来统计队列中元素的个数
private final AtomicInteger count = new AtomicInteger(0);
private transient Node<E> head;//头结点
private transient Node<E> last;//尾节点
private final ReentrantLock takeLock = new ReentrantLock();
private final Condition notEmpty = takeLock.newCondition();
private final ReentrantLock putLock = new ReentrantLock();
private final Condition notFull = putLock.newCondition();
LinkedBlockQueue的实现有两个ReentrantLock和两个Condition
LinkedBlockQueue底层结构为链表,采用的头尾节点,每一个节点执行下一个节点,数据都存储在node中.引入了两个锁,一个是入队列锁,一个是出队列锁.
为什么需要两把锁?一把锁行不行?
一把锁是完全可以实现,但是一把锁意味着入队列和出队列同时只能有一个在执行,另一个必须等待释放锁才能进行操作。LinkedBlockingQueue实现上有head和last节点是分离的,互相独立的,入队列不会修改出队列的值,同样出队列不会修改入队列的值,出队列和入队列是互不干扰的,通过两把锁,可以实现出队列和出队列的并发处理
SynchronousQueue
SynchronousQueue同步队列:每个插入数据操作必须等待另一个移除操作,队列中最多只能有一个元素
快速的传递元素的一种实现方式
队列对比
如果不需要阻塞队列,优先选择ConcurrentLinkedQueue;
如果需要阻塞队列,队列大小固定优先选择ArrayBlockingQueue,
队列大小不固定优先选择LinkedBlockingQueue;
如果需要对队列进行排序,选择PriorityBlockingQueue;
如果需要一个快速交换的队列,选择SynchronousQueue;
如果需要对队列中的元素进行延时操作,则选择DelayQueue。
死锁
死锁问题
死锁:是指两个或者两个以上的进程(线程)在执行的过程中,由于竞争资源而造成的阻塞问题,若无外力的作用下会无法继续推进,此时系统称之为死锁状态
死锁的形成:
如图所示:将设存在两个线程SetThread和GetThread,现在SetThread持有了ObjectA资源请求资源ObjectB。
GetThread持有了ObjectB资源请求资源ObjectA,ObjectA和ObjectB资源的获取是互斥的,两个线程等待另一个资源而不释放持有的资源,就会无限等待下去,这就是死锁。
死锁原因:
1、因竞争资源产生死锁
2、进程推进顺序不当会发生死锁
死锁代码示例
两个线程来竞争两个资源
public class DeadLockDemo {
private static Object obj1 = new Object();
private static Object obj2 = new Object();
public static void main(String[] args) {
//线程1先获取obj1在获取obj2
//线程2先获取obj2在获取obj1
Runnable t1 = new Runnable() {
@Override
public void run() {
synchronized (obj1) {
System.out.println("线程1获取到资源1");
synchronized (obj2) {
System.out.println("线程1获取资源2");
}
}
}
};
Runnable t2 = new Runnable() {
@Override
public void run() {
synchronized (obj2) {
System.out.println("线程2获取到资源2");
synchronized (obj1) {
System.out.println("线程2获取资源1");
}
}
}
};
//启动线程
new Thread(t1).start();
new Thread(t2).start();
}
}
死锁产生的四个必要条件
1、互斥条件:一个资源每次只能被一个线程使用
2、请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
3、不剥夺条件:进程已获得的资源,在未使用之前,不能强行剥夺
4、循环等待条件:若干进程之间形成一种头尾相连接的循环等待资源关系
死锁的解除与预防
解决死锁的三种途径:预防、避免、检测与恢复
死锁预防
预防死锁只需要破坏4个必要条件就可以
资源一次性分配:(破坏请求与保持的)
可剥夺资源:当线程来获取新资源为满足时,需要将已占有资源释放掉(破坏不可剥夺条件)
资源的有序分配法:系统给每一个资源进行编号,每一个线程按照编号递增顺序请求资源,释放资源正好相反(破坏请求与保持)
避免死锁(银行家算法)
避免死锁的策略中,允许进程进行资源的动态的申请,系统在资源分配先进行预分配,先计算资源分配的安全性,不会导致系统进不到不安全的状态,系统会将资源真正的分配给进行,经典的避免死锁的方法就是银行家算法
检测和解除死锁
当发现进程死锁后,立即从死锁状态解救处理啊,采用方式:
**剥夺资源:**从其他进程中剥夺足够多的资源给死锁进程,以避免死锁状态(MYSQL中死锁解决方案就是这个思路)
**撤销进程:**直接撤销死锁进程或者是撤销代价最小的进程,直至有足够资源可用时时才执行撤销线程
银行家算法(扩展)
当一个进程申请使用资源的时候,银行家算法通过先 试探 分配给该进程资源,
然后通过安全性算法判断分配后的系统是否处于安全状态,
若不安全则试探分配作废,让该进程继续等待。
首先银行家算法的进程:
包含进程pi的需求资源的数量M(每个进程最大的需求你资源数量,MAX)
已分配的进程的资源A(Allocation)
还需要的资源数量N(Need=M-A)
系统资源池中空闲的资源数量:Avaliable(Available+已分配的资源数量A=系统中资源的总量)
通过安全性分析:找到了安全性序列:P0\p3\p4\p1\p2,当前的资源分配时安全的,即可以给线程进程资源分析,真正开始资源分配
思考:
如进程P2提出请求Ruquest(1,2,2,2)
当前请求是无法找到安全性序列的,系统不会对当前的操作分配资源
代码实现:
/**
* 银行家算法实现
*/
public class Banker {
int available[] = new int[]{3, 3, 2};//可得到的资源
int max[][] = new int[][]{{7, 5, 3}, {3, 2, 2}, {9, 0, 2}, {2, 2, 2}, {4, 3, 3}};//每个进程最大资源数
int allocation[][] = new int[][]{{0, 1, 0}, {2, 0, 0}, {3, 0, 2}, {2, 1, 1}, {0, 0, 2}};//每个进程目前拥有的资源数
int need[][] = new int[][]{{7, 4, 3}, {1, 2, 2}, {6, 0, 0}, {0, 1, 1}, {4, 3, 1}};//每个进程需要的资源数
void showData() {
//展示数据输出每个进程的相关数
System.out.println("进程号 Max All Need ");
System.out.println(" A B C A B C A B C");
for (int i = 0; i < 5; i++) {
System.out.print(i + " ");
for (int m = 0; m < 3; m++) System.out.print(max[i][m] + " ");
for (int m = 0; m < 3; m++) System.out.print(allocation[i][m] + " ");
for (int m = 0; m < 3; m++) System.out.print(need[i][m] + " ");
System.out.println();
}
}
boolean change(int inRequestNum, int inRequest[]) {
//分配数据
int requestNum = inRequestNum;
int request[] = inRequest;
if (!(request[0] <= need[requestNum][0] && request[1] <= need[requestNum][1] && request[2] <= need[requestNum][2])) {
//每一类请求资源小于当前线程need的资源数
System.out.println("请求的资源数超过了所需要的最大值,分配错误");
return false;
}
if ((request[0] <= available[0] && request[1] <= available[1] && request[2] <= available[2]) == false) {
//当前线程的每一类请求资源小于等于资源池对应资源的数量
System.out.println("尚无足够资源分配,必须等待");
return false;
}
for (int i = 0; i < 3; i++) {
//试分配数据给请求的线程
available[i] = available[i] - request[i];
//资源池的每类资源减去每类请求资源数量
allocation[requestNum][i] = allocation[requestNum][i] + request[i];
//当前线程allocation中每类资源加上每类资源请求数量
need[requestNum][i] = need[requestNum][i] - request[i];
//当前线程need中每类资源数量减去每类资源的请求数量
}
boolean flag = checkSafe(available[0], available[1], available[2]);//进行安全性检查并返回是否安全
if (flag == true) {
System.out.println("能够安全分配");
return true;
} else {
//不能通过安全性检查 恢复到未分配前的数据
System.out.println("不能够安全分配");
for (int i = 0; i < 3; i++) {
available[i] = available[i] + request[i];
allocation[requestNum][i] = allocation[requestNum][i] - request[i];
need[requestNum][i] = need[requestNum][i] + request[i];
}
return false;
}
}
boolean checkSafe(int a, int b, int c)//安全性检查
{
int work[] = new int[3];
work[0] = a;
work[1] = b;
work[2] = c;
int i = 0;
boolean finish[] = new boolean[5];
//寻找一个能够满足的认为完成后才去执行下一进程
while (i < 5) {
if (finish[i] == false && need[i][0] <= work[0] && need[i][1] <= work[1] && need[i][2] <= work[2]) {//找到满足的修改work值,然后i=0,重新从开始的为分配的中寻找
System.out.println("分配成功的是" + i);
for (int m = 0; m < 3; m++)
work[m] = work[m] + allocation[i][m];
finish[i] = true;
i = 0;
} else//如果没有找到直接i++
i++;
}
for (i = 0; i < 5; i++) {
//通过finish数组判断是否都可以分配
if (finish[i] == false)
return false;
}
return true;
}
public static void main(String[] args) {
Banker bank = new Banker();
bank.showData();
//请求线程资源存放的数组
int request[] = new int[3];
int requestNum;
String source[] = new String[]{"A", "B", "C"};
Scanner s = new Scanner(System.in);
String choice = new String();
while (true) {
//循环进行分配
System.out.println("请输入要请求的进程号(0--4):");
requestNum = s.nextInt();
System.out.print("请输入请求的资源数目");
for (int i = 0; i < 3; i++) {
System.out.println(source[i] + "资源的数目:");
request[i] = s.nextInt();
}
bank.change(requestNum, request);
System.out.println("是否再请求分配(y/n)");
choice = s.next();
if (choice.equals("n"))
break;
}
}
}
死锁案例
• 生产者、消费者使用不当会产生死锁
一个生产者、一个消费者使用notify
一个生产者,多个消费者使用notify(!!!)
• 多线程操作多把锁
多线程操作多把锁的时候,加锁解锁顺序必须保持一致,否则可能导致死锁
• 哲学家就餐问题
哲学家就餐问题(扩展)
哲学家就餐问题是线程并发的经典问题
1、圆桌上有5位哲学家,每两位中间有一只筷子
2、每个哲学家有两件事:思考、吃饭(必须拿到两只筷子才能吃饭)
3、哲学家之间并不知道对方何时吃饭,何时思考
4、指定一个拿筷子的策略,使哲学家就餐不会因为拿筷子而出现死锁
资源分级解决
为资源(筷子)分配一个偏序结构,资源按照顺序访问,拿筷子必须按照顺序来拿
如:先拿编号较小的筷子,再拿编号较大的筷子,放下筷子的顺序则无所谓,这样按照筷子编号较小的优先拿则4号(编号最大)筷子没人拿,则其中一位哲学家就可以获取到2个筷子进行就餐,避免了死锁
仲裁者解决方案
引入一个仲裁者,哲学家为了拿筷子必须向仲裁者发起请求,仲裁者每次只服务一个哲学家,知道它拿到两个筷子位置,仲裁者必须使用锁实现
chandy/misra解法
允许任意的用户(编号P1,…,Pn)争用任意数量的资源。与资源分级解法不同,这里编号随意
1、对每一个哲学家编号
2、对每一个竞争一个筷子资源的哲学家,新拿一个筷子,给编号较低的哲学家
3、每只筷子有两种状态:“干净的”或者“”脏的,初始所有筷子都是脏的
4、当一个哲学家要吃饭他必须拥有两个筷子,当他缺乏某只筷子的时候,发起请求
5、当拥有筷子的哲学家收到请求时,如果筷子是干净的,那么他继续留着使用,否则就擦干净并交出筷子
6、哲学家吃完的时候,筷子变脏,当有邻座请求筷子时,擦干净筷子,然后给邻座
死锁问题的定位思路
使用Java提供的命令工具:jps/jstack JvisualVM(图形化工具) JDK bin目录存放的是Java命令
1.首先需要确定java进程是否发生死锁
2.打开jvisualvm工具,专门分析JVMCPU,内存使用情况,以及线程的运行信息查看当前java进程各个线程运行的状态(颜色)
3.通过jvisualvm的线程dump或者jstack命令,把当前java进程所有线程的调用堆栈信息打印出来
4.分析main线程和子线程有没有关键短语:
waiting for(资源地址)
waiting to lock(资源地址)
5.看线程函数调用栈,定位到源码上,具体问题具体分析
线程池
JDK版本:1.7
需要掌握了解部分
1、为什么要有线程池?
2、线程池的类的继承关系?
2.1、存在哪些接口,都提供了哪些方法,有什么作用
3、ThreadPoolExecutor的研究
3.1、构造参数有哪些?分别什么意思
4、Executors静态工厂几种常用线程池
线程池使用
源码分析
1、线程池的执行流程
2、线程池如何提交任务
3、线程池如何终止
线程池的基础
为什么要线程池
多线程目的是用来最大化的发挥多核处理器的处理能力。但是线程是不能无限的进行创建,当线程创建数量比较多时,反而会消耗CPU和内存资源
• 线程的创建和销毁是需要时间的。加入一个服务完成需要的时间:T1表示创建线程时间,T2表示线程执行任务的耗时,T3表示线程消耗的时间,如果:T1+T3远大于T2,得不偿失
• 线程需要占用内存资源,大量线程的创建会占用宝贵内存资源,可以导致OOM(Out Of Memory)异常
• 大量的线程回收时也会给GC操作带来很大的压力,延长GC的停顿时间
• 线程抢占CPU资源,CPU不停的在各个线程中进行上下文切换,上下文的切换也是耗时的过程
多线程的创建不仅耗时,也占用资源,基于其缺陷提供了线程池的解决方案
什么是线程池
线程池就是事先创建若干个可执行的线程放入一个池中(容器),有任务需要执行时就从池中获取空闲的线程,任务执行完成后将线程放回池,而不用关闭线程,可以减少创建和销毁线程对象的开销
避免了频繁的创建和销毁线程,让创建的线程达到复用的目的,线程池的内部维护一部分活跃的线程,如果有需要就从线程中取线程使用,用完归还到线程池,线程池对线程的数量是有一定的限制。
线程池的本质是对线程资源的复用
线程池的优势
• 降低资源的消耗:通过复用已经创建的线程降低线程创建和销毁的消耗
• 提高响应速度:当任务到达时,任务直接从线程池中获取到一个线程就能立即执行
• 提高线程的可管理性:线程资源的管理,提高系统的稳定性
• 线程池可以进行统一的分配、调度和监控等功能
Executor是最基础的执行的接口
ExecutorService接口继承了Excutor接口,提供了一些对线程池操作的扩展方法:shutdown,submit,真正的线程池接口
AbstractExecutorService抽象类实现了ExecutorService接口提供的大部分方法
ThreadPoolExecutor继承自AbstractExecutorService,部分方法的具体实现
ScheduledExecutorService接口继承了ExecutorService接口,提供了带"周期执行"功能ExecutorService;
ScheduledThreadPoolExecutor既继承了TheadPoolExecutor线程池,也实现了ScheduledExecutorService接口,是带"周期执行"功能的线程池;
Executors是线程池的静态工厂,其提供了快捷创建线程池的静态方法。
Executor接口
执行者Executor提供了一个方法
public interface Executor {
void execute(Runnable command);
}
可以用来执行以及提交的Runable的任务对象,这个接口提供将一种将“任务提交”和“任务执行”解耦的方法
ExecutorService接口
真正的线程池的额接口,在Executor的基础上做了扩展
任务终止的相关方法shutdown
/**
* 启动一次有序的关闭,之前提交的任务执行,但不接受新任务
* 这个方法不会等待之前提交的任务执行完毕
*/
void shutdown();
/**
* 试图停止所有正在执行的任务,暂停处理正在等待的任务,返回一个等待执行的任务列表
* 这个方法不会等待正在执行的任务终止
*/
List<Runnable> shutdownNow();
/**
* 如果已经被shutdown,返回true
*/
boolean isShutdown();
/**
* 如果所有任务都已经被终止,返回true
* 是否为终止状态
*/
boolean isTerminated();
/**
* 在一个shutdown请求后,阻塞的等待所有任务执行完毕
* 或者到达超时时间,或者当前线程被中断
*/
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
任务提交相关操作:
/**
* 提交一个可执行的任务,返回一个Future代表这个任务
* 等到任务成功执行,Future#get()方法会返回null
*/
Future<?> submit(Runnable task);
/**
* 提交一个可以执行的任务,返回一个Future代表这个任务
* 等到任务执行结束,Future#get()方法会返回这个给定的result
*/
<T> Future<T> submit(Runnable task, T result);
/**
* 提交一个有返回值的任务,并返回一个Future代表等待的任务执行的结果
* 等到任务成功执行,Future#get()方法会返回任务执行的结果
*/
<T> Future<T> submit(Callable<T> task);
FutureTask和Future异同点?
ScheduledExecutorService接口
/**
* 在给定延时后,创建并执行一个一次性的Runnable任务
* 任务执行完毕后,ScheduledFuture#get()方法会返回null
*/
public ScheduledFuture<?> schedule(
Runnable command,
long delay,
TimeUnit unit);
/**
* 在给定延时后,创建并执行一个ScheduledFutureTask
* ScheduledFuture 可以获取结果或取消任务
*/
public <V> ScheduledFuture<V> schedule(
Callable<V> callable,
ong delay,
TimeUnit unit);
/**
* 创建并执行一个在给定初始延迟后首次启用的定期操作,后续操作具有给定的周期
* 也就是将在 initialDelay 后开始执行,然后在 initialDelay+period 后执行,接着在 initialDelay + 2 * period 后执行,依此类推
* 如果执行任务发生异常,随后的任务将被禁止,否则任务只会在被取消或者Executor被终止后停止
* 如果任何执行的任务超过了周期,随后的执行会延时,不会并发执行
*/
public ScheduledFuture<?> scheduleAtFixedRate(
Runnable command,
long initialDelay,
long period,
TimeUnit unit);
//scheduleAtFixedRate(c,1000,5,mills) 1000秒后执行,任务耗时是1秒 1000 1005 1010 ...
/**
* 创建并执行一个在给定初始延迟后首次启用的定期操作,随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟
* 如果执行任务发生异常,随后的任务将被禁止,否则任务只会在被取消或者Executor被终止后停止
*/
public ScheduledFuture<?> scheduleWithFixedDelay(
Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
//scheduleWithFixedDelay(c,1000,5,mills) 1000后执行,任务执行耗费1秒 1001 1006 1011 ...
ThreadPoolExecutor类
ThreadPoolExecutor构造参数
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)
corePoolSize:线程池中核心的线程数。当提交一个任务时,线程池创建一个新的线程执行任务,直到线程数量等于corePoolSize,如果当前的线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行
maximumPoolSize:线程池中允许的最大的线程数。如果哦当前的阻塞队列满了,继续提交的任务,则创建新的线程来执行任务,前提是线程池中线程数量小于maximumPoolSize
keepAliveTime:线程空闲是的存活时间。当前的线程池中没有任务执行线程继续存活时间,默认情况下,存活时间只在线程大于corePoolSize时才有用
unit:存活时间的单位
workQueue:等待队列,队列必须是BlockingQueue阻塞队列,当线程池中的线程数超过corePoolSize时,新提交的任务会进入到阻塞队列中进行阻塞等待,等待空闲的线程来队列中获取任务并执行
排队策略:
不排队,直接提交:SynchronoursQueue 无界队列:LinkedBlockingQueue
有界队列:ArrayBlockingQueue
threadFactory:创建线程的工厂,自定义实现时(实现ThreadFactory),提供给线程创建时设置一个具有识别度的线程名(线程池炒年糕提供了默认的线程工厂:DefaultThreadFactory 线程的命名规则:“Pool-数字-thread-数字”)
RejectedExecutionHandler:线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果还有新提交的任务,就需要采用饱和策略来处理了
线程池提供了4种策略:
(1)AbortPolicy:直接抛出异常,默认策略;
(2)CallerRunsPolicy:用调用者所在的线程来执行任务;
(3)DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
(4)DiscardPolicy:直接丢弃任务;
当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。
ThreadPoolExecutor的执行过程
线程池中提交新任务的执行过程:
1)如果线程池中的线程数量少于corePoolSize,就创建新的线程来执行新添加的任务;
(2)如果线程池中的线程数量大于等于corePoolSize,但队列workQueue未满,则将新添加的任务放到workQueue中,按照FIFO的原则依次等待执行(线程池中有线程空闲出来后依次将队列中的任务交付给空闲的线程执行);
(3)如果线程池中的线程数量大于等于corePoolSize,且队列workQueue已满,但线程池中的线程数量小于maximumPoolSize,则会创建新的线程来处理被添加的任务;
(4)如果线程池中的线程数量等于了maximumPoolSize,就用RejectedExecutionHandler来做拒绝处理
总结,当有新的任务要处理时,先看线程池中的线程数量是否大于corePoolSize,再看缓冲队列workQueue是否满,最后看线程池中的线程数量是否大于maximumPoolSize
另外,当线程池中的线程数量大于corePoolSize时,如果里面有线程的空闲时间超过了keepAliveTime,就将其移除线程池
Executors常见的线程池创建方式
Exectors工厂类提供了线程池创建的初始方法,主要4种
newSingleThreadExecutor:单个线程的线程池
单个线程的线程池实现:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
newSingleThreadExecutor底层实现是使用的ThreadPoolExecutor构造函数创建,其中参数corePoolSize和maxinumPoolSize相等,都是1,阻塞队列使用的是无界队列LinkedBlockingQueue。
适用场景:newSingleThreadExecutor常见的线程池是单个线程可以满足按照任务提交的顺序进行执行的需求场景
newFixedThreadPool:固定线程数量的线程池
代码演示:
newFixedThreadPool的构造实现:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
newFixedThreadPool底层实现使用
的是ThreadPoolExecutor,corePoolSize和maxinumPoolSize都是一样的,
阻塞队列是无界队列:LinkedBlockingQueue
典型优秀的线程池的实现,创建多线程进行复用,不用考虑频繁的创建和销毁的开销
newCachedThreadPool:可缓存工作线程的线程池
代码演示:
newCachedThreadPool的构造函数实现:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
newCachedThreadPool底层使用的是ThreadPoolExecutor来构造,corePoolSize是0,maxinumPoolSize是Integer.MAX_VALUE,阻塞队列使用是同步队列:SynchronousQueue
适用场景:适合耗时短,不需要考虑同步的场景
newScheduledThreadPool:周期性的执行任务的线程池
代码演示:
newScheduledThreadPool构造函数:
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
newScheduledThreadPool底层实现是使用ScheduledThreadPoolExecutor类来实现的
适用场景:执行周期性的任务是需要用该线程池的创建方法
Executors慎用
在《阿里巴巴Java开发手册》中有一条
【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
Executors 返回的线程池对象的弊端如下: FixedThreadPool 和 SingleThreadPool :
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 CachedThreadPool 和
ScheduledThreadPool : 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致
OOM。 在阿里的手册中给定的是强制,意味着这种操作一定要慎用,最佳实践的积累,
创建线程池是推荐使用ThreadPoolExecutor来创建线程池,可以基于当前的业务来合理的设置核心线程数,最大线程数和阻塞队列,拒接策略以及保持存活的时间,动态自定义的实现。
ThreadPoolExecutor的源码研究
线程池的执行流程
任务的提交execute(Runnable r)类型的任务,如何执行提交的任务:
任务提交执行的步骤:
1、提交任务时,如果线程池中线程数量小于corePoolSize,即使线程池有空闲的线程不会执行,直接创建新的线程来执行提交的任务
2、如果线程池中线程数量大于等于corePoolSize,队列未满时,新任务的提交添加到任务队列workQueue,等待线程池中有空闲线程来执行
3、如果线程池线程数量大于等于corePoolSize,并且阻塞队列已满,当前线程池中线程数量小于maxinumPoolSize,创建新的线程来执行任务体
4、当线程池的线程数量大于maxinumPoolSize,新任务提交时就会执行拒绝策略
线程池的状态
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
// Packing and unpacking ctl
private static int runStateOf(int c) { return c & ~CAPACITY; }//获取高3位的线程状态
private static int workerCountOf(int c) { return c & CAPACITY; }//获取低29位的线程数量
private static int ctlOf(int rs, int wc) { return rs | wc; } //rs表示线程池状态,wc表示线程池中线程数量,通过两个参数可获取ctl值
其中ctl是一个AtomicInteger类型的属性,器提供了线程池的状态及线程池中线程的数量的统计,其中高3位用于维护线程池的运行状态,低29位用来维护线程池中线程的数量
RUNNING = -1 << COUNT_BITS;即高3位为1,低29位0,该状态的线程池可以接受新任务,也会处理阻塞队列中的任务
SHUTDOWN = 0 <<
COUNT_BITS:即高3位是0,低29位为0,该状态的线程池不会接受新的任务,但是会处理阻塞队列中的任务 STOP = 1
<<
COUNT_BITS:即高3位为001,低29位为0,该状态的线程池不会接受新任务,也不会处理阻塞队列中的任务,并且还会终止正在执行的任务
TIDYING = 2 <<
COUNT_BITS:即高3位为010,低29位为0,所有的任务都被终止了,线程的数量为0,还要调用terminated()方法
TERMINATED = 3 << COUNT_BITS:即高3位为011,低29位为0,terminated()方法执行之后的状态
这些状态是int类型,大小关系为RUNNING<SHUTDOWN<STOP<TIDYING<TERMINATED
任务提交过程
execute():提交任务
public void execute(Runnable command) {
//提交的任务不能为null
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//判断工作线程数量小于核心线程数量
if (workerCountOf(c) < corePoolSize) {
//新创建一个线程来执行任务体====addWorker(command, true)
if (addWorker(command, true))
//添加成功,返回
return;
//添加任务失败;线程状态已经到showdown,不在接收新任务/工作线程数量大于核心线程数量
c = ctl.get();
}
//线程是运行状态,将任务添加到阻塞队列
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);
}
else if (!addWorker(command, false))
reject(command);
}
addWorker(Runnable r,Boolean b)
线程池中的Work而类是AbstractQueuedSynchronizer子类,并且实现了Runable接口,在线程池中所有的任务都会封装成Worker对象实例,提交的任务体可以是runnable(execute()\submit())或者是Callable类型的任务(submit())都会通过worker进行封装(FutureTask),将提交的任务封装成Worker类型的任务存放到阻塞队列
线程池终止:
shutdown():终止线程池执行:不在接收新任务,会处理正在执行的和阻塞队列中等待的任务
shutdownNow():立即终止线程池执行:不在接收新任务,不在处理阻塞队列中的任务,还尝试中断正在执行中的工作线程,返回一个list的集合存放的是阻塞队列中还未执行的任务