小白问一个 Java 线程 jmm 的问题

2020-03-06 22:15:08 +08:00
 liliumss

小弟正在学 java jmm 知识,做了个实验代码和执行结果如下,启动了 2 个线程来看共享的 count 变量是否被线程共享,按照 jmm 理论 线程应该会加载公共变量到自己的线程,互相不影响,咋执行结果线程 2 赋值后,线程 1 就不++了,往指点,谢谢了

public class Test {
    private static Long count = 0L;
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            System.out.println("Thread1 start");
            while (count < 10000L) {
                count++;
                System.out.println("Thread1 count:" + count);
            }
            System.out.println("Thread1 end");
        }).start();

        new Thread(() -> {
            System.out.println("Thread2 start");
            count = 10000L;
            System.out.println("Thread2 count:" + count);
            System.out.println("Thread2 end");
        }).start();
    }
}

执行结果

Thread1 start
Thread1 count:1
Thread2 start
Thread1 count:2
Thread1 end
Thread2 count:10000
Thread2 end

2870 次点击
所在节点    Java
22 条回复
xmh51
2020-03-06 22:38:18 +08:00
你定义了一个静态变量。。
kevincai100
2020-03-06 22:41:31 +08:00
静态变量在方法区,共享内存,被别人改了立即可见的, 应该不是线程里面的副本
fihserman123
2020-03-06 22:51:48 +08:00
静态变量在方法区( JDK8 后是 meta space )中,线程或者说 run 方法的变量存于栈中。而位于 enclosing class 中的静态变量 private static Long count = 0L; 对于 run 方法而言是可见的,换句话说,你代码里的三个 count 存有同一个 int 数值,且相互影响。你需要做的是在俩线程的 run 方法中分别额外定义一个 count 变量。
fihserman123
2020-03-06 22:52:23 +08:00
静态变量貌似去堆中了(逃。
sagaxu
2020-03-06 22:57:02 +08:00
jmm 哪个条款说互不影响了?
liliumss
2020-03-06 22:58:21 +08:00
@fihserman123 但是我顶一个静态的 boolean 值确是 2 线程互不影响的 这是什么原理呢
liliumss
2020-03-06 23:01:51 +08:00
@kevincai100 我测试了下 boolean 值确是可以的 第一个线程会一直卡在循环中
代码如下:
public class Test {
private static Boolean flag = false;

public static void main(String[] args) throws InterruptedException {

new Thread(() -> {
System.out.println("Thread3 start");
while (!flag) {
}
}).start();

Thread.sleep(1000);
new Thread(() -> {
flag = true;
System.out.println("Thread4 end");
}).start();
}
}
liliumss
2020-03-06 23:03:47 +08:00
@fihserman123 用 boolean 值确是可以的 第一个线程一直卡在循环,这是为啥呢
```
public class Test {
private static Boolean flag = false;

public static void main(String[] args) throws InterruptedException {

new Thread(() -> {
System.out.println("Thread3 start");
while (!flag) {
}
}).start();

Thread.sleep(1000);
new Thread(() -> {
flag = true;
System.out.println("Thread4 end");
}).start();
}
}



```
Jooooooooo
2020-03-06 23:31:30 +08:00
@liliumss 没有定义 volatile 导致 thread 3 寄存器的里的值一直是旧的. 由于非 volatile 的, 这里 thread 3 跑的那个 cpu 可以无限期的去使用寄存器缓存里面存放的 count 值.(当然这个行为是不定的, 不同机器上表现也不会一致)

而题目里那种场景, 因为 thread 1 有更新, 等于是和主内存有交互(其实是 L1 cache), 寄存器的值就被更新成最新的了. 一般硬件的 MESI 协议会保证各个 cpu 核上看见的值是一致(大体是这种意思, 更具体的可以搜搜 MESI)
liliumss
2020-03-06 23:49:07 +08:00
@Jooooooooo 谢谢你的回答
意思就是线程 1 的那个 count++ 触发了 MESI 协议与主内存有交互了,而正好线程 2 把 count 值改变了所以线程 1 就直接满足条件推出了
而线程 3 一直没更新,又没使用 volatile 保证可见性,所以即使线程 4 更改了 boolean 值也无法从循环跳出
我理解的对吧,关键就是线程 1 的 count++导致了 2 种不同变量在后面操作的差别
az467
2020-03-07 00:08:30 +08:00
JMM 和 JVM 的内存结构不是一一对应的关系,或者说他们定义的不是一个层面上的事情,
所以不要去用堆栈元空间什么的去分析。

JMM 什么时候刷新缓存到主存,什么时候读取主存,如果不加 volatile,那么都是不一定的。
你多执行几次就会发现执行结果还可能是这样:

Thread1 start
Thread1 count:1
Thread1 count:2
Thread1 count:3
Thread1 count:4
Thread1 count:5
Thread1 count:6
Thread1 count:7
Thread1 count:8
Thread1 count:9
Thread1 count:10
Thread1 count:11
Thread1 count:12
Thread1 count:13
Thread1 count:14
Thread1 count:15
Thread1 count:16
Thread1 count:17
Thread1 count:18
Thread2 start
Thread1 count:19
Thread1 end
Thread2 count:10000
Thread2 end
liliumss
2020-03-07 00:32:56 +08:00
@az467 我本地也是这个结果 我纳闷是根据 jmm 搜第一个 thread 还在在循环 assgin count 的时候 第二个线程结束同步 write 给主存的值为啥影响了第一个线程的 count 这里并没设置 valiate 修饰 而用 boolan 做的 demo (见楼层)确是可以的
sagaxu
2020-03-07 00:50:57 +08:00
两个建议
1. 测试并发时内存模型的代码,尽量用 jcstress 而不是自己构造。
2. 不要调用 System.out.println 这样的方法,你怎么知道这个方法没有起到同步的作用?

事实上,某个 JDK 给这个 println 加了同步语义,两个线程都调用,那就在调用点建立了 happens-before 关系
public void println(boolean x) {
synchronized(this) {
this.print(x);
this.newLine();
}
}
az467
2020-03-07 02:39:38 +08:00
@liliumss JMM 并没有规定对非 volatile 变量的修改对其他线程完全不可见,不存在什么“互不影响”。
我们只能说这是不确定的,在不同的平台上于不同的时间执行会得到多种结果,出现什么情况都不奇怪。

第一个 demo,如果某 CPU 还没有把修改后的值写入 L1cache,或者 CPU 根本不保证缓存一致性,那么修改还是可能不(立即)可见。

第二个 demo,Thread4 会无限循环,是因为 JIT 的神奇优化,你把 JIT 关了程序就可以退出了,而 JIT 并不是必须的,各版本的 JIT 也不尽相同,所以还是可能可见。

java -Xint DemoApplication
Thread3 start
Thread4 end
//然后程序退出

所以这根本不违背 JMM,没有什么好纳闷的,真要纳闷的话,那些凌乱的底层原理可太多了。

如果要深究为何如此,那就与 JMM 无关了。
liliumss
2020-03-07 08:11:19 +08:00
@sagaxu 谢谢 请问使用什么方法替代 system.out.print 呢
liliumss
2020-03-07 08:12:45 +08:00
@az467 谢谢 请问看这方面知识有啥好文档呢
yanyueio
2020-03-07 08:56:22 +08:00
找本靠谱点的 JAVA 语法关键字指北吧,然后再去理解 jvm,jmm,并且一定注意 jdk/jre 实现版本。同楼上,很多优化导致了不确定,具体根据想象以及自己的操作系统功底,具体分析。
lewis89
2020-03-07 10:49:00 +08:00
多线程研究这些变量之间的一致性,说实话真的没必要,加锁就好了 锁的语义就有 invalid cache 从主内存读 然后 invalid cache 写入主内存的意思
lewis89
2020-03-07 10:53:54 +08:00
中间还涉及到一些类似 memory barrier MESI 协议之类的 说实话真的太复杂了,对大部分应用层程序员 你只要了解涉及到多线程数据同步的问题,加锁搞定一切,不加锁一定出事。
sagaxu
2020-03-07 11:55:20 +08:00
@liliumss 不是替代 IO 输出,是改变 IO 输出的时机,观测变量时不要有 IO,也不能调用其他可能有副作用的方法,你把观测结果记录下来,等观测完了再调用 IO 输出。System.out.println 是最常见的多线程测试代码的坑,它不仅速度巨慢还自带加锁。构造不正确同步的多线程测试代码,有很多注意点,并且不是所有处理器架构都能重现,有些 sparc 下才有的问题,在 X86 下面却没有。并发的诡异之处在于,你照做了一定对,不照做未必一定错,有些错要构造重现也没那么容易。

Java(JLS) --> JVM --> 单 CPU --> 多 CPU

除了内存可见性,还有指令重排,CPU 还有乱序执行,每一个层面都有自己的同步方式,同一层的不同实现还不一样,从 Java 用户的角度,JLS 是法则,是我们唯一能依赖的东西。

JMM 是给写 JVM 的人看的,不是给 JVM 用户看的,包括 JLS 都不推荐一般人看。

https://item.jd.com/25578376712.html

这是 JDK 核心开发者们写的<<Java 并发编程实践>>的中文译本,非常值得反复看。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/650554

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX