如果有兴趣了解更多相关内容,欢迎来我的个人网站看看:耶瞳空间
JMM即Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。JMM体现在以下几个方面:
可以先看下面一段代码:
public class Demo {static boolean run = true;public static void main(String[] args) {new Thread(() -> {while(run) {}System.out.println("停止循环");}).start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("停止t");run = false;}
}
可以看到,即使主线程将run改为了false,子线程还是没有停止循环。

子线程刚开始的时候从主存读取了run的值到工作内存:

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

1秒之后,main线程修改了run的值,并同步至主存,而子线程是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值。

我们可以用volatile(易变关键字)解决问题,它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存。不过这种方式也降低了程序的运行效率。

除此之外,还可以用synchronized解决该问题:
public class Demo {static boolean run = true;// 锁对象final static Object lock = new Object();public static void main(String[] args) {new Thread(() -> {while(true) {synchronized (lock) {if (!run) break;}}System.out.println("停止循环");}).start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("停止t");synchronized (lock) {run = false;}}
}

synchronized实现可见性的原理:
synchronized与volatile的区别:
JVM会在不影响正确性的前提下,根据情况调整语句的执行顺序。
先看下面的代码,对i和j赋值,先对i赋值再对j赋值其实跟先对j赋值再对i赋值没有区别,所以在真正执行的时候,这两个顺序都有可能。这种特性称之为指令重排。
static int i;
static int j;// 在某个线程内执行如下赋值操作
i = ...;
j = ...;
指令重排存在的意义,其实说到底都是源于对性能的优化,CPU运行效率相比缓存、内存、硬盘IO之间效率有着指数级的差别,CPU作为系统的宝贵资源,那么如何更好的优化和利用这个资源就能提升整个计算机系统的性能。其实指令重排序就是一种来源于生活的优化思想,比如做菜,一般会把熟得最慢的菜放到最开始(比如煲汤),因为在等待这些菜熟的过程中(IO等待)我们(CPU)还可以做其它事情,这就是一种时间上的优化。在计算机领域也是一样,它也会根据指令的类别做一些优化,目的就是把CPU的资源利用起来,这样就能就能提升整个计计算机的效率。
三种重排序场景
指令重排序的原则(as-if-serial语义):编译器和处理指令也并非什么场景都会进行指令重排序的优化,而是会遵循一定的原则,只有在它们认为重排序后不会对程序结果产生影响的时候才会进行重排序的优化,如果重排序会改变程序的结果,那这样的性能优化显然是没有意义的。而遵守as-if-serial语义规则就是重排序的一个原则,as-if-serial 的意思是说,可以允许编译器和处理器进行重排序,但是有一个条件,就是不管怎么重排序都不能改变单线程执行程序的结果。
在复杂的多线程环境下,编译器和处理器是根本无法通过语义分析来知道代码指令的依赖关系的,所以这个问题只有写代码的人才知道,这个时候编写代码的人就需要通过一种方式显示的告诉编译器和处理器哪些地方是存在逻辑依赖的,这些地方不能进行重排序。所以在编译器层面和CPU层面都提供了一套内存屏障来禁止重排序的指令,编码人员需要识别存在数据依赖的地方加上一个内存屏障指令,那么此时计算机将不会对其进行指令优化。
不过因为不同的CPU架构和操作系统都有各自对应的内存屏障指令,为了简化开发人员的工作,避免开发人员需要去了解各种不同的底层的系统原理,所以在JAVA里面封装了一套规范,把这些复杂的指令操作与开发人员隔离开来,这套规范就是我们常说的Java内存模型(JMM),JMM定义了几个happens before原则来指导并发程序编写的正确性。程序员可以通过Volatile、synchronized、final几个关键字告诉编译器和处理器哪些地方是不允许进行重排序的。
内存屏障有两个作用:
内存屏障分为两种,读屏障(Load Barrier)和写屏障(Store Barrier):
java的内存屏障通常所谓的四种即LoadLoad,StoreStore,LoadStore,StoreLoad实际上也是上述两种的组合,完成一系列的屏障和数据同步功能:
volatile的底层实现原理是内存屏障,Memory Barrier(Memory Fence),其内存屏障策略非常严格保守,非常悲观且毫无安全感的心态:
写屏障(Store Barrier):保证在该屏障之前的,对共享变量的改动,都同步到主存当中
volatile static boolean ready = false;public void actor2(I_Result r) {num = 2;ready = true; // ready是volatile赋值带写屏障// 写屏障
}
读屏障(Load Barrier):保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
volatile static boolean run = false;public void actor1(I_Result r) {// 读屏障// ready是volatile读取值带读屏障if (ready) {r.r1 = num + num;} else {r.r1 = 1;}
}

由于内存屏障的作用,避免了volatile变量和其它指令重排序、线程之间实现了通信,使得volatile表现出了锁的特性。
volatile 性能:volatile的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
happens-before规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下happens-before规则,JMM并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见。
线程解锁m之前对变量的写,对于接下来对m加锁的其它线程对该变量的读可见:
static int x;static Object m = new Object();public static void main(String[] args) {new Thread(() -> {synchronized(m) {x = 10;}}, "t1").start();new Thread(() -> {synchronized(m) {System.out.println(x);}}, "t2").start();}
线程对volatile变量的写,对接下来其它线程对该变量的读可见:
volatile static int x;public static void main(String[] args) {new Thread(() -> {x = 10;}, "t1").start();new Thread(() -> {System.out.println(x);}, "t2").start();}
线程start前对变量的写,对该线程开始后对该变量的读可见:
static int x;public static void main(String[] args) {x = 10;new Thread(() -> {System.out.println(x);}, "t2").start();}
线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用t1.isAlive()或t1.join()等待它结束):
static int x;public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {x = 10;});t.start();t.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(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}x = 10;t2.interrupt();}, "t1").start();while (!t2.isInterrupted()) {Thread.yield();}System.out.println(x);}
对变量默认值(0,false,null)的写,对其它线程对该变量的读可见:
volatile static int x;static int y;public static void main(String[] args) {new Thread(() -> {x = 20;y = 10;}, "t1").start();new Thread(() -> {// x=20对t2可见,同时y=10也对t2可见System.out.println(x);System.out.println(y);}, "t2").start();}
上一篇:23.03.05 《CLR via C#》 笔记 1
下一篇:谈谈软件的持续交付