接上篇 juc–并发编程的核心问题总结②
一、异步回调
1. 什么是异步回调
我们平时最常见的是同步回调,同步回调是会阻塞的,单个的线程需要等待结果的返回才能继续执行。
假设有两个任务A和B,A任务中需要使用B任务计算成果,有两种方法实现:
-
A和B在同一个线程中顺序执行。即先执行B,得到返回结果之后再执行A。
-
A和B并行执行。当A需要B的计算结果时如果B还没有执行完,A可以先做其他的工作,避免阻塞,过一段时间后再询问一次B。
我们可以直接在A中写一个方法对B处理完的结果进行处理,然后B处理完之后调用A这个方法。
2. java实现异步回调
通过Future接口实现
Future类存在于JDK的concurrent包中,主要用途是接收Java的异步线程计算返回的结果。
java.util.concurrent.Future
- 异步执行
- 成功回调
- 失败回调
public class FutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 发送请求 Void:没有返回结果
CompletableFuture<Void> future = CompletableFuture.runAsync(()->{
try {
// 模仿耗时的操作
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"runAsync-》Void");
});
System.out.println(Thread.currentThread().getName()+"线程");
}
}
只有main线程执行了
public class FutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 发送请求 Void:没有返回结果
CompletableFuture<Void> future = CompletableFuture.runAsync(()->{
try {
// 模仿耗时的操作
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"runAsync-》Void");
});
System.out.println(Thread.currentThread().getName()+"线程");
//获取阻塞执行结果
Void result = future.get();
}
}
// 有返回值的异步回调 supplyAsync
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName()+"runAsync-》Integer");
//成功则返回1024
return 1024;
});
System.out.println(future.whenComplete((t, u) -> {
System.out.println("t:" + t + " u:" + u);
}).exceptionally((e) -> {
System.out.println(e.getMessage());
//失败则返回233
return 233;
}).get());
添加一行错误代码:
// 有返回值的异步回调 supplyAsync
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName()+"runAsync-》Integer");
int i = 2/0;
//成功则返回1024
return 1024;
});
System.out.println(future.whenComplete((t, u) -> {
System.out.println("t:" + t + " u:" + u);
}).exceptionally((e) -> {
System.out.println(e.getMessage());
//失败则返回233
return 233;
}).get());
}
二、JMM
JMM-----Java内存模型
在java中,所有实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享。
局部变量、方法定义参数、异常处理器参数不会在线程间共享,它们不会有内存可见性的问题,也不收内存模型的影响。
JMM定义了线程和主内存间的抽象关系:线程之间的共享变量存储在主存中,每个线程都有一个私有的本地内存,本地内存存储了该线程读/写共享变量的副本。本地内存是JMM的一个抽象概念并不实际存在。
JMM的约定:
- 线程解锁前,必须把共享变量立刻刷新回主存。
- 线程加锁前,必须读取主存中的最新值到工作内存中。
- 加锁和解锁是同一把锁。
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)
- lock(锁定)︰作用于主内存的变量,把一个变量标识为线程独占状态。
- unlock(解锁)︰作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
- load(载入)︰作用于工作内存的变量,它把read操作从主存中变量放入工作内存中。
- use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令。
- assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中。
- store (存储)︰作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用。
- write(写入)∶作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
JMM对这八种指令的使用,制定了如下规则:
- 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write。
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存。
- 不允许一个线程将没有assign的数据从工作内存同步回主内存。
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作。
- 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁。
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。
- 对一个变量进行unlock操作之前,必须把此变量同步回主内存。
若线程A修改了共享变量的值,但线程B的本地内存中该变量仍然是旧值,即线程B不能及时可见。怎样让B知道主内存中值发生了变化呢?使用Volatile。
三*、Volatile
Volatile是java虚拟机提供的轻量级的同步机制。
- 保证可见性
- 不保证原子性
- 禁止指令重排
四、CAS详解
什么是CAS?
CAS是Compare and Swap的缩写,就是比较并替换。
CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
假设存在多个线程执行CAS操作并且CAS的步骤很多,有没有可能在判断V和E相同后,正要赋值时,切换了线程,更改了值。造成了数据不一致呢?
答案是否定的,因为CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(999);
//compareAndSet(int expect, int update)
System.out.println(atomicInteger.compareAndSet(999, 666));//true
System.out.println(atomicInteger.get());//666
}
}
ABA问题:
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然为A值,那就能说明它的值没有被改变过吗?不能,因为在这段时间里它的值可能被修改成了一个值B,后来又被修改回来了A,那CAS就会误以为它从来就没有变过。
JUC包为了解决这个问题,提供了一个带有标记的原子引用类AtomicStampedReference,它可以通过控制变量值的版本来保证CAS的正确性。不过目前这个类比较鸡肋。
CAS的缺点:
- ABA问题。
- CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。
- CAS底层是自旋锁,在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。