Java后端
·2025-11-12学习笔记
java如何解决可见性和有序性?我们都知道了,可见性和有序性的问题就是CPU缓存问题以及编译优化导致的执行重排序问题,那么怎么解决可见性问题和有序性的问题也很明了了,把CPU缓存以及编译优化全禁用了不就行了?确实,禁用确实性,但是我们的性能就糟糕了。所以我们不能盲目的禁用,要按需的禁用。
java自然提供了方法,让我们得以按需禁用,比如说volatile。volatile在C语言中也有,它的原始的意义就是禁用CPU缓存。但当我们秉着volatile禁用CPU缓存的这个认知去学习代码的时候,也会遇到一些困惑,比如说下列代码:
class Example {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
// 这里x会是多少呢?
x=?
}
}
}
线程A执行writer方法,将v的改动写入内存,然后线程b执行reader方法,从内存中获取到v==true,但是在再去读取x的值时,x的值是多少呢?x没有使用volatile声明,它是不是就没有禁用缓存呢?这两个疑问,我先回答第一个,第二个疑问后续会解答的。x的值无非就两种,要么是42,要么是0,这取决于java的版本,如果java在1.5之前,那么即可能是42,也可能是0,如果java在1.5之后呢,就是42了。
为什么?因为java1.5之后对volatile进行了增强,使用了happens-before规则。什么是happens-before规则?简单来说,A happens-before B,意思就是A的操作对于B的操作是可见的。对于我们程序员来说,我们需要知道的happens-before规则,共有六项,下面就来详细讲解,也会去解答上面代码的volatile疑问。
第一项规则:程序的顺序性规则。
这个很好理解。在单线程的前提下,按照程序的执行顺序,前面执行的操作,对于后面执行的操作是可见的。这基本就符合我们编写代码时的直觉了,毕竟在单线程情况下不用考虑那么多的。
第二项规则:volatile规则。
假如一个变量v被volatile声明了 ,那么对变量A的操作就会禁用内存,那么就会实现线程A对v的操作 happens-before 线程B对v的操作,就好像上述代码中的一样,线程A调用方法writer,修改了变量v的值,线程B调用reader方法去读取变量v的值时,是可见的,线程B能读取到v最新的值。到这,可能还是有小伙伴疑惑,那这跟上面代码的x有什么关系吗?到底为什么线程B能读到变量x的最新值42?别急,我们看第三项规则。
第三项规则:传递性
简单来说,A happens-before B 且 B happens-before C,那么A happends-before C成立。
直接使用上面代码的例子说明。根据第一项规则,对于线程A来说,它执行writer方法时,先执行操作x=42,然后执行操作v=true,那么线程A执行x=42这一操作对于线程A去执行v=true这一操作时,是可见的,即线程A执行x=42 happens-before 线程A执行v=true.然后线程B执行reader方法,根据第二项规则,v使用volatile声明,所以线程A执行v=true这一操作对于线程B去进行读取v的操作时,是可见的,即 线程A执行v=true happens-before 线程B读取v。最后,根据第三项规则,线程A执行x=42这一操作,线程B也能看到!也就是说线程A执行x=42 happens-before 线程B操作x。现在来回答“x没有使用volatile声明,它是不是就没有禁用缓存呢?”这个疑问,x其实并没有被禁用缓存,那么它怎么被可见的呢?是强制将缓存中x的值刷新到内存里了。
第四项规则:管程中锁的规则
什么是管程?管程是一种同步原语,synchronized是在java对管程的实现,在这个阶段,我们先把管程当成synchronized即可。
这个规则也符合我们的直觉,理解起来应该不难。如下列的例子
synchronized (this) {
// x是共享变量,初始值=10
if (this.x < 12) {
this.x = 12;
}
}
线程A拿到锁之后,对x进行了读取并修改,然后自动释放锁,线程B又拿到了这个锁,然后读取x时,能看到线程A对x的修改,也就是说它能读到x=12。这就是规则4,符合我们的直觉,不难理解。
第五项规则:线程的start规则
主线程要使用start去开启一个子线程去执行任务,在start之前,主线程对变量的操作,子线程是可见的。比如说:
Thread B = new Thread(()->{
// 主线程调用B.start()之前
// 所有对共享变量的修改,此处皆可见
// 此例中,var==77
});
// 此处对共享变量var修改
var = 77;
// 主线程启动子线程
B.start();
第六项规则:线程的join规则
子线程在执行任务时,主线程使用join方法等待子线程的返回,如果join方法成功返回,那么在join方法后,主线程能看到子线程对共享变量的操作,如:
Thread B = new Thread(()->{
//此处可以读取到主线程对var的操作
var == 77;
// 此处对共享变量var修改
var = 66;
});
// 例如此处对共享变量修改,
// 则这个修改结果对线程B可见
var =77;
// 主线程启动子线程
B.start();
B.join()
// 子线程所有对共享变量的修改
// 在主线程调用B.join()之后皆可见
// 此处主线程可以读取到子线程对var的操作
var == 66;
1
0
分享
操作
评论
问答助学
相关内容
0个评论
全部评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
