小弟正在学 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
1
xmh51 2020-03-06 22:38:18 +08:00
你定义了一个静态变量。。
|
2
kevincai100 2020-03-06 22:41:31 +08:00 1
静态变量在方法区,共享内存,被别人改了立即可见的, 应该不是线程里面的副本
|
3
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 变量。
|
4
fihserman123 2020-03-06 22:52:23 +08:00
静态变量貌似去堆中了(逃。
|
5
sagaxu 2020-03-06 22:57:02 +08:00 via Android
jmm 哪个条款说互不影响了?
|
6
liliumss OP @fihserman123 但是我顶一个静态的 boolean 值确是 2 线程互不影响的 这是什么原理呢
|
7
liliumss OP @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(); } } |
8
liliumss OP @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(); } } ``` |
9
Jooooooooo 2020-03-06 23:31:30 +08:00 1
@liliumss 没有定义 volatile 导致 thread 3 寄存器的里的值一直是旧的. 由于非 volatile 的, 这里 thread 3 跑的那个 cpu 可以无限期的去使用寄存器缓存里面存放的 count 值.(当然这个行为是不定的, 不同机器上表现也不会一致)
而题目里那种场景, 因为 thread 1 有更新, 等于是和主内存有交互(其实是 L1 cache), 寄存器的值就被更新成最新的了. 一般硬件的 MESI 协议会保证各个 cpu 核上看见的值是一致(大体是这种意思, 更具体的可以搜搜 MESI) |
10
liliumss OP @Jooooooooo 谢谢你的回答
意思就是线程 1 的那个 count++ 触发了 MESI 协议与主内存有交互了,而正好线程 2 把 count 值改变了所以线程 1 就直接满足条件推出了 而线程 3 一直没更新,又没使用 volatile 保证可见性,所以即使线程 4 更改了 boolean 值也无法从循环跳出 我理解的对吧,关键就是线程 1 的 count++导致了 2 种不同变量在后面操作的差别 |
11
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 |
12
liliumss OP @az467 我本地也是这个结果 我纳闷是根据 jmm 搜第一个 thread 还在在循环 assgin count 的时候 第二个线程结束同步 write 给主存的值为啥影响了第一个线程的 count 这里并没设置 valiate 修饰 而用 boolan 做的 demo (见楼层)确是可以的
|
13
sagaxu 2020-03-07 00:50:57 +08:00 1
两个建议
1. 测试并发时内存模型的代码,尽量用 jcstress 而不是自己构造。 2. 不要调用 System.out.println 这样的方法,你怎么知道这个方法没有起到同步的作用? 事实上,某个 JDK 给这个 println 加了同步语义,两个线程都调用,那就在调用点建立了 happens-before 关系 public void println(boolean x) { synchronized(this) { this.print(x); this.newLine(); } } |
14
az467 2020-03-07 02:39:38 +08:00 1
@liliumss JMM 并没有规定对非 volatile 变量的修改对其他线程完全不可见,不存在什么“互不影响”。
我们只能说这是不确定的,在不同的平台上于不同的时间执行会得到多种结果,出现什么情况都不奇怪。 第一个 demo,如果某 CPU 还没有把修改后的值写入 L1cache,或者 CPU 根本不保证缓存一致性,那么修改还是可能不(立即)可见。 第二个 demo,Thread4 会无限循环,是因为 JIT 的神奇优化,你把 JIT 关了程序就可以退出了,而 JIT 并不是必须的,各版本的 JIT 也不尽相同,所以还是可能可见。 java -Xint DemoApplication Thread3 start Thread4 end //然后程序退出 所以这根本不违背 JMM,没有什么好纳闷的,真要纳闷的话,那些凌乱的底层原理可太多了。 如果要深究为何如此,那就与 JMM 无关了。 |
17
yanyueio 2020-03-07 08:56:22 +08:00 via Android
找本靠谱点的 JAVA 语法关键字指北吧,然后再去理解 jvm,jmm,并且一定注意 jdk/jre 实现版本。同楼上,很多优化导致了不确定,具体根据想象以及自己的操作系统功底,具体分析。
|
18
lewis89 2020-03-07 10:49:00 +08:00 1
多线程研究这些变量之间的一致性,说实话真的没必要,加锁就好了 锁的语义就有 invalid cache 从主内存读 然后 invalid cache 写入主内存的意思
|
19
lewis89 2020-03-07 10:53:54 +08:00
中间还涉及到一些类似 memory barrier MESI 协议之类的 说实话真的太复杂了,对大部分应用层程序员 你只要了解涉及到多线程数据同步的问题,加锁搞定一切,不加锁一定出事。
|
20
sagaxu 2020-03-07 11:55:20 +08:00 1
@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 并发编程实践>>的中文译本,非常值得反复看。 |