九江网站设计公司表白网页制作免费网站制作
Java 内存模型
JMM(java memory model )它从java层面定义了主存,工作内存抽象概念。底层对应着CPU寄存器,缓存,硬件内存,CPU指令优化等。JMM体现如下方面
- 原子性 :保证指令不会受到线程上下文切换的影响
- 可见性 :保证指令不会受到CPU缓存的影响
- 有序性 :保证指令不会受到CPU指令并优化的影响
原子性
定义:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。原子性就像数据库里面的事务一样,他们是一个团队,同生共死。
原子操作的描述:多个线程执行一个操作时,其中任何一个线程要么完全执行完此操作,要么没有执行此操作的任何步骤,那么这个操作就是原子的。也就是原子类操作是不能被线程调度机制中断的操作。
可见性
定义:可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
public class Test1 {static boolean run = true;public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while(run){// ....
// System.out.println(2323); 如果加上这个代码就会停下来}});t.start();utils.sleep(1);System.out.println(3434); run = false; // 线程t不会如预想的停下来}
}
以上案例main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止,陷入了无限循环
分析
初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。
因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中, 减少对主存中 run 的访问,提高效率
1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量 的值,结果永远是旧值
解决方案
-
volatile(易变关键字)可以用来修饰成员变量和静态成员变量,禁止线程从自己的魂缓存中查找变量的值,必须到主存中获取它的值。(也就是直接从主存中取值)
-
然而使用synchronized也有同样的效果,在java内存模型中,synchronized规定,线程在加锁的时候,需要先清空工作内存,并在主存中拷贝最新变量的副本到工作内存,在每次执行代码之后将最新的共享变量的值同步刷新到主内存中,释放互斥锁
总结
-
前面例子所讲的就是可见性,它保证的是多个线程之间,一个线程对应volatille变量的修改对于另一个线程可见,不能保证原子性,仅用一个写线程,多核线程读的情况
-
synchronized既可以保证代码的原子性,也可以保证代码内变量的可见性
-
缺点就是synchronized是重量级锁,性能较低。
-
而 System.out.println() 也能保证共享变量的统一,是因为它的底层也使用了synchronized 关键字
public void println(String x) {//使用了synchronized关键字synchronized (this) {print(x);newLine();}}
如何保证可见性
- 写屏障:保证在该屏障之前的,对共享变量的改动,都同步到主存当中
public void actor2(I_Result r) {num = 2;ready = true; // ready是被volatile修饰的 ,赋值带写屏障// 写屏障
}
- 读屏障:保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
public void actor1(I_Result r) {// 读屏障// ready是被volatile修饰的 ,读取值带读屏障if(ready) {r.r1 = num + num;} else {r.r1 = 1;}
}
两阶段终止模式
package com.niecun;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.TimeUnit;public class T1 {public static void main(String[] args) throws InterruptedException {Monitor monitor = new Monitor();monitor.start();Thread.sleep(3);monitor.stop();}
}
@Slf4j(topic = "c.Monitor")
class Monitor{private static Thread monitor;private volatile static boolean flag = false;public void start(){monitor = new Thread(() -> {log.debug("监控开启。。。");while (true){if(flag){log.debug("处理事情。。。");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}log.debug("处理完毕。。。。");break;}log.debug("监控中。。。");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {log.debug("被打断了");}}}, "monitor");monitor.start();}public void stop() {monitor.interrupt();flag = true;}
}
模式之 Balking
- 定义:Balking (犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回。有点类似于单例。
package com.niecun;import lombok.extern.slf4j.Slf4j;
import org.omg.PortableInterceptor.LOCATION_FORWARD;import java.util.concurrent.TimeUnit;public class T2 {public static void main(String[] args) throws InterruptedException {Blaking monitor = new Blaking();monitor.start();monitor.start();Thread.sleep(3);monitor.stop();}
}@Slf4j(topic = "c.Monitor")
class Blaking{private static Thread monitor;private volatile static boolean flag = false;private static boolean isStaring = false;public void start(){monitor = new Thread(() -> {synchronized (this){if (isStaring){log.debug("已被开启");return;}isStaring = true;}log.debug("监控开启。。。");while (true){if(flag){log.debug("处理事情。。。");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}log.debug("处理完毕。。。。");break;}log.debug("监控中。。。");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {log.debug("被打断了");}}}, "monitor");monitor.start();}public void stop() {monitor.interrupt();flag = true;}}
有序性(指令重排)
JVM 会在不影响正确性的前提下,可以调整语句的执行顺序
改变如上程序的先后执行顺序,结果不变(这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性。)
比如:在多线程的情况下去执行如下结果
int num = 0;// volatile 修饰的变量,可以禁用指令重排 volatile boolean ready = false; 可以防止变量之前的代码被重排序
boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {if(ready) {r.r1 = num + num;} else {r.r1 = 1;}
}
// 线程2 执行此方法
public void actor2(I_Result r) {num = 2;ready = true;
}
num = 0
线程1在执行actor2 执行 ready = true,切换到线程1,进入 if 分支,相加为 0,同时在切换会线程1执行num = 2num = 1
线程1在执行到actor2中的 num = 2 线程2先执行if判断直接到else 赋值为1num= 4
正常情况,依次执行
注意:这种现象叫做指令重排,是 JIT 编译器在运行时的一些优化,这个现象需要通过大量测试才能复现,可以使用jcstress工具进行测试。
重排序也需要遵守一定规则
- 重排序操作不会对存在数据依赖关系的操作进行重排序:比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。
- 重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变:比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系,所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。
( 重排序)问题分析
- 问题:重排序在单线程模式下是一定会保证最终结果的正确性,但是在多线程环境下,无法保证其正确性。
- 解决方法:volatile 修饰的变量,可以禁用指令重排,使用synchronized加锁的操作
- 注意:synchronized虽然也可以,但是该锁为重量级锁,极其消耗性能。volatile不能保证原子性
如何保证有序性
- 写屏障:保证指令重排序时,不会将屏障之前的代码排到屏障之后
public void actor2(I_Result r) {num = 2;ready = true; // ready是被volatile修饰的 , 赋值带写屏障// 写屏障
}
- 读屏障:确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
public void actor1(I_Result r) {// 读屏障// ready是被volatile修饰的 ,读取值带读屏障if(ready) {r.r1 = num + num;} else {r.r1 = 1;}
}
分析总结(指令有序性)
-
不能解决指令交错
-
写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证其他线程的读不能跑到他的前面
-
故有序性也只是保证了被线程内部的相关代码不被重排序
happens-before
定义:A happens-before B就是A先行发生于B,定义为HB(A,B)在java内存模型中,happens-before的意思是前一个操作的结果可以被后续操作获取
- 为何需要该模式
JVM会对代码进行编译优化,会出现指令重排序的情况,为了避免编译优化对于编程安全性的影响,需要happens-before定义一些禁止编译优化的场景,保证并发编程结果的正确性。
- happens-before相关规则(详情)
-
程序次序规则:在一个线程内一段代码的执行结果是有序的,就是还会指令重排,但是无论怎么排序,结果都不会变化。
-
管程锁定规则:就是无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现)
-
volatile变量规则:就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。
-
线程启动规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。
-
线程终止规则:在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。也称线程join()规则。
-
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断。
-
传递性规则:这个简单的,就是happens-before原则具有传递性,即hb(A, B) , hb(B, C),那么hb(A, C)。
-
对象终结规则:这个也简单的,就是一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法。
as-if-serial
解决单线程的可见性问题