JVM Java内存模型(JMM)

news/2024/11/18 8:36:33/

很多人将Java内存结构Java内存模型傻傻分不清,Java内存模型Java memory model(JMM)的意思。简单地说,JMM定义了一套在多线程的环境下读写共享数据(比如成员变量、数组)时,对数据的可见性有序性原子性的规则和保障。所以他跟Java内存结构是没有什么关系。

原子性

问题分析

两个线程对初始值为0的静态变量一个做自增,一个做自检,各做50000次,结果是0吗?答案是:结果不一定是0。

public class Test {static int i = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int j = 0; j < 50000; j ++) {i ++;}});Thread t2 = new Thread(() -> {for (int j = 0; j < 50000; j ++) {i --;}});t1.start();t2.start();t1.join();// join()方法的作用就是让主线程等待子线程执行结束之后再运行主线程。t2.join();System.out.println(i);}
}

运行后就出现各种结果,有时出现负数,有时出现正数,当然有时也会输出为0。这是因为Java中对静态变量的自增、自减并不是原子操作,即多线程时他们会被CPU交错执行。而所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何context switch(切换到另一个线程)。

例如对于i++而言(i为静态变量),实际会产生如下的JVM字节码指令:

getstatic	i	// 获取静态变量i的值
iconst_1		// 准备常量1
iadd			// 加法(局部变量的i++调用的是iinc,直接在局部变量槽上执行,而静态变量是在操作数栈上执行。)
putstatic	i	// 将修改后的值存入静态变量i(在操作数栈加完后再put回静态变量)

而对应i--也是类似:

getstatic	i	// 获取静态变量i的值
iconst_1		// 准备常量1
isub			// 减法
putstatic	i	// 将修改后的值存入静态变量i

而Java的内存模型如下图(图片取自网络黑马,以下同):
图片取自网络黑马教程,以下同
内存模型由两部分组成,一部分叫主内存,一部分叫工作内存。但需要注意的是这里的主内存工作内存不能和堆栈混淆起来,像堆、栈这样的是在Java内存结构上的说法,而这里的主内存工作内存是指JMM里的说法。虽然名称有点相似,但是不要混淆。

i这样的静态变量(换句话说共享的变量信息)他们是放在主内存中的,而线程是在工作内存中的。所以假如要完成上面的四行字节码,他的执行需要在主内存工作内存中需要数据的交换。即getstatic是把i的值从主内存中读到工作内存的线程中,然后在工作内存中完成了加法后,他又得把结果写会主存中去。

如果是在单线程下,执行以上8行代码是顺序执行(不会交错)就没有问题:

getstatic	i	// 线程1-获取静态变量i的值,线程内i=0
iconst_1		// 线程1-准备常量1
iadd			// 线程1-自增 线程内i=1
putstatic	i	// 线程1-将修改后的值存入静态变量i 静态变量i=1
getstatic	i	// 线程1-获取静态变量i的值 线程内i=1
iconst_1		// 线程1-准备常量1
isub			// 线程1-自减,线程内i=0
putstatic	i	// 线程1-将修改后的值存入静态变量i 静态变量i=0

但在多线程下,这8行代码可能交错执行。出现交错的原因是Java的线程模型(乃至整个操作系统的线程模型)是一种抢先式多任务系统,就是线程呢会轮流拿到cpu的使用权,cpu会以时间片为单位,比如在时间片1把使用权交给线程1使用,在时间片2再把时间分给线程2执行,也就是多个线程轮流使用cpu。

比如出现负数的情况(假设i初始值为0,同下):

getstatic	i	// 线程1-获取静态变量i的值,线程内i=0
getstatic	i	// 线程2-获取静态变量i的值 线程内i=0
iconst_1		// 线程1-准备常量1
iadd			// 线程1-自增 线程内i=1
putstatic	i	// 线程1-将修改后的值存入静态变量i 静态变量i=1
iconst_1		// 线程2-准备常量1
isub			// 线程2-自减,线程内i=-1
putstatic	i	// 线程2-将修改后的值存入静态变量i 静态变量i=-1

