技术点
- 并发的挑战
减少上下文切换、解决死锁 - 并发底层实现原理
volatie可见性、synchronized锁的四种状态、原子操作的实现原理 - Java内存模型
线程之间的通信和同步、顺序一致性、volatile内存语义、CAS实现原理、ReetrantLock源码、concurrent包实现原理、JMM设计原理、happens-before,双重检查锁 - Java并发编程基础
线程状态转换、Daemon线程、等待/通知机制、线程池 - Java中的锁
Lock、AQS
并发的挑战
任务从保存到再加载的过程就是一次上下文切换,上下文切换会影响多线程的执行速度
线程因为创建和上下文切换的开销,有时并发执行的速度会比串行慢
减少上下文切换的三种方法
- 无锁并发编程
如将数据ID按照Hash算法取模分段,不同的线程处理不同段的数据 - CAS算法
Java的Atomic包使用CAS算法更新数据,而不需要加锁 - 使用最少线程和使用协程
再单线程里实现多任务的调度,并在单线程里维持多个任务间的切换
避免死锁的四种方法
- 避免一个线程同时获取多个锁
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
- 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
- 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况
并发底层实现原理
volatile
在硬件层面上是如何实现volatile的
如果一个字段被声明成volatile,JMM确保所有线程看到这个变量的值是一致的
volatile如何保证可见性的?
volatile变量进行写操作的时候,会多出一行汇编代码,前缀为lock,此行代码在多核处理器下引发两件事情:
- 将当前处理器缓存行的数据写回系统内存
- 写回内存的操作会使其它CPU里的缓存了该内存地址的数据无效(总线嗅探、MESI)
synchronized
锁的四种状态
- 方法:锁的是当前实例对象
- 类方法:锁的是当前类对象
- 代码块:括号里的对象
在Java中任何一个对象都有一个monitor与之关联,当且一个monitor被持有后,这个对象处于锁定状态。尝试获得锁就是尝试获取对象所对应的monitor的所有权
为了减少获得锁和释放锁带来的性能消耗,锁引入了四种状态,级别从高到低依次是无锁、偏向锁、轻量级锁、重量级锁
- 偏向锁
(1)获取:当一个线程获取锁,会在对象头和栈帧中存储锁偏向的线程ID,以后该线程进入和退出不需要CAS,只需要检测Mark Word中是否是指向当前线程的偏向锁
(2)撤销:等到竞争出现才释放锁的,而且需要等待全局安全点 - 轻量级锁
(1)加锁:JVM首先Displaced Mark Word,然后尝试CAS将对象头中的Mark Word替换成指向锁记录的指针。成功测获锁,失败则表示其他线程竞争锁,当前线程尝试自旋获取锁
(2)解锁:CAS将Displaced Mark Word替换回到对象头
因为自旋会消耗CPU,所以一旦锁升级成重量级锁,就不会回复到轻量级锁状态
优缺点比较
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加解锁不需要额外消耗 | 如果存在竞争,会带来额外锁撤销的消耗 | 只有一个线程访问同步块的场景 |
轻量级锁 | 竞争的线程不会阻塞 | 如果始终得不到锁竞争的线程会因为自旋消耗CPU | 追求响应时间,同步快执行速度非常快 |
重量级锁 | 竞争不使用自旋,不消耗CPU | 线程阻塞,响应时间慢 | 追求吞吐量,同步块执行较长 |
原子操作的实现原理
Intel处理器和Java怎么实现原子操作的
处理器是基于对缓存加锁或总线加锁的方式来实现原子操作
- 使用总线锁
典型的例子:i++
想要保证读改写共享变量的操作是原子的,就必须保证CPU1读改写的时候,CPU2不能操作缓存了该内存地址的缓存
总线锁:使用处理器提供的一个LOCK #信号。当一个处理器在总线上输出此信号时,其它处理器的请求被阻塞,那么该狐狸其可以独占共享内存
- 使用缓存锁
总线锁把CPU和内存之间的通信锁住了,开销大,使用缓存锁替代总线锁来进行优化
缓存锁定:内存区域如果被缓存在处理器的缓存行中,并且Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器会修改内部的内存地址,并允许MESI保证操作的原子性
Java中通过锁和循环CAS方式实现原子操作
-
锁
除了偏向锁,都用了循环CAS -
循环CAS
CAS利用了处理器提供的CMPXCHG指令。自旋CAS思路是循环进行CAS直到成功AtomicBoolean,AtomicInteger,AtomicLong支持原子操作
CAS存在的三个问题
- ABA问题:如果一个值原来是A,变成了B,又变成了A,CAS发现值没有变化,但实际上却变化了。该问题解决思路是使用版本号,Atomic类的AtomicStampedReference类解决。
- 循环时间开销大
- 只能保证一个共享变量的原子操作:多个变量要用锁,获取合并成一个变量。AtomicReference类保证引用对象之间的原子性
Java内存模型
Java内存模型的基础
关键问题:线程之间如何通信、线程之间如何同步
Java采用的是共享内存模型,JMM决定一个线程对共享变量的写入何时对另一个线程可见,通信过程必须经过主内存
在命令式编程中,并发模型两种:共享内存和消息传递
共享内存:通过读写内存中的公共状态进行隐式通信,必须显式指定需要同步的代码块
消息传递:必须通过发送消息来显式通信,由于发消息在收消息之前,所以同步是隐式的
顺序一致性
两大特性
- 一个线程中的所有操作必须按照程序的顺序的执行
- 所有线程都只能看到一个单一的操作执行顺序,每个操作都必须原子执行并且立刻对所有线程可见
为了提高性能,指令会重排序。JMM通过禁止特定类型的编译器重排序和处理器重排序为程序员提高一致的内存可见性保证
同步原语
volatile自身特性
- 可见性
- 原子性
volatile写的内存语义
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存中
volatie读的内存语义
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,接下来将从主内存中读取共享变量
锁释放对应volatile写,锁获取对应volatile读
借助ReentrantLock源代码分析锁内存语义的具体实现机制
首先分析公平锁
- 加锁方法lock()的调用轨迹是
ReentrantLock:lock() -> FairSync:lock() -> AQS:acquire(int arg) -> ReentrantLock:tryAquire(int acquires)
第四步才真正加锁,该方法的源代码
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); //获取锁的开始,首先读volatile变量state
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
- 解锁方法unlock()的调用轨迹是
ReentrantLock:unlock() -> AQS:release(int arg) -> Sync:tryRelease(int releases)
第三步才真正释放锁,该方法的源代码
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c); //释放锁的最后,写volatile变量state
return free;
}
非公平锁的释放和公平锁完全一样
非公平锁加锁方法lock()的调用轨迹是
ReentrantLock:lock() -> NofairSync:lock() -> AQS:compareAndSetState(int expect, int update)
第三步才真正加锁,该方法的源代码
protected final boolean compareAndSetState(int expect, int update) {
return STATE.compareAndSet(this, expect, update);
}
该方法以CAS原子的操作更新state变量
由于编译器不会对volatile读与volatile读后面的任意内存操作重排序,编译器不会对volatile写与volatile写前面的任意内存操作重排序,组合这两个条件意味着编译器不能对CAS与CAS前面和后面的任意内存操作重排序
CAS是本地方法,最终如果程序是多处理器上运行,就为cmpxchg指令加上lock前缀;如果是单处理器上运行,就省略lock前缀
lock前缀的说明,最后两条具有内存屏障效果,所以CAS同时具有volatile读和写的内存语义
- 确保对内存的读-改-写操作原子执行
- 禁止该指令与之前和之后的读写指令重排序
- 把写缓冲区的所有数据刷新到内存中
所以,Java线程之间的通信有了以下4种方式
- A线程写volatile变量,随后B线程读这个volatile变量
- A线程写volatile变量,随后B线程用CAS更新这个volatile变量
- A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量
- A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量
整个concurrent包得以实现的基石就是:volatile变量的读/写和CAS,通用化的实现方式是
- 声明共享变量是volatile
- 使用CAS更新来实现线程之间的同步
- 配合volatile和CAS实现通信
设计原理
happens-before:目的是在不改变程序执行结果的前提下,尽可能提高并行度
定义:
- 对程序员的承诺:如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前
- 对编译器和处理器重排序的约束原则:两个操作存在happens-before关系,如果重排之后执行结果一致,那么允许这种重排序
规则
- 程序顺序规则:一个线程的每个操作,happens-before于该线程中的任意后续操作
- 监视器锁规则:解锁happens-before于随后对这个锁的加锁
- volatile变量规则:写happens-before读
- 传递性:A happens-before B,B happens-before C,则A happens-before C
- start()规则:如果线程A执行操作ThreadB.start(),那么这个操作happens-before于线程B的任意操作
- join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么B的任意操作happens-before于线程A从ThreadB.join()成功返回
左边start()解释: - 1 happens-before 2:程序顺序规则
- 2 happens-before 4:start()规则
- 1 happens-before 4:传递性
右边join()解释: - 2 happens-before 4:join()规则
- 4 happens-before 5:程序顺序规则
- 2 happens-before 5:传递性
双重检查锁定
public class DoubleCheckedLocking { // 1
private static Instance instance; // 2
public static Instance getInstance() { // 3
if (instance == null) { // 4:第一次检查
synchronized (DoubleCheckedLocking.class) { // 5:加锁
if (instance == null) // 6:第二次检查
instance = new Instance(); // 7:问题的根源出在这里
}
}
return instance;
}
}
代码的第7行创建了一个对象,可以分解为如下3行代码:
memory = allocate(); //1.分配对象的内存空间
ctorInstance(memory); //2.初始化对象
instance = memory; //3.设置instance指向刚分配的内存地址
2和3之间可能会重排序
memory = allocate(); //1.分配对象的内存空间
instance = memory; //3.设置instance指向刚分配的内存地址
//注意:此时对象还没有初始化
ctorInstance(memory); //2.初始化对象
多线程执行情况,由于线程B在第4行判断instance不为null,接下来就会去访问instance引用的对象,但这个对象还没有被A初始化!
解决方法:将instance声明为volatile型
public class DoubleCheckedLocking { // 1
private volatile static Instance instance; // 2
public static Instance getInstance() { // 3
if (instance == null) { // 4:第一次检查
synchronized (DoubleCheckedLocking.class) { // 5:加锁
if (instance == null) // 6:第二次检查
instance = new Instance(); // 7:问题的根源出在这里
}
}
return instance;
}
}
重排序被禁止后,代码将按如下时序执行
Java并发编程基础
- 什么是线程?
操作系统调度的最小单元,也叫轻量级进程,每个线程拥有各自的计数器、堆栈、局部变量等属性,并且能够访问共享的内存变量 - 线程优先级
1~10,用setPriority(int)修改,默认是5。程序正确性不能依赖优先级,因为操作系统可以完全忽略Java对于优先级的设定 - 线程状态
Daemon线程
被用作后台调度和支持性工作,使用Thread.setDaemon(true)设置
当JVM不存在非Daemon线程的时候,JVM会退出,可能导致Daemon线程的finally()块并不一定会执行,所以不能依靠finally来确保执行关闭或清理资源的逻辑
线程的启动和终止
- 构造线程
- 启动线程:start()
- 中断:其它线程通过调用该线程的interrupt()方法对其进行中断
方法在抛出InterrupedException之前,JVM会清除中断标识位再抛异常,此时调用isInterrupted()会返回false
等待/通知机制是指线程A调用了对象O的wait()方法进入等待状态,线程B调用notify()或notifyAll(),线程A收到通知后从对象O的wait()返回,进行后续操作
- wait()、notify()、notifyAll()都需要先对调用对象加锁
- wait()后线程从running变为waiting
- notify()或notifyAll(),等待线程不会立即返回,需要调用此方法的线程释放锁
Thread.join():线程A执行了thread.join()含义是线程A等待thread线程终止之后才返回,可以设置超时时间
ThreadLocal,即线程变量,用过set()和get()设置和获取值
线程池解决了线程创建和消亡浪费系统资源问题
本质是使用一个线程安全的工作队列连接工作者线程和客户端线程。客户端线程将任务放入工作队列后返回,工作者线程不断地从工作队列取出工作并执行
Java中的锁
Java并发包中与锁相关的API和组件的使用和实现
Lock接口
Java SE5之前,Java是依靠synchronized实现锁功能
Java SE5之后,新增Lock接口实现锁功能,提供了与synchronized相似的同步功能,只是需要显式的获取和释放锁
虽然失去了隐式的便捷,但拥有了以下特性
特性 | 描述 |
---|---|
尝试非阻塞式获取锁 | 当前线程尝试获取锁,如果这一刻没有被其它线程获取到,则成功获取并持有锁 |
能被中断地获取锁 | 获取到锁的线程能够响应中断,当被中断时,中断异常将被抛出,同时锁会被释放 |
超时获取锁 | 在指定的截止时间之前获取锁,时间到仍旧无法获取锁就返回 |
Lock的API,Lock接口的实现一般是通过聚合一个同步器的子类完成线程的访问控制
方法 | 描述 |
---|---|
void lock() | 获取锁 |
void lockInterruptibly() throws InterruptedException | 在锁的获取中可以响应中断 |
boolean tryLock() | 尝试非阻塞获取锁,调用后立即返回,能够获取返回true |
boolean tryLock(long time, TimeUnit unit) throws InterruptedException | 超时获取锁,三种返回:在时间内获得锁;在时间内被中断;超时 |
void unlock() | 释放锁 |
Condition newCondition() | 获取等待通知组件,该组件和当前锁绑定,只有获取锁才能调用该组件的wait(),调用后释放锁 |
队列同步器AQS
int成员变量表示同步状态,FIFO队列完成线程排队,使用方式主要是继承,设计基于模板方法模式
同步器三个方法访问或修改同步状态
方法 | 描述 |
---|---|
getState() | 获取 |
setState(int newState) | 设置 |
compareAbdSetState(int expect, int update) | 使用CAS设置 |
同步器可重写的方法
方法 | 描述 |
---|---|
protected boolean tryAcquire(int arg) | 独占式获取同步状态 |
protected boolean tryRelease(int arg) | 独占式释放同步状态 |
protected int tryAcquireShared(int arg) | 共享式获取同步状态 |
protected boolean tryReleaseShared(int arg) | 共享式释放 |
protected boolean isHeldExclusively() | 是否被当前线程所独占 |
实现分析
- 同步队列
- 独占式同步状态获取与释放
调用同步器的acquire(int arg)获取同步状态,如果失败进入同步队列,后续对线程进行中断,线程不会从同步队列中移除
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
上述代码主要逻辑
- tryAcquire(arg)获取同步状态
- 失败则构造同步节点并通过addWaiter(Node.EXCLUSIVE)方法加入队列尾部
- 最后调用acquireQueued()使该节点以死循环的方式获取同步状态
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
if (interrupted)
selfInterrupt();
throw t;
}
}
为什么只有前驱结点的头节点才能尝试获取同步状态?
- 头节点是成功获取同步状态的节点,头节点释放后唤醒其后继节点,后继节点环形后需要检查自己的前驱节点是否是头节点
- 维护同步队列的FIFO原则
总结:在获取同步状态时,同步器维护一个同步队列,获取失败的线程都会加入到队列中并在队列中进行自旋;移出队列的条件是前驱节点是头节点并且成功获取同步状态。在释放时,同步器调用tryRelease()方法释放,然后唤醒头节点的后继节点
重入锁ReentrantLock
ReentrantLock默认是非公平的
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
公平锁唯一的不同是判断条件多了hasQueuedPredecessors()
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
读写锁
读写锁维护了一对锁,读锁和写锁
在读多于写的情况下,读写锁有更好的并发性和吞吐量
在写锁获取到时,后续其它线程的读写都会被阻塞
在读锁获取时,其它线程可以读
如果存在读锁,则写锁不能被获取
写锁是支持重入的排他锁
LockSupport
当需要阻塞或唤醒一个线程的时候,使用LockSupport工具类
park()、unpack()
Condition
Object有一组监视器方法wait(),nofify(),notifyAll(),Condition提供了类似的
Condition是由Lock对象创建出来的
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void conditionWait() throws InterruptedException {
lock.lock();
try {
condition.await(); //当前线程释放锁并等待
} finally {
lock.unlock();
}
}
public void conditionSignal() throws InterruptedException {
lock.lock();
try {
condition.signal(); //通知当前线程,从await()返回并且在返回前已获得锁
} finally {
lock.unlock();
}
}
Condition的实现分析
Condition是AQS的内部实现
- 调用condition.await(),那么该线程释放锁,加入等待队列进入等待状态
- 调用condition.signal(),将首节点移到同步队列中,唤醒节点
- signalAll()类比notifyAll()
Java并发容器和框架
ConcurrentHashMap
锁分段:将数据分成一段一段存储,每一段配一把锁
ConcurrentHashMap由Segment和HashEntryzucheng
segments数组初始化
- 长度ssize是通过DEFAULT_CONCURRENCY_LEVEL计算得出,长度是2的N次方
- segmentShif用于定位参与散列运算的位数,32是因为hash()方法输出的最大数是32位
- segmentMask是掩码,每个位都是1
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
// For serialization compatibility
// Emulate segment calculation from previous version of this class
int sshift = 0;
int ssize = 1;
while (ssize < DEFAULT_CONCURRENCY_LEVEL) {
++sshift;
ssize <<= 1;
}
int segmentShift = 32 - sshift;
int segmentMask = ssize - 1;
@SuppressWarnings("unchecked")
Segment<K,V>[] segments = (Segment<K,V>[])
new Segment<?,?>[DEFAULT_CONCURRENCY_LEVEL];
//...
}
get()操作不加锁是因为要使用的共享变量都定义成volatile,在get操作只需要读不需要写,所以可以不用加锁
put()操作首先定位segment再插入。插入第一步判断是否扩容(只对单个segment扩容),第二步定位
size操作,先尝试两次通过不锁住segment统计各个大小,如果统计的过程中,利用modCount变量判断容器的count是否发生变化,如果变化再加锁
ConcurrentLinkedQueue
实现一个线程安全的队列有两种方法
- 阻塞算法:使用一个或两个锁实现
- 非阻塞算法:CAS
ConcurrentLinkedQueue是非阻塞的
默认情况下head节点存储的元素为空,tail节点等于head,都用volatile修饰
入队
- 将入队节点设置成当前队列尾节点的下一个节点
- 更新tail节点
- 如果tail节点的next节点不为空,则入队节点设置成tail节点
- 如果tail节点的next节点为空,则将入队节点设置成tail的next节点,所以tail节点不总是尾节点
出队
并不是每次出队都更新head,只有当head节点没有元素的时候才更新,减少CAS消耗
阻塞队列
-
支持阻塞的插入方法:当队列满时,队列会阻塞插入元素的线程,直到队列不满
-
支持阻塞的移除方法:当队列为空时,获取元素的线程会等待队列变为非空
常用于生产者/消费者的场景 -
ArrayBlockingQueue
默认不保证线程公平的访问队列 -
LinkedBlockingQueue
默认和最大长度为Integer.MAX_VALUE -
PriorityBlockingQueue
默认采取自然顺序升序排列,也可以自定义compareTo()指定,也可以指定构造参数Comparator -
DelayQueue
支持延时获取,队列使用PriorityQueue,元素必须实现Delay接口,应用场景有缓存系统的缓存失效、定时任务调度 -
SynchronousQueue
不存储元素的阻塞队列,每一个put都等待一个take,否则不能继续添加元素 -
LinkedTransferQueue
-
LinkedBlockingDueue
并发工具类
等待多线程完成的CountDownLatch
CountDownLatch的构造器接收一个int的参数作为计数器,等待N个点完成
countDown()方法时候,N会减一
await()阻塞当前线程,直到N变成0
由于countDown()可以用在任何地方,所以N可以是N个线程,也可以是N个执行步骤
同步屏障CyclicBarrier
让一组线程达到一个屏障(也可以叫同步点)时阻塞,直到最后一个线程到达屏障时,屏障才会打开,所有线程才会继续运行
每个线程调用await()告诉CyclicBarrier我已经到达,然后被阻塞
CountDownLatch和CyclicBarrier区别
- CountDownLatch计数器只能用一次,CyclicBarrier可以使用reset()方法重置
控制并发线程数的Semaphore
Semaphore(信号量)是用来控制同时访问特定资源的线程数量
线程间交换数据的Exchanger
用于线程间的数据交换
如果第一个线程先执行exchange(),它会等第二个线程也执行exchange(),然后将本线程生成的数据传递给对方
线程池
优点
- 限制资源消耗
- 提高响应速度
- 提高线程的可管理性
当一个新任务到线程池的时候,处理流程
- 核心线程池是否执行任务?如果不是,创建一个新的工作线程来执行;如果是,下个流程
- 工作队列是否已满?如果没满,存储到工作队列;如果满,下个流程
- 线程池的线程是否都处于工作状态?如果不是,创建一个新的工作线程执行;如果是,交给饱和策略处理这个任务
创建线程池的参数
-
corePoolSize线程池基本大小:当提交一个任务到线程池,线程池会创建一个线程执行,即使其它空闲的基本线程能够执行新任务也会创建,直到需要执行的任务数大于此参数才不再创建。
如果调用prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程
-
runnableTaskQueue任务队列:用于保存等待执行的任务的阻塞队列
-
maximumPoolSize线程池最大数量:线程池允许创建的最大线程数。如果队列满,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务
如果使用了无界队列,则此参数无效
-
ThreadFactory:用于创建线程的工厂
-
RejectedExecutionHandle饱和策略:当队列和线程池都满了,说明线程池处于饱和状态。此策略默认是AbortPolicy
JDK中有以下四种策略
- AbortPolicy:直接抛出异常
- CallerRunsPolicy:只用调用者所在线程来运行任务
- DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务
- DiscardPolicy:不处理,丢弃掉
-
keepAliveTime线程活动保持时间:线程池的工作线程空闲后,保持存活的时间。所以如果任务很多且每个执行时间比较短,可以调大时间,提高线程利用率
-
TimeUnit线程活动保持时间的单位
向线程池提交任务
- execute():提交不需要返回值的任务。无法判断是否被线程池执行成功
- submit():提交需要返回值的任务,返回future类型的对象,可通过此对象判断是否执行成功,调用get()获取返回值,get()会阻塞当前线程直到任务完成,可以设置时间
关闭线程池:shutdown或shutdownNow
原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的线程永远无法终止
区别:
shutdownNow首先将线程池状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表
shutdown将线程池状态设置成SHUTDOWN,然后中断所有没有正在执行任务的线程
两种方法的任一调用,isShutdown都返回true
所有任务关闭才表示线程池关闭成功,isTerminated方法返回true
监控线程池的属性
- taskCount:线程池需要执行的任务数量
- completedTaskCount:已完成的任务数量
- largestPoolSize:曾经创建过的最大线程数量
- getPoolSize:线程池的线程数量。如果线程池不销毁的话,线程池里的线程不会自动销毁,所以这个大小只增不减
- getActiveCount:获取活动的线程数