JUC并发编程之:简单概述(三)
##本章概述
上一篇文章讲述了Monitor主要关注的是访问共享变量时,保证临界区代码的【原子性】
本篇我们了解下多线程间的【可见性】与多条指令执行时的【有序性】问题
##本章重点JMM:
·可见性:由JVM缓存优化引起的
·有序性:由JVM指令重排优化引起的
一、Java内存模型
·JMM即Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着CPU寄存器、
缓存、硬件内存、CPU指令优化等
(主存:所有线程共享的变量;工作内存:线程私有的变量)
·JMM体现在一下几个方面:
>原子性:保证指令不会受到线程上下文切换的影响
>可见性:保证指令不会受到CPU缓存的影响
>有序性:保证指令不会受到CPU指令并行优化的影响
二、可见性
2.1、可见性
@Slf4j
public class Test01 {
static boolean runFlag = true;
public static void main(String[] args) {
new Thread(()->{
while (runFlag){
//
}
},"t1").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("main 线程修改flag");
runFlag = false;
}
}
如上代码 main线程修改runFlag后 t1线程没有停止
##分析
1、初始状态:t1线程刚开始从主存读取了runFlag的值到自己的工作内存

2、因为t线程频繁地从主存中读取runFlag的值,JIT编译器会将runFlag的值缓存至自己的
工作内存中的高速缓存中,减少对主存runFlag的访问,提高效率

3、1秒之后,main线程修改了runFlag的值,并同步至主存,而t是从自己工作内存中的
高速缓存中读取的runFlag的值,结果永远是旧值