比如线程1获取到了i的值为0getstatic),但是他恰巧在这个时刻他的时间片用完了,cpu就把他踢出去了,踢出去以后cpu开始执行线程2的代码,线程2的代码执行的还是getstatic,他也获取了静态变量i的值,也是0,因为线程1还没来得及修改。假设之后CPU又切换回了线程1,线程1准备了常量并执行了加法(iconst_1 iadd),然后将相加后的结果写回静态变量(putstatic),所以静态变量变成了1。这时cpu又把时间片分给了线程2,线程2也准备常量1(iconst_1),然后做了减法(isub),但线程2读到的i0,所以减的结果是-1,然后写回静态变量(putstatic)。所以虽然两个线程各进行了加一和减一,但结果却是-1,因为线程2的结果覆盖了线程1加完后的结果。

也可能会出现正数,比如:

getstatic	i	// 线程1-获取静态变量i的值,线程内i=0
getstatic	i	// 线程2-获取静态变量i的值 线程内i=0
iconst_1		// 线程1-准备常量1
iadd			// 线程1-自增 线程内i=1
iconst_1		// 线程2-准备常量1
isub			// 线程2-自减,线程内i=-1
putstatic	i	// 线程2-将修改后的值存入静态变量i 静态变量i=-1
putstatic	i	// 线程1-将修改后的值存入静态变量i 静态变量i=1 

虽然这八个字节码本意是执行一次加法和一次减法,但却出现了正数结果。

以上是在多线程的情况下,由于指令交错而产生的问题的分析。

解决方法

在Java内存模型中,通过synchronized关键字来保证原子性。语法如下:

synchronized(对象) {要作为原子操作代码
}

这样写的话,比如线程1来了,他就会被“对象”加锁,加锁以后,他可以安全的去执行同步代码块儿内的代码。这时如果有线程2过来想执行同步代码块儿内的代码的话,他就执行不了了,他就会等待线程1释放“对象”所加的锁,也就是说线程1把同步块儿内的代码都运行完毕,线程1就会把这个锁释放开,那其他的线程才会有机会去争抢“对象”的锁。即同一时刻,只有一个线程能进入同步代码块儿,这样就保证了同步代码块儿内的这些代码的原子性。

public class Test {static int i = 0;// 定义一个静态Object对象static Object obj = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int j = 0; j < 50000; j ++) {synchronized(obj) {i ++;}}});Thread t2 = new Thread(() -> {for (int j = 0; j < 50000; j ++) {synchronized(obj) {i --;}}});t1.start();t2.start();t1.join();// join()方法的作用就是让主线程等待子线程执行结束之后再运行主线程。t2.join();System.out.println(i);}
}

这样做的话,i++相关的那四个指令就会以一个正体来运行,i--相关指令亦是如此。比如,线程1来了后,他进入同步代码块儿以后(比如i++那里),他就会在obj上加上锁了,如果这时候线程2来要执行i--那边,他就执行不了了,他只能等待i++这个部分执行完毕以后,锁释放开了,那线程2才有机会获得锁去执行i--操作。这样就保障了i++i--相关指令都是以整体来执行的。

可以做一些比喻,你可以把obj想象成一个房间,这个房间只能有1个人进入,synchronized关键字就是你其中一个线程进入房间以后,给他做一个加锁的操作(网友1:上厕所),即进入房间后把这个门给反锁了,那在你解开这个锁之前,其他的线程只能在门外等待(网友1:obj就是钥匙),当第一个线程执行完了以后,他会解开这个锁,从房间内出来,此时房间空了,那其他的线程才有机会进入这个房间,重复刚刚的过程。

在这里插入图片描述
红色的大圈是这个对象(网友1:是obj吗?网友2:obj不是monitor,obj里面有个指向monitor的引用)的monitor区,即每个对象他都有自己的一个monitor区,即监视器monitor是在我们利用了synchronized这种同步关键字以后他才会生效,对没有加同步关键字的对象或方法时,他根本就不会考虑monitor。这个monitor可以再把它划分成3块儿,第一块儿(蓝色)可以叫做ownerowner表示monitor监视器的所有者,同一时刻只能有一个线程成为owner。黄色的一块儿可以把它叫做EntryList,而绿色的叫WaitSet。那比如线程1来了,然后他发现这个monitor中的owner是空的,并没有其他线程所占据,那么此时,线程1就会成为owner。并且他会相当于比如JVM指令中的monitor enter来对这个monitor进行一个锁定。那如果有线程2来了,线程2发现线程1已经成为owner了,并且用monitor enter指令,把monitor锁住了,所以线程2就不能成为owner,但他可以进入EntryList,这是一个排队等候区,他先在这里等待,这个等待专业的叫法是阻塞,也就是线程2会被阻塞住,他不会占用cpu时间,只有当线程1执行完毕了,线程2才有机会成为owner,线程1执行完毕后就会执行虚拟机指令monitor exit,这个指令就会通知EntryList里面这些正在等待的线程“owner已经空出来了,你们可以来争抢了”,此时EntryList中线程2(因为这个例子中就他一个人)就可以去成为新的owner,同样线程2也会执行monitor enter命令,锁住整个对象的monitor区,防止其他线程对他的干扰。这是从专业的角度去解释同步代码块儿内这些个概念。当然,若在EntryList里面有多个线程的话,那等线程1执行完毕后,他们就会去争抢成为owner

可见性

退不出的循环

看下面的一个现象(退不出的循环),main线程对run变量的修改对于t线程不可见,导致了t线程无法停止:

public class Test {static boolean run = true;public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while(run) {// ...}});t.start();Thread.sleep(1000);run = false;// 是为了让t线程退出while循环后停下来,所以主线程里改了该值。}
}

但运行后,可以发现1秒钟后虽然run变成了false,但线程t并不会停下来,所以程序一直在循环中。出现这种情况是因为:
在这里插入图片描述
刚开始初始状态时,t线程主内存读取静态变量run的值为true,读到了自己的工作内存中,读进来以后,他发现runtrue,所以就开始不断的循环,但他每循环一次,他都要到去主内存中取这个静态变量,即相当于很频繁的从主内存中取读取run的值,所以JIT即时编译器他就会认为,循环超了一定的次数,那我就要做一些优化了,优化后如下:
在这里插入图片描述
他就会把这个主存中的run值缓存在工作内存高速缓存里,相当于我们把这个true读进来了,读进来后,你反复反复的用,那我为了提高效率,从等价的Java代码的角度来说,你可以认为他就把他变成了一个临时变量即从static变成了一个局部的变量,那下次你再循环就不用到主存中去找了,你直接到局部变量这儿去找就行(这是为了便于理解,所以描述成了局部变量,实际并不是这样;实际上他是把run放入到了工作内存中的高速缓存里),这主要是为了提高效率。

但是问题来了,1秒睡眠之后,主线程读到了主内存中的run的值,把这个run改成了false,并写回了主存,那么实际上此时主存上的run已经变成了false,但是t线程由于刚才已经做了优化,t还是源源不断的从自己的高速缓存中去读取run的值,那肯定读到的是旧值。这就是一个退不出的循环问题。
在这里插入图片描述

解决方法

解决方法是要引入一个关键字,就是volatile易变关键字)。他可以用来修饰成员变量和静态成员变量,他的作用其实很简单,就是避免线程从自己的工作内存高速缓存中查找变量值,即用volatile修饰变量,每次他都会到主存中查找变量的最新值。即改成如下:

...
volatile static boolean run = true;
...

这时候,运行的话,一秒之后,就程序结束了。因为volatile修饰的变量,他的读取是每次都到主存中去读取的,这样就保证了读取的这个线程他看到的是最新的结果。

volatile体现的是可见性这个特性,他保证的是在多个线程之间,一个线程对volatile变量的修改对另一个线程是可见的,即上面例子中主线程volatile变量写入了falset线程就可以看到他的写入,而不是看到他的旧值。但volatile他仅仅保证的是可见性,但他不能保证原子性,所以他适用的场景是“一个线程写,多个线程读”的情况。

注意
synchronized语句块既可以保证代码块儿的原子性,也同时保证代码块儿内变量的可见性。但缺点是synchronized是属于重量级操作,性能相对更低。而volatile只能保证可见性,不能保证原子性

小实验
如果在前面示例的死循环中加入System.out.println()会发现即使不加volatile修饰符,线程t也能正确看到对run变量的修改了,这是为何?比如:

public class Test {static boolean run = true;public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while(run) {// ...System.out.println(1);}});t.start();Thread.sleep(1000);run = false;// 是为了让t线程退出while循环后停下来,所以主线程里改了该值。}
}

就写了个System.out.println(1),但运行后会发现程序会1秒后结束,这说明t线程读到了run,这是因为底层是synchronized关键字起到的作用,比如println()方法源码如下:

PrintStream.java

public void println(int x) {synchronized(this) {print(x);newLine();}
}

可以看到这里加了synchronized关键字,要对PrintStream.java打印输出流)做一个同步,同步关键字也可以防止当前线程从高速缓存中获取值,即他也是强制让你的当前线程(也就是t)去读取主存中的run值,也就是破坏了JIT优化。因此,只要涉及到了synchronized关键字,他既可以保证可见性也可以保证原子性,而volatile只能保证可见性

有序性

诡异的结果

int num = 0;
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;
}

这里有两个变量,一个是intnum,一个是boolready。现在有两个方法被多个线程并发执行,方法actor1是判断ready是否真,若真就num翻倍,并赋值给I_Result对象r1属性,这里的I_Result可以当做是一个用来保存结果的对象,其中的r1用它存储结果,若ready是假,直接给r1赋值1。而线程2执行的actor2方法是直接给num赋值2,然后给ready赋值true。有的人把结果可能如下分析:

情况1:

  • 线程1先执行,这时ready=false,说以进入else分支结果为1

情况2:

  • 线程2先执行num=2,但没来得及执行ready=true线程1执行,还是进入else分支,结果为1

情况3:

  • 线程2执行到ready=true线程1执行,这回进入if分支,结果为4因为num已经被线程2执行为赋值3了

但是,这两个线程并发执行,还有一种情况,结果还有可能是0。即如下:

情况4:

  • 线程2执行ready=true,切换到线程1,进入if分支,相加为0,再切回线程2执行num=2。(网友1:晕了!网友2:指令重排序。

在Java内存模型中,这种现象称之为“指令重排”,也是JIT编译器在运行时的一些优化,这个现象需要通过大量的线程进行大批量的测试才能复现,所以甚至有些人认为这是假的。

测试需要借助Java并发压测工具jcstress(https://wiki.openjdk.java.net/display/CodeTools/jcstress)来完成这个测试。

1)利用下面命令创建maven骨架项目(项目名jcstress

mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jcstress -DarchetypeArtifactId=jcstress-java-test-archetype 
-DgroupId=org.sample -DartifactId=test -Dversion=1.0

2)编写测试类
ConcurrencyTest.java

@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
@State
public class ConcurrencyTest {int num = 0;boolean ready = false;@Actorpublic void actor1(I_Result r) {if(ready) {r.r1 = num + num;}else {r.r1 = 1;}}@Actorpublic void actor2(I_Result r) {num = 2;ready = true;}
}

测试类的代码和上面提出问题时的代码一样,只是在被测的两个方法上因为它两是将来要通过不同的线程来测试他的并发,所以这两个方法上加了@Actor标签。然后比较重要的是@Outcome注解,@Outcome就是去检查我感兴趣的一些结果,比如我们的结果保存在了I_Result对象的r1属性中,那我们在@Outcome里你就把这个结果进行分类,比如说我的结果是14的话,那我就把他分到一个叫Expect.ACCEPTABLE即可接受的)分类里,desc = "ok"表示这个结果是我意料之中的。还有一种情况就是刚才说‘(我们认为)不可能发生的’结果,即相加的结果是0,这种情况就把他归类到Expect.ACCEPTABLE_INTERESTING即我感兴趣)的结果里,也给了描述desc“!!!!”。这样写完以后,就可以执行压测了。

但他执行压测,稍微复杂一些,我们需要用maven命令来执行他。在项目名称里点击右键->Open in Terminal,然后执行如下:

mvn clean install

先把他清除一下并重新编译,编译之后他就会生成一些jar包,在target目录下可以看到生成了两个jar包:

jcstress-1.0-SNAPSHOT.jar 这是源码的jar包
jcstress.jar 这是压测的入口jar包

然后用Java命令去运行这个jar包:

java -jar target/\jcstress.jar

执行后,从控制台输出信息中可以看到他会进行大量的测试,比如其输出信息中有每秒钟执行1.5*10个7次方这种描述。

测试结束后,控制台中输出的结果内容如下:

...
*** INTERESTING tests // 这里是你感兴趣的结果,也就是刚才的注解,比如他会把出现0这个结果的记录给我们统计出来
...
(JVM args: [-XX: -TieredCompilation]) // 一个是带了JVM参数,关闭了分层编译。
Observed state	Occurrences	Expectation		Interpretation0	1,703		ACCEPTABLE_INTERESTING	!!!!1	47,088,060	ACCEPTABLE		ok4	6,445,628	ACCEPTABLE		ok[OK] test.ConcurrencyTest(JVM args: [])// 还有一个是没带任何JVM参数的。(这两种都出现了ACCEPTABLE_INTERESTING这种结果)Observed state	Occurrences	Expectation		Interpretation0	2,009		ACCEPTABLE_INTERESTING	!!!!1	58,884,709	ACCEPTABLE		ok4	8,091,363	ACCEPTABLE		ok

