Java并发
1 中断
1.1 打断阻塞线程
Thread t1 = new Thread(()->{
try {
sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "t1");
t1.start();
sleep(1);
t1.interrupt();
log.debug(" 打断状态: {}", t1.isInterrupted());
1.2 打断运行线程
一个比较关键的点在于:正在运行的线程接受到interrupt后并不会即可打断线程,而是会修改该线程的状态值,在被打断线程内我们可以对这个值进行判断,去做一些善后操作
Thread t1 = new Thread(() -> {
while (true){
}
});
t1.start();
TimeUnit.SECONDS.sleep(1);
t1.interrupt();
System.out.println("interrupt state: " + t1.isInterrupted());
如上可见,打断之后线程仍然在运行。
由此我们可以做如下善后操作:
Thread t1 = new Thread(() -> {
while (true){
if (Thread.currentThread().isInterrupted()){
System.out.println("线程被打断了");
break;
}
}
});
t1.start();
TimeUnit.SECONDS.sleep(1);
t1.interrupt();
System.out.println("interrupt state: " + t1.isInterrupted());
如此我们就实现了对线程的打断操作。
1.3 打断需注意⚠️
不可使用stop停止线程
stop() 方法会真正的杀死线程,如果此时线程锁住了共享资源,那么当它被杀死后就不会有机会释放锁,其他线程也会永远无法获取锁。
不可使用System.exit()
目的仅是停止一个线程,但是该方法会使整个程序停下。
1.3 俩阶段打断
class InterruptTask{
private Thread monitor;
public void start(){
monitor = new Thread( () -> {
while (true) {
Thread current = Thread.currentThread();
if (current.isInterrupted()) {
System.out.println("程序打断");
break;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 关键点在此,程序在睡眠状态被打断后,会重制标记值,此时
// true -》 false 则不会再执行打断的善后操作
// 所以需要回复标记才可
current.interrupt();
}
}
} );
}
public void stop(){
monitor.interrupt();
}
}
⚠️防止在睡眠阶段被打断
所以需要重新标记打断值。切记,这里指的是在sleep那一行代码执行中被打断捕获异常后才会恢复打断标记,此时我们才需要重制打断标记。
2 共享模型之管程
2.1 对象底层
2.1.1 Java对象头
以32位虚拟机为例:
普通对象
|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------------|-------------------------|
数组对象
|---------------------------------------------------------------------------------|
| Object Header (96 bits) |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|
⚠️对象头的组成
这部分主要用来存储对象自身的运行时数据,如hashcode、gc分代年龄等。mark word
的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word
为32位,64位JVM为64位。
为了让一个字大小存储更多的信息,JVM将字的最低两个位设置为标记位,不同标记位下的Mark Word示意如下:
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | lock:2 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | lock:2 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | lock:2 | Marked for GC |
|-------------------------------------------------------|--------------------|
其中各部分的含义如下:
*lock*:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个mark word表示的含义不同。
biased_lock | lock | 状态 |
---|---|---|
0 | 01 | 无锁 |
1 | 01 | 偏向锁 |
0 | 00 | 轻量级锁 |
0 | 10 | 重量级锁 |
0 | 11 | GC标记 |
biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold
选项最大值为15的原因。
identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()
计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。
thread:持有偏向锁的线程ID。
epoch:偏向时间戳。
ptr_to_lock_record:指向栈中锁记录的指针。
ptr_to_heavyweight_monitor:指向管程Monitor的指针。
64位下的标记字与32位的相似,不再赘述:
|------------------------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|------------------------------------------------------------------------------|--------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | Normal |
|------------------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | Biased |
|------------------------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62 | lock:2 | Lightweight Locked |
|------------------------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | lock:2 | Heavyweight Locked |
|------------------------------------------------------------------------------|--------------------|
| | lock:2 | Marked for GC |
|------------------------------------------------------------------------------|--------------------|
2.1.2 class pointer
这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。
如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项+UseCompressedOops
开启指针压缩,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:
- 每个Class的属性指针(即静态变量)
- 每个对象的属性指针(即对象变量)
- 普通对象数组的每个元素指针
当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。
2.1.3 array length
如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着JVM架构的不同而不同:32位的JVM上,长度为32位;64位JVM则为64位。64位JVM如果开启+UseCompressedOops
选项,该区域长度也将由64位压缩至32位
2.2 Monitor
如上图所示:
- 初始状态Owner为NULL,但是当
Thread-1
开行执行到synchronized时会将Monitor的所有者Owner置为Thread-1
, 但是该指针只能指向一个对象 - 所在当
Thread-1
获取Owner控制权后,如果其他线程仍然要执行同步代码块就会进入EntryList
,进而被阻塞,线程状态被置为BLOCKED - 当
Thread-1
执行完毕后,则唤醒EntryList
中的线程,进行非公平竞争 - 同时出国获取锁途中出现wait则进入WaitSet
2.3 Synchronized原理
2.3.1 轻量级锁
轻量级锁是JDK1.6之中加入的新型锁机制,它名字中的“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就称为“重量级”锁。首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。
执行流程如下:
⚠️注意:
当对象已经被轻量级锁定的时候,会判断是否是锁重入,如果是重入的话,会记录一条Displaced Mark Word为空的Lock Record。如果不是重入,会膨胀为重量级锁。需要注意的是,即使膨胀为重量级锁,没有获取到锁的线程也不会马上阻塞,而是通过适应性自旋尝试获取锁,当自旋次数达到临界值后,才会阻塞未获取到的线程。JVM认为获取到锁的线程大概率会很快的释放锁,这样做是为了尽可能的避免用户态到内核态的切换
2.3.2 偏向锁
在没有实际竞争的情况下,还能够针对部分场景继续优化。如果不仅仅没有实际竞争,自始至终,使用锁的线程都只有一个,那么,维护轻量级锁都是浪费的。偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。
“偏向”的意思是,偏向锁假定将来只有第一个申请锁的线程会使用锁(不会有任何线程再来申请锁),因此,只需要在Mark Word中CAS记录owner(本质上也是更新,但初始值为空),如果记录成功,则偏向锁获取成功,记录锁状态为偏向锁,以后当前线程等于owner就可以零成本的直接获得锁;否则,说明有其他线程竞争,膨胀为轻量级锁。
偏向锁无法使用自旋锁优化,因为一旦有其他线程申请锁,就破坏了偏向锁的假定。
同样的,如果明显存在其他线程申请锁,那么偏向锁将很快膨胀为轻量级锁。
其次,偏向锁是延迟加载的,可通过jvm参数禁用偏向锁延迟机制
禁用偏向锁
Object obj = new Object();
obj.hashCode(); // 该操作会禁用偏向锁
-xx:UseBiasedLocking # 参数禁用
2.3.3 自旋优化
首先,内核态与用户态的切换上不容易优化。但通过自旋锁,可以减少线程阻塞造成的线程切换(包括挂起线程和恢复线程)。
如果锁的粒度小,那么锁的持有时间比较短(尽管具体的持有时间无法得知,但可以认为,通常有一部分锁能满足上述性质)。那么,对于竞争这些锁的而言,因为锁阻塞造成线程切换的时间与锁持有的时间相当,减少线程阻塞造成的线程切换,能得到较大的性能提升。具体如下:
- 当前线程竞争锁失败时,打算阻塞自己
- 不直接阻塞自己,而是自旋(空等待,比如一个空的有限for循环)一会
- 在自旋的同时重新竞争锁
- 如果自旋结束前获得了锁,那么锁获取成功;否则,自旋结束后阻塞自己
如果在自旋的时间内,锁就被旧owner释放了,那么当前线程就不需要阻塞自己(也不需要在未来锁释放时恢复),减少了一次线程切换。
“锁的持有时间比较短”这一条件可以放宽。实际上,只要锁竞争的时间比较短(比如线程1快释放锁的时候,线程2才会来竞争锁),就能够提高自旋获得锁的概率。这通常发生在锁持有时间长,但竞争不激烈的场景中。
缺点
- 单核处理器上,不存在实际的并行,当前线程不阻塞自己的话,旧owner就不能执行,锁永远不会释放,此时不管自旋多久都是浪费;进而,如果线程多而处理器少,自旋也会造成不少无谓的浪费。
- 自旋锁要占用CPU,如果是计算密集型任务,这一优化通常得不偿失,减少锁的使用是更好的选择。
- 如果锁竞争的时间比较长,那么自旋通常不能获得锁,白白浪费了自旋占用的CPU时间。这通常发生在锁持有时间长,且竞争激烈的场景中,此时应主动禁用自旋锁。
2.4 wait 和 Notify
2.4.1 sleep 与 wait 区别
- sleep是Thread方法,而wait是Object方法
- sleep不需要强制和synchronized配合,后者需要
- sleep不会释放锁,但是wait会,并且曾经必须拥有过锁
2.4.2 正确用法
synchronized(lock) {
while(条件不成立) {
lock.wait();
}
// 干活
}
//另一个线程
synchronized(lock) {
lock.notifyAll();
}
2.4.3 同步模式之保护性暂停
@Slf4j
public class GuardedObject {
private Object response;
public final Object lock = new Object();
public Object get() throws InterruptedException {
synchronized (lock){
while (response == null ){
log.info("进入等待---");
lock.wait(2000);
}
log.info("被唤醒后: 值为" + ((AtomicInteger) response).get());
}
return this.response;
}
public void complete(Object response){
synchronized (lock){
this.response = response;
log.info("唤醒");
lock.notifyAll();
}
}
}
class TestMain{
public static void main(String[] args) throws InterruptedException {
GuardedObject guardedObject = new GuardedObject();
Thread get = new Thread(() -> {
try {
guardedObject.get();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
get.start();
TimeUnit.SECONDS.sleep(5);
Thread complete = new Thread(() -> {
guardedObject.complete(new AtomicInteger(1));
});
complete.start();
}
}
下面是改进版的超时版本
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public class GuardedObject {
private Object response;
public final Object lock = new Object();
public Object get(long timeout) throws InterruptedException {
synchronized (lock){
long begin = System.currentTimeMillis();
long passedTime = 0;
while (response == null ){
long waitTime = timeout - passedTime;
if (waitTime <= 0)
break;
log.info("进入等待---");
lock.wait(waitTime);
passedTime = System.currentTimeMillis() - begin;
}
if (response != null)
log.info("被唤醒后: 值为" + ((AtomicInteger) response).get());
else
log.info("获取超时");
}
return this.response;
}
public void complete(Object response){
synchronized (lock){
this.response = response;
log.info("唤醒");
lock.notifyAll();
}
}
}
class TestMain{
public static void main(String[] args) throws InterruptedException {
GuardedObject guardedObject = new GuardedObject();
Thread get = new Thread(() -> {
try {
guardedObject.get(7000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
get.start();
TimeUnit.SECONDS.sleep(5);
Thread complete = new Thread(() -> {
guardedObject.complete(new AtomicInteger(1));
});
complete.start();
}
}
2.4.4 解耦
2.4.5 生产者消费者模式
2.5 park 与 UnPark
LockSupport.park();
LockSupport.unpark(Thread);
原理如图
-
调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
-
唤醒 _cond 条件变量中的 Thread_0
-
Thread_0 恢复运行
-
设置 _counter 为 0
2.6 线程状态
2.7 多锁问题
业务无关联情况,可设计独立多锁
2.8 活跃性
2.8.1 模拟死锁
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j
public class DeadLockDemo {
public static void main(String[] args) {
final Object lockA = new Object();
final Object lockB = new Object();
Thread t1 = new Thread( () -> {
synchronized (lockA){
log.info("t1 lock A");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (lockB){
log.info("t1 lock B");
}
}
} );
Thread t2 = new Thread( () -> {
synchronized (lockB){
log.info("t1 lock B");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (lockA){
log.info("t1 lock A");
}
}
} );
t1.start();
t2.start();
}
}
哲学家就餐问题
2.9 ReentrantLock
相对于synchronized它具有如下特点:
- 可中断
- 可以设置超时时间
- 可以设置公平锁
- 支持多个条件变量
2.9.1 条件变量
2.9.2 同步模式之顺序控制
穿行输出abc
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 串行输出
*/
@Slf4j
public class SerialReentrantLockDemo {
private static final Lock lock = new ReentrantLock();
public static final Condition _A_put = lock.newCondition();
public static final Condition _B_put = lock.newCondition();
public static final Condition _C_put = lock.newCondition();
public static String flag = "c";
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
lock.lock();
try {
for (int i = 0; i < 10; i++) {
while (flag != "c"){
_A_put.await();
}
flag = "a";
System.out.println(flag);
_B_put.signal();
}
} catch (InterruptedException e) {
} finally {
lock.unlock();
}
});
Thread t2 = new Thread(() -> {
lock.lock();
try {
for (int i = 0; i < 10; i++) {
while (flag != "a"){
_B_put.await();
}
flag = "b";
System.out.println(flag);
_C_put.signal();
}
} catch (InterruptedException e) {
} finally {
lock.unlock();
}
});
Thread t3 = new Thread(() -> {
lock.lock();
try {
for (int i = 0; i < 10; i++) {
while ( flag != "b"){
_C_put.await();
}
flag = "c";
System.out.println(flag);
_A_put.signal();
}
} catch (InterruptedException e) {
} finally {
lock.unlock();
}
});
t1.start();
t2.start();
t3.start();
}
}
简化版本如下:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class SerialDemo {
}
class SignalLock extends ReentrantLock{
private int loopNum;
public SignalLock (int loopNum){
this.loopNum = loopNum;
}
public void print(String str, Condition current, Condition next){
for (int i = 0; i < loopNum; i++) {
lock();
try {
current.await();
System.out.println(str);
next.signal();
} catch (InterruptedException e) {
}finally {
unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
SignalLock lock = new SignalLock(10);
Condition a = lock.newCondition();
Condition b = lock.newCondition();
Condition c = lock.newCondition();
Thread t1 = new Thread( () -> {
lock.print("a", a, b);
});
Thread t2 = new Thread( () -> {
lock.print("b", b ,c);
});
Thread t3 = new Thread( () -> {
lock.print("c" ,c ,a);
});
t1.start();
t2.start();
t3.start();
TimeUnit.SECONDS.sleep(1);
lock.lock();
try {
System.out.println("begin");
a.signal();
}finally {
lock.unlock();
}
}
}f
2.10 JMM
JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。
JMM 体现在以下几个方面
-
原子性 - 保证指令不会受到线程上下文切换的影响
-
可见性 - 保证指令不会受 cpu 缓存的影响
-
有序性 - 保证指令不会受 cpu 指令并行优化的影响
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
// ....
}
});
t.start();
sleep(1);
run = false; // 线程t不会如预想的停下来
}
根据上述代码块可以得出一个结论:
程序运行起来后不会因为 main 线程对 flag 的重制标记而停止:
===》 这是因为 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,
减少对主存中 run 的访问,提高效率
JIT内存优化
那么如何解决上述问题呢?
采用关键词 volatile
@Slf4j
public class TestJITDemo {
static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (flag){
}
});
thread.start();
TimeUnit.SECONDS.sleep(2);
log.info("开始 begin -> to false");
flag = false;
}
}
使用该关键词后不会导致内存优化,每次执行还是去取值,从而避免去工作缓存取值造成歧义(可见性问题)
2.10.1 同步模式之 Balking
Balking (犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回
实现:
public class MonitorService {
// 用来表示是否已经有线程已经在执行启动了
private volatile boolean starting;
public void start() {
log.info("尝试启动监控线程...");
synchronized (this) {
if (starting) {
return;
}
starting = true;
}
// 真正启动监控线程...
}
}
2.11 指令重排
高并发情况下指令重排会导致数据处理错误,所以大多需要禁止指令重排
只需要在关键量处加上 volatile
volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
-
对 volatile 变量的写指令后会加入写屏障(对屏障前代码同步内存)
-
对 volatile 变量的读指令前会加入读屏障(对屏障后代码同步内存)
2.11.1 如何保证可见性
2.11.2 如何保证有序性
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
还是那句话,不能解决指令交错:
写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去
而有序性的保证也只是保证了本线程内相关代码不被重排序
2.12 Happens-before
happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见
- 线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见
- 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
- 线程 start 前对变量的写,对该线程开始后对该变量的读可见
- 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束)
- 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过t2.interrupted 或 t2.isInterrupted)
- 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
- 具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z ,配合 volatile 的防指令重排
3 无锁并发
3.1 CAS
线程数少于CPU核心数时合理
其实 CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性。
在多核状态下,某个核执行到带 lock 的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的。
CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果
结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
- CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再
重试呗。
- synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想
改,我改完了解开锁,你们才有机会。
- CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思
- 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
- 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响
3.2 原子整数系列
3.3 原子引用
3.4 ABA问题
AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如: A -> B -> A -> C ,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次。但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了AtomicMarkableReference
3.5 原子数组
3.6 原子更新器
3.7 原子累加器
AtomicIntegerFieldUpdater fieldUpdater = AtomicIntegerFieldUpdater.newUpdater(Test5.class, "field");
必须要volatile修饰
3.8 Unsafe
获取Unsafe
Field unsafe = Unsafe.class.getDeclaredField("theUnsafe");
unsafe.setAccessible(true);
Unsafe o = (Unsafe) unsafe.get(null);
只能通过反射获取
4 不可变
4.1 享元模式
1. 简介
定义 英文名称:Flyweight pattern. 当需要重用数量有限的同一类对象时
wikipedia: A flflyweight is an object that minimizes memory usage by sharing as much data as
possible with other similar objects
出自 “Gang of Four” design patterns
归类 Structual patterns
注意:
-
Byte, Short, Long 缓存的范围都是 -128~127
-
Character 缓存的范围是 0~127
-
Integer的默认范围是 -128~127
- 最小值不能变
- 但最大值可以通过调整虚拟机参数
-Djava.lang.Integer.IntegerCache.high
来改变
-
Boolean 缓存了 TRUE 和 FALSE
5 线程池
5.1 自定义线程池
5.2 线程池
6 CompleteFuture
6.1 创建异步任务
6.1.1 supplyAsync
//使用默认内置线程池ForkJoinPool.commonPool(),根据supplier构建执行任务
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
//自定义线程,根据supplier构建执行任务
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)
6.1.2 runAsync
//使用默认内置线程池ForkJoinPool.commonPool(),根据runnable构建执行任务
public static CompletableFuture<Void> runAsync(Runnable runnable)
//自定义线程,根据runnable构建执行任务
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
6.2 异步任务回调
6.2.1 thenRun/thenRunAsync
CompletableFuture<Void> future = CompletableFuture.runAsync( () -> System.out.println("程序员沈自在") );
future
.thenRun(() -> System.out.println("非常帅"))
.thenRunAsync(() -> System.out.println("上面这个人说的都是真的"));
System.out.println("123");
区别
如果你执行第一个任务的时候,传入了一个自定义线程池:
- 调用thenRun方法执行第二个任务时,则第二个任务和第一个任务是共用同一个线程池。
- 调用thenRunAsync执行第二个任务时,则第一个任务使用的是你自己传入的线程池,第二个任务使用的是ForkJoin线程池
6.2.2 thenAccept/thenAcceptAsync
6.2.3 thenApply/thenApplyAsync
6.2.4 exceptionally
CompletableFuture的exceptionally方法表示,某个任务执行异常时,执行的回调方法;并且有抛出异常作为参数,传递到回调方法。
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
System.out.println("进入异步方法");
String a = null;
a.length();
return "沈自在";
});
CompletableFuture<String> exceptionally = future.exceptionally((e) -> {
System.out.println("异常是: " + e.getMessage());
return "出现异常了";
});
System.out.println("执行结果是" + exceptionally.get());
6.2.5 whenComplete
共用一个线程
public class FutureWhenTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<String> orgFuture = CompletableFuture.supplyAsync(
()->{
System.out.println("当前线程名称:" + Thread.currentThread().getName());
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "捡田螺的小男孩";
}
);
CompletableFuture<String> rstFuture = orgFuture.whenComplete((a, throwable) -> {
System.out.println("当前线程名称:" + Thread.currentThread().getName());
System.out.println("上个任务执行完啦,还把" + a + "传过来");
if ("捡田螺的小男孩".equals(a)) {
System.out.println("666");
}
System.out.println("233333");
});
System.out.println(rstFuture.get());
}
}
//输出
当前线程名称:ForkJoinPool.commonPool-worker-1
当前线程名称:ForkJoinPool.commonPool-worker-1
上个任务执行完啦,还把捡田螺的小男孩传过来
666
233333
捡田螺的小男孩
6.2.6 handle
public class FutureHandlerTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<String> orgFuture = CompletableFuture.supplyAsync(
()->{
System.out.println("当前线程名称:" + Thread.currentThread().getName());
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "捡田螺的小男孩";
}
);
CompletableFuture<String> rstFuture = orgFuture.handle((a, throwable) -> {
System.out.println("上个任务执行完啦,还把" + a + "传过来");
if ("捡田螺的小男孩".equals(a)) {
System.out.println("666");
return "关注了";
}
System.out.println("233333");
return null;
});
System.out.println(rstFuture.get());
}
}
//输出
当前线程名称:ForkJoinPool.commonPool-worker-1
上个任务执行完啦,还把捡田螺的小男孩传过来
666
关注了
6.3 多个任务组合处理
6.3.1 AND
thenCombine / thenAcceptBoth / runAfterBoth都表示:将两个CompletableFuture组合起来,只有这两个都正常执行完了,才会执行某个任务。
区别在于:
- thenCombine:会将两个任务的执行结果作为方法入参,传递到指定方法中,且有返回值
- thenAcceptBoth: 会将两个任务的执行结果作为方法入参,传递到指定方法中,且无返回值
- runAfterBoth 不会把执行结果当做方法入参,且没有返回值。
CompletableFuture<String> first = CompletableFuture.supplyAsync(() -> "b");
CompletableFuture<String> second = CompletableFuture.supplyAsync(() -> "a");
CompletableFuture<String> future = second
.thenCombine(first, (x, y) -> {
System.out.println(x);
System.out.println(y);
return "组合了";
});
System.out.println(future.get());
thenAcceptBoth
CompletableFuture<String> first = CompletableFuture.supplyAsync(() -> "b");
CompletableFuture<String> second = CompletableFuture.supplyAsync(() -> "a");
CompletableFuture<Void> future = second
.thenAcceptBoth(first, (x, y) -> {
System.out.println(x);
System.out.println(y);
});
System.out.println(future.get());
runAfterBoth
CompletableFuture<String> first = CompletableFuture.supplyAsync(() -> "b");
CompletableFuture<String> second = CompletableFuture.supplyAsync(() -> "a");
CompletableFuture<Void> future = second
.runAfterBoth(first, () -> {
try {
System.out.println(first.get());
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
});
System.out.println(future.get());
6.3.2 OR
applyToEither / acceptEither / runAfterEither 都表示:将两个CompletableFuture组合起来,只要其中一个执行完了,就会执行某个任务。区别在于:
- applyToEither:会将已经执行完成的任务,作为方法入参,传递到指定方法中,且有返回值
- acceptEither: 会将已经执行完成的任务,作为方法入参,传递到指定方法中,且无返回值
- runAfterEither: 不会把执行结果当做方法入参,且没有返回值。
6.3.3 AllOf
所有任务都执行完成后,才执行 allOf返回的CompletableFuture。如果任意一个任务异常,allOf的CompletableFuture,执行get方法,会抛出异常
public class allOfFutureTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Void> a = CompletableFuture.runAsync(()->{
System.out.println("我执行完了");
});
CompletableFuture<Void> b = CompletableFuture.runAsync(() -> {
System.out.println("我也执行完了");
});
CompletableFuture<Void> allOfFuture = CompletableFuture.allOf(a, b).whenComplete((m,k)->{
System.out.println("finish");
});
}
}
//输出
我执行完了
我也执行完了
finish
6.3.4 AnyOf
任意一个任务执行完,就执行anyOf返回的CompletableFuture。如果执行的任务异常,anyOf的CompletableFuture,执行get方法,会抛出异常
public class AnyOfFutureTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Void> a = CompletableFuture.runAsync(()->{
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我执行完了");
});
CompletableFuture<Void> b = CompletableFuture.runAsync(() -> {
System.out.println("我也执行完了");
});
CompletableFuture<Object> anyOfFuture = CompletableFuture.anyOf(a, b).whenComplete((m,k)->{
System.out.println("finish");
// return "捡田螺的小男孩";
});
anyOfFuture.join();
}
}
//输出
我也执行完了
finish
6.3.5 thenCompose
thenCompose方法会在某个任务执行完成后,将该任务的执行结果,作为方法入参,去执行指定的方法。该方法会返回一个新的CompletableFuture实例
- 如果该CompletableFuture实例的result不为null,则返回一个基于该result新的CompletableFuture实例;
- 如果该CompletableFuture实例为null,然后就执行这个新任务
public class ThenComposeTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<String> f = CompletableFuture.completedFuture("第一个任务");
//第二个异步任务
ExecutorService executor = Executors.newSingleThreadExecutor();
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> "第二个任务", executor)
.thenComposeAsync(data -> {
System.out.println(data); return f; //使用第一个任务作为返回
}, executor);
System.out.println(future.join());
executor.shutdown();
}
}
//输出
第二个任务
第一个任务