/**
* 解决方案一:将runFlag变量用volatile修饰
*/
@Slf4j
public class Test01 {
//易变的,易挥发的
//volatile修饰后的字段,就不能从高速缓存中读取了(效率上有损失)
volatile static boolean runFlag = true;
public static void main(String[] args) {
new Thread(()->{
while (runFlag){
//
}
},"t1").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("main 线程修改flag");
runFlag = false;
}
}
##volatile 易变关键字
·volatile是用来修饰成员变量和静态成员变量,它可以避免线程从自己的工作缓存中查找
变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存
【volatile只能保证可见性(读)、不能保证原子性】
【synchronized和ReentrantLock才能保证原子性】
【volatile也可以防止发生指令重排,在有序性会写】
/**
* 解决方案二:使用synchronized
*/
@Slf4j
public class Test01 {
static boolean runFlag = true;
final static Object lock = new Object();
public static void main(String[] args) {
new Thread(()->{
while (true){
synchronized(lock){
if(!runFlag){
break;
}
}
}
},"t1").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("main 线程修改flag");
synchronized(lock){
}
}
}
2.2、可见性 vs 原子性
·2.1中体现的实际就是可见性,它保证的是在多个线程之间,一个线程对volatile变量的
修改对另一个线程可见,不能保证原子性,仅用在一个写线程,多个读线程的情况
如:两个线程一个i++一个i--,volatile只能保证看到最新值,但不能解决指令交错的问题
从字节码角度看:
//假设i的初始值为0
getstatic i //t2线程获取静态变量i的值 线程内i=0
getstatic i //t1线程获取静态变量i的值 线程内i=0
iconst_1 //t1线程准备常量1
iadd //t1自增,线程内i=1
putstatic i //t1将修改后的值存入静态变量i 静态变量i=1
iconst_1 //t2线程准备常量1
iadd //t2自减,线程内i=-1
putstatic i //t2将修改后的值存入静态变量i 静态变量i=-1
##注意:
synchronized语句块即可以保证代码块的原子性,也同时保证代码块内变量的可见性,
但缺点是synchronized的monitor是属于重量级操作,性能相对更低
如果在前面示例的死循环中加入System.out.print()会发现即使不加volatile修饰符,
线程t也能正确看到堆run变量的修饰(因为其内部使用了synchronized)
2.3、练习:两阶段终止模式
·两阶段终止模式Two phrase termination:
即在一个线程t1中如何优雅地终止线程t2,这里的优雅指的是给t2一个处理后续的机会
##之前的代码
private Thread monitorThread;
//启动线程
public void start(){
monitorThread = new Thread(()->{
while(true){
Thread currentThread = Thread.currentThread();
if(currentThread.isInterrupted()){
log.debug("t1线程被终止后,处理后续业务");
break;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
//sleep线程被打断后会重置打断标记
log.debug("t1线程 休眠 被异常中断,重置 打断标记");
currentThread.interrupt();
}
log.debug("t1线程正在处理业务");
}
},"t1");
monitorThread.start();
}
//终止线程
public void stop(){
monitorThread.interrupt();
}
之前我们使用的是interrupt打断,和isInterrupt()进行判断
但Thread.sleep被异常中断后我们需要考虑打断标记被重置,容易忘记,因此我们做下优化
private Thread monitorThread;
private volatile boolean stop = false;
//启动线程
public void start(){
monitorThread = new Thread(()->{
while(true){
Thread currentThread = Thread.currentThread();
if(stop){
log.debug("t1线程被终止后,处理后续业务");
break;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1线程正在处理业务");
}
},"t1");
monitorThread.start();
}
//终止线程
public void stop(){
stop = true;
//防止线程正在睡眠且睡眠时间过长,直接从睡眠过程中打断
monitorThread.interrupt();
}
2.4、练习:同步模式之Balking - 犹豫模式
犹豫模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需
在做了,直接结束返回
【保证某个方法只被执行一次】
//判断是否执行过start方法
private boolean starting = false;
//启动线程
public void start(){
synchronized(this){
if(starting){
return;
}
starting = true;
}
monitorThread = new Thread(()->{
while(true){
Thread currentThread = Thread.currentThread();
if(stop){
log.debug("t1线程被终止后,处理后续业务");
break;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1线程正在处理业务");
}
},"t1");
monitorThread.start();
}
三、有序性
·JVM会在不影响正确性的前提下,调整语句的执行顺序,如下:
static int i;
static int j;
//在某个线程内执行如下赋值操作
i = 1;
j = 5;
可以看到,至于是先执行i还是j,对最终结果不会产生影响,所以上面代码真正执行时,既可以
是
i = 1;
j = 5;
也可以是
j = 5;
i = 1;
这种特性称之为【指令重排】,多线程下【指令重排】会影响正确性。
@Slf4j
public class Test02 {
int num = 0;
//volatile boolean ready = false;
boolean ready = false;
public void actor_1(int r){
if(ready){
r = num + num;
log.info("r : {}",r);
}else{
r = 1;
log.info("r : {}",r);
}
}
public void actor_2(){
num = 2;
ready = true;
}
}
正常情况下 actor_2执行后 actor_1 ready=true num=2 r = 4
但如果actor_2发生指令重排
public void actor_2(){
ready = true;
num = 2;
}
ready = true; num还没有赋值,actor_1就执行了,此时r=0
【解决方法 ready使用volatile】
【为什么要在ready上增加volatile,在num上可不可以?】
【不可以,volatile可以让ready"之前"的代码不发生重排】
3.1、volatile原理
·volatile的底层实现原理是内存屏障,Memory Barrier或Memory Fence
> 对volatile变量的写指令后会加入"写屏障"
> 堆volatile变量的读指令前会加入"读屏障"
3.1.1、volatile如何保证可见性
·写屏障sfence,保证在该【屏障之前】的,对共享变量的改动,都同步到主存中
public void actor_2(int r){
num=2;
ready=true;//ready是volatile赋值带写屏障
//写屏障---之前的代码会都同步到主存中
}
·读屏障lfence保证在该【屏障之后】,对共享变量的读取,加载的是主存中更新数据
public void actor_1(int r){
//读屏障---之后的代码读的是主存中的数据
//ready是volatile读取值带读屏障
if(read){
r = num + num;
}else{
r = 1;
}
}
3.1.2、volatile如何保证有序性
·写屏障会保证指令重排时,不会将【写屏障之前】的代码排在 "写屏障" 之后
public void actor_2(int r){
num = 2;
ready = true; //ready是volatile赋值带写屏障
//写屏障---防止之前的代码指令重排写到写屏障之后
}
·读屏障会保证指令重排时,不会将【读屏障之后】的代码排在 "读屏障" 之前
public void actor_1(int r){
//读屏障---防止之后的代码不会被重排序排到读屏障之前
//ready是volatile读取值带读屏障
if(read){
r = num + num;
}else{
r = 1;
}
}
3.2、dcl问题
·dcl即double checked locking
以单例-懒汉式为例:
public final class Singleton{
private Singleton(){}
private static Singleton INSTANCE = NULL;
public static synchronized Singleton getInstance(){
if(INSTANCE==NULL){
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
##以上实现特点:
·懒惰实例化
·首次使用getInstance()才使用synchronized加锁,后续使用无需加锁
·有隐含的关键一点:第一个if使用了INSTANCE变量,是在同步块之外
##完善修改:
public final class Singleton{
private Singleton(){}
private static Singleton INSTANCE = NULL;
public static Singleton getInstance(){
if(INSTANCE==NULL){
//首次访问会同步,而之后的使用没有synchronized
synchronized(Singleton.class){
if(INSTANCE==NULL){
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
##上述代码字节码如下:
0: getstatic #2 //获取静态变量
3: innonnull 37 //判断是否为null 是的话 跳转至37行
6: ldc #3 //(加锁)获得类对象
8: dup //复制类对象的引用指针
9: astore_0 //存储一份,为了以后解锁用
10: monitorenter //进入同步代码块
11: getstatic #2
14: ifnonnull 27
17: new #3 //创建对象,将对象引用入栈//new Singleton()
20: dup //复制一份对象引用 //引用地址
21: invokespecial #4 //通过对象引用,调用构造方法
24: putstatic #2 //将引用赋值给静态变量//static INSTANCE
27: aload_0
28: monitorexit //跳出同步代码块
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2
40: areturn
17~24行的代码 JVM可能会指令重排优化为:先执行24赋值,再执行21调用构造方法
可能会出现 t1线程执行到了24行赋值后,t2线程执行3行代码判断,然后返回INSTANCE使用,
但这时这个INSTANCE的21行构造方法还没有执行
##解决方案:
private static volatile Singleton INSTANCE = NULL;
3.3、happens-before规则
·happens-before规定了堆共享变量的写操作对其他线程的读操作可见,它是可见性与有序性
的一套规则总结。
##线程解锁m之前对变量的写,对于接下来对m加锁的其他线程对该变量的读可见
static int x;
static Object m = new Object();
new Thread(()->{
synchronized(m){
x=10;
}
},"t1").start();
new Thread(()->{
synchronized(m){
System.out.println(x);
}
},"t2").start();
/**线程对volatile变量的写,堆接下来其他线程堆该变量的读可见**/
volatile static int x;
new Thread(()->{
x=10;
},"t1").start();
new Thread(()->{
System.out.println(x);
},"t2").start();
/**线程start前对变量的写,对该线程开始后对变量的读可见**/
static int x;
x = 10;
new Thread(()->{
System.out.println(x);
},"t2");
/**线程结束前对变量的写,对其他线程得知他结束后的读可见**/
/**(比如其他线程调用t1.isAlive()或t1.join()等待它结束)**/
static int x;
new Thread(()->{
x=10;
},"t1");
t1.staty();
t1.join();
System.out.println(x);
/**线程t1打断t2(interrupt)前对变量的写,对于其他线程得知t2被打断后对变量的**/
/**读可见(通过t2.interrupted或t2.isInterrupted)**/
static int x;
public static void main(String[] args){
Thread t2 = new Thread(()->{
while(true){
if(Thread.currentThread().isInterrupted()){
System.out.println(x);
break;
}
}
},"t2");
t2.start();
new Thread(()->{
sleep(1);
x=10;
t2.interrupt();
},"t1").start();
while(!t2.isInterrupted()){
Thread.yield();
}
System.out.println(x);
}
/**对变量默认值(0,false,null)的写,对其他线程对该变量的读可见**/
/**具有传递性,如果x hb->y 并且 y hb->z那么 x hb->z,配合volatile的防指令重排**/
volatile static int x;
static int y;
new Thread(()->{
y = 10;
x = 20;
},"t1").start();
new Thread(()->{
System.out.println(x);
},"t2").start();