47,088,060等数字可以看得出总的测试数次非常多,比如47,088,060表示做了四千多万次的方法的并发调用,那么可以看到大部分的情况都出现了结果1,也有相当一部分情况下出现了4,至于0这个结果虽然少,但也出现了一千次以上,没带JVM参数时出现了两千多次,虽然出现0的次数占的比例很少,不是经常出现,但毕竟他也是出现了,这说明指令重排的问题确实存在。

解决方法

解决方法是可以用volatile修饰变量,这个修饰的变量,可以禁用指令重排。刚才的boolean ready = false;改为volatile boolean ready = false,这样的话,比如线程2去执行actor2去往变量readytrue时(由于禁用了指令重排,所以这时已经确定执行完num=2了?),那么线程1执行actor1时从volatile变量去读,那么这个读写操作就不会受到指令重排的影响了,修改maven工程的代码后,重新测试。结果输出内容中可以看到:

...
*** INTERESTING tests0 matching test results.
...

即你感兴趣的结果只有0次匹配,也就是说没有出现结果为0的情况了。

有序性理解

那为何会发生指令重排呢?这就牵扯到Java内存模型中的有序性的理解。比如下面一段代码:

static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...;// 较为耗时的操作
j = ...;

这段代码可能要对ij做赋值操作,但i的赋值可能要做一些计算,所以比较耗时,但j马上就会运算完毕,这种情况下JVM就会对这种指令进行调整,他会认为你这个i的操作比较耗时,那可不可以先把i排后,先把j的操作排在前面,因为它两之间没有任何交叉,至于先执行i还是j,对最终结果不会产生影响,那么他就可能会做一些优化,比如执行顺序是先给i赋值然后给j赋值,或先给j赋值然后给i赋值,即对他指定的顺序做了一个调整。那么在同一个线程内,这个调整是没问题的,不会影响到最终结果的正确性。但是在多线程的情况下,这个指令重排就会产生一些问题,比如上面已经简单说过了。

那么除此以外,在多线程下的指令重排还有一个重要的案例,就是写单例模式时,著名的double-checked locking模式实现的单例模式:

public final class Singleton {private Singleton() {}private static Singleton INSTANCE = null;public static Singleton getInstance() {// 实例没创建,才会进入内部的synchronized代码块儿if (INSTANCE == null) {synchronized (Singleton.class) {// 也许有其他线程已经创建实例,所以再判断一次if (INSTANCE == null) {INSTANCE = new Singleton();}}}return INSTANCE;}
}

这个单例类实现一个懒惰初始化,首先把构造方法设为私有,然后有静态成员变量INSTACNE,刚开始没值,接下来用getInstance方法来获得单例对象,然后先看这个单例有没有创建,没有创建我才去new单例对象,若已经创建了,就直接拿到上次创建好的单例对象并return

但为了实现这个懒惰初始化,也得考虑线程安全问题,若多个线程并发调用getInstance,就有可能造成单例对象被创建多次,有一种方法是在方法上直接加锁,但这样加锁范围太大,每次调用方法都得加锁,实际上对象还没被创建时加锁就行了,所以后续只要对象创建出来了,获取这个对象时是不需要加锁的。所以过程是,先判断单例对象是否为空,是空的话说明他还没被创建,此时再加锁,加锁以后继续判断他是否为空,若仍然为空就创建对象。

为何加锁后还要做一次判断呢,比如场景如下,线程1来了,发现对象没被创建,就进入同步代码块儿,锁住了类对象,然后去执行下面的代码,但如果线程2在同一时刻也来调用getInstance方法,那么第二个线程肯定被挡在了synchronized之外,会等待线程1去执行完里面的代码,线程1把对象创建好了以后,就退出了同步代码块儿,然后锁解开了,所以线程2就进入了同步代码块儿,这是两个线程首次调用getInstance时就会出现这种情况。所以线程2进来以后,若没有里面的第二次判断的话,线程2还得去new对象,这就与单例的含义不符了,所以线程2进来时还要加一个if判断。这样两次检查单例对象是否为空,就是double-checked的名字由来,即双重检查锁

这种方式看起来完美,但却是有问题的,因为它没考虑指令重排的问题,当然指令重排问题仅仅是在多线程环境下才有问题。比如,在多线程环境下,上面的INSTANCE = new Singleton()对应的字节码为:

0: new 			#2	// class com.cnm.Singleton 用new关键字先给Singleton对象分配空间
3: dup				// 上面的执行结果是把对象的引用放入操作数栈,然后操作数栈把这个对象引用复制了一份儿,就相当于栈顶有两个对象的引用
4: invokespecial	#3	// Method "<init>":()V 第一个对象的引用交给了构造方法
7: putstatic		#4	// Field INSTANCE:Lcom/cnm/Signleton;第二个对象的引用交给了putstatic,就是给静态变量赋值了

问题就处在47,这两行代码有可能发生指令重排问题,即他的执行顺序是不固定的,因为你到底是先new构造,还是先给静态变量赋值,那么JVM就会认为他谁先执行谁后执行对结果没啥影响。下面是其中的一种可能,比如有两个线程t1 t2,若按如下时间序列执行:

时间1 t1 线程执行到 INSTANCE = new Singleton();
时间2 t1 线程分配空间,为Singleton对象生成了引用地址(0处)
时间3 t1 线程将引用地址赋值给INSTANCE,这时INSTANCE !=null(7处)
时间4 t2 线程进入getInstance()方法,发现INSTANCE != null(synchronized块外),直接返回INSTANCE
时间5 t1 线程执行Singleton的构造方法(4处)

但是可以想,如果构造方法内的代码比较多,即构造过程比较复杂的话,那么t2线程拿到的INSTANCE是一个不完整的对象实例,因为t1的构造方法还没执行完,其中有的属性赋值好了但有的属性还没来得及赋值,此时,t2线程拿到的就是一个没有经过完全构造完成的单例对象,去使用时就可能出现问题。

这说明双重检查锁可能有这种指令重排问题,但几率非常小。

那如何解决呢?很简单,给INSTANCE变量多加一个volatile修饰符即可。这样可以禁用指令重排,但要注意在JDK5以上的版本的volatile才会真正有效。

happens-before

happens-before 规定了哪些写操作可以对其他线程的读操作可见,他其实就是可见性有序性的一套规则:

【例子1】一个线程对volatile变量的写操作,对接下来其他线程对该变量是读可见的

volatile static int x;new Thread(()->{x = 10;
},"t1").start();new Thread(()->{System.out.println(x);
},"t2").start();

比如有两个线程t1 t2,还有一个volatile修饰的共享变量x,假设t1是先运行的,那他对这个共享变量做了一个写操作,假如t2线程是后运行的,那他要对这个共享变量做一个读操作,那他肯定能读到刚才x的最新的值10。(其实就是一个可见性的体现

【例子2】线程在解锁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();

m是作为一个对象锁,那假设也是t1先执行t2后执行,t1先对m对象加锁,他在同步代码块儿内对x变量进行了写操作,接下来等锁释放开t2线程就可以获取这个锁了,获得锁之后,他对x读操作肯定是读到最新值的。

【例子3】线程start前对变量的写操作,对该线程开始后对该变量的读是可见的

static int x;x = 10;new Thread(()->{System.out.println(x);
},"t2").start();

比如说有共享变量x,那在t2线程start之前,对x进行了赋值,等你线程t2运行了以后,再去读x是肯定能读到最新值。

【例子4】t1线程结束前对变量的写操作,对其他线程得知t1结束后对x的读操作是可见的(比如其他线程调用t1.isAlive()或t1.join()等待他结束)

static int x;Thread t1 = new Thread(()->{x = 10;
},"t1");
t1.start();t1.join();
System.out.println(x);

比如现在有线程t1开始运行,然后在他结束之前给x赋值10,那么主线程调用t1.join()等待t1结束,那等t1结束以后,再去读x肯定是读到t1结束前对他的写操作的最新值。

【例子5】线程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);
}

比如有t2线程,他有个while(true)循环,即不断运行,先不看while里面的if。然后下面有个t1线程t1线程先睡了1秒中,1秒后,他对共享x变量做写操作,赋值10,然后他把t2线程打断t2.interrupt();了,即t2线程在1秒之后被t1给打断了。还有主线程也在不断循环while(!t2.isInterrupted()),看看t2有没有被打断,如果没有被打断,就不断循环。那1秒之后,t1线程t2线程打断了,打断以后,主线程的while条件就不成立了,那他就退出这个循环,主线程再去拿x的值,输出,这时他肯定能拿到x的最新值。因为这个x的写操作是在打断t2前写的,那么等你主线程得知他打断了以后,你再去读x的值,那肯定是能够拿到。(网友1:这个相当于一种范式,告诉你这样可以保证顺序性,理解就好了。)当然,上面的t2里面的if跟主线程里面while的判断条件是类似的,t2里的if判断是为了让t2里面的while循环优雅的退出,即在他t2被打断了以后,他还会继续循环,所谓的打断就是设置一个线程的打断标记,他不会影响这个线程t2的继续运行,那等t2被打断以后,下次再循环进入if判断时,这个条件就成立了,然后就break了,当然他得知自己被打断后,他再去读x的值,肯定是能读到其他线程对在打断前对他的一个变量(x)的写操作。

【例子6】对变量默认值(0,false,null)的写,对其他线程对该变量的读可见。即默认值优先于其他线程对该变量的读。
【例子7】happens-before具有传递性,如果x hb->y,y又 hb->z,那么就会有x hb -> z。

以上happens-before例子中的变量都是指共享的变量,也就是指成员变量或静态变量。以上是对happens-before规则的解读,他其实就是描述了哪些写操作对其他线程的读操作可见的。


http://www.ppmy.cn/news/4167.html

相关文章

傻白入门芯片设计,盘点GPU业界的大佬(十五)

在PC个人电脑时代&#xff0c;英特尔&#xff08;Inter&#xff09;是无可争议的芯片巨头&#xff0c;凭借着X86架构在数据中心CPU中的压倒性地位&#xff0c;一度垄断全球90%的市场份额。然而在人工智能时代&#xff0c;以英伟达&#xff08;NVIDIA&#xff09;为首的GPU、AI芯…

早餐店+饮品+烘焙,如何做多群体早中晚生意?

早餐吃好、午餐吃饱、晚餐少吃&#xff0c;如今早餐店已经不再局限于豆浆油条、鸡蛋面包&#xff0c;同时还有饮品&#xff0c;芳芳珍早鲜奶店是一家早餐店&#xff0c;30个SKU销量很高&#xff0c;一天中有近75%的销售都集中在早餐场景。 这家早餐店是如何做的&#xff1f; 01…

5G基站射频传导测试研究与应用

【摘 要】基站是5G网络中的重要节点,其RF(射频)性能与其网络覆盖范围和服务质量高度相关。详细介绍了5G基站的射频测试标准,并通过对测试项目物理意义和实际影响的分析,给出了射频传导测试方法以及测试环境构建中的注意事项,通过对实际基站的测试及对其结果的分析,指出…

Servlet转发与重定向

✅作者简介&#xff1a;热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏&#xff1a;JAVA开发者…

c拾遗-二级指针、数组内函数

1、c语言中二级指针 用途1&#xff1a;一般被用在多维数组的环境中&#xff0c;一个二级指针可以指向一个二维数组&#xff1b; 用途2&#xff1a;二级指针作为函数参数使用时&#xff0c;可以通过函数修改实参指针值的目的。 对于第二点用途的理解&#xff1a; c语言的函数调…

Socket Websocket 客户端和服务端实现

最近在写一个上位机&#xff0c;用到了Websocket&#xff0c;这里就整理一下&#xff0c;顺便把Socket的东西也整理的了&#xff0c;方便以后查阅。 Socket Websocket 客户端和服务端实现Socket客户端和服务端实现Socket客户端Socket服务端实现效果Websocket 客户端和服务端实现…

一维数组定义遍历

一维数组&#xff0c;是由数字组成的以单纯的排序结构排列的结构单一的数组&#xff0c;是计算机程序中最基本的数组。二维及多维数组可以看作是一维数组的多次叠加产生的。 在程序中可以使用下标变量&#xff0c;即说明这些变量的整体为数组&#xff0c;数组中的每个变量的数…

大数据Kudu(八):Kudu与Impala整合

文章目录 Kudu与Impala整合 一、​​​​​​​Kudu与Impala整合配置