在看了 https://www.v2ex.com/t/1102734 这篇帖子后,我动手了试了一下。
有两个问题搞不懂,希望得到大佬解答(代码附在留言中):
问题一、 主线程 唤醒 后,会导致子线程不再主动从 主内存 刷新数据到 工作内存?
Thread.sleep(100);
添加这行代码,会导致直接死循环卡住,只有 t0 线程的相关操作得到执行。这个问题原帖 op 也问到了。
然后更神奇的是,当我用 jstack 查看线程状态的时候,发现实际上 t0 、t1 、t2 都处于 runnable 的状态。此时如果尝试用 jprofier 连接 jvm ,会报错相关端口被占用,而代码会马上执行下去。
再有,如果改成 Thread.sleep(1); 运行则不会卡住。经过多次尝试,发现 sleep 特定时长,可以产生输出数字到一半卡死的现象。而且使用 jdk8 和 jdk17 ,这个数字一般是 3 左右,使用 jdk21 则是 28 左右。
看上去就好像,主线程睡醒后,在主线程睡着之前就开 run 的线程不会再去主动同步主内存了一样?
问题二、Thread.currentThread() 会导致 jdk17 及以下版本死循环?
System.out.println(Thread.currentThread().getName() + " : " + su.getA());
这段代码在 jdk17 会死循环,但是在 jdk21 中不会。
研究了老半天没搞懂,菜鸡真心求教。
1
kandaakihito OP 代码:
class Solution { private int a = 0; public void incr() { a++; } public int getA() { return a; } public static void main(String[] args) throws InterruptedException { Solution su = new Solution(); Thread t1 = new Thread(() -> { while (su.getA() <= 100) { if (su.getA() % 3 == 0) { System.out.println(su.getA()); su.incr(); // System.out.println(Thread.currentThread().getName() + " : " + su.getA()); } } }); Thread t2 = new Thread(() -> { while (su.getA() <= 100) { if (su.getA() % 3 == 1) { System.out.println(su.getA()); su.incr(); } } }); Thread t3 = new Thread(() -> { while (su.getA() <= 100) { if (su.getA() % 3 == 2) { System.out.println(su.getA()); su.incr(); } } }); t2.start(); t3.start(); System.out.println("current: " + su.getA()); // Thread.sleep(10); Thread.sleep(100); // System.out.println(Thread.currentThread().getName() + " : " + su.getA()); t1.start(); } } |
2
kandaakihito OP |
3
zizon 1 天前
getA -> redis.getA
incr -> redis.incr i++ -> redis.getA , ++ , redis.setA |
4
sagaxu 1 天前 1
研究这种 UB 没有任何意义,内存模型解决的是正确性问题,对未做正确保证的执行,行为是未定义的
|
5
yearliny 1 天前 1
问题一:
默认情况下,主线程会等待所有用户线程执行完毕,程序才会终止。主线程和通过 new Thread() 创建的线程默认是用户线程。 而每个线程在自己的条件内运行(% 3 == 0, % 3 == 1, % 3 == 2 ),但由于没有协调机制: 1. 某个线程可能持续运行,而其他线程无法推进 a 的值,使条件永远无法满足。 2. 线程 t1 、t2 和 t3 可能互相等待某个状态,但无法确定谁应该推进 a ,从而导致卡住状态。 问题二: 按你描述的情况,应该是不成立的,我怀疑还是上面的原因导致的,出自于同一原因。 |
6
chengyiqun 1 天前 1
你这逻辑有问题, a 这个变量是非原子的, 线程 2 修改了 a 变量后, 对线程 1 来说, 不可见, 所以会陷入死循环, 这涉及到多核处理器的缓存同步问题(如果你是在单核处理器上运行, 就没有问题了)
线程读取变量的时候, 从缓存中读取, 而不同的核心之间除了 L3 缓存是共享的, 其他缓存都是不共享的. 你可以加一个内存屏障 private volatile int a = 0; volatile 让每次读取变量 a 的值的时候总是从内存中读取 不过, 这还不是原子的, 最好使用 AtomitInt 来定义 a 变量 ``` public class Solution { private final AtomicInteger a = new AtomicInteger(0); public void incr() { a.incrementAndGet(); } public int getA() { return a.get(); } public static void main(String[] args) throws InterruptedException { Solution su = new Solution (); Thread t1 = new Thread(() -> { while (su.getA() <= 100) { System.out.println(Thread.currentThread().getName() + " : " + su.getA()); if (su.getA() % 3 == 0) { System.out.println(su.getA()); su.incr(); } } }); Thread t2 = new Thread(() -> { while (su.getA() <= 100) { if (su.getA() % 3 == 1) { System.out.println(su.getA()); su.incr(); } } }); Thread t3 = new Thread(() -> { while (su.getA() <= 100) { if (su.getA() % 3 == 2) { System.out.println(su.getA()); su.incr(); } } }); t2.start(); t3.start(); System.out.println("current: " + su.getA()); // Thread.sleep(10); Thread.sleep(100); // System.out.println(Thread.currentThread().getName() + " : " + su.getA()); t1.start(); } } ``` 这是修改后的代码 |
7
kandaakihito OP |
8
chengyiqun 1 天前
a++ 是一个复合操作,读取 a 的值、增加值、写回值,这个操作本身不是原子性的(这个你反编译字节码可以看到)
为了保证多线程环境下的准确性, 请务必使用原子变量自增,或者在 incr 方法加上 synchronized 关键字 |
9
chengyiqun 1 天前
@kandaakihito #7 线程 1 执行的时候,永远读取到旧值,while (su.getA() <= 100) 这个自旋操作,其实是一个很耗费 CPU 的操作,你要是在循环里加一个 Thread.sleep(1),就不会卡死了
|
10
kandaakihito OP @chengyiqun #9 是的,我之前也试过,在每个线程里面睡一下确实能不卡死。这一点我前面没提到。
之所以前面没提到,是因为我认为:线程每次唤醒的时候,是会从主存刷新数值到缓存的。这么做和直接给变量 a 加 volatile 没啥区别。同理还有 sout 等 synchronized 的操作。 然而,“线程 1 执行的时候,永远读取到旧值” 这句话是有条件的。变量 a 没有 volatile 不代表子线程永远不会去刷新缓存。实际上只要主线程不睡觉或者不获取当前线程名称,程序虽然有数据正确性问题,但是并不会卡死! <br/> 简单概括:我知道这段代码的变量可见性无法保证,但是我实在是想不通,为什么主线程唤醒会导致子线程不再主动刷新工作区内存? |
11
ccpp132 1 天前 1
问题一感觉更像等了一会之后 jvm 发现这段程序 cpu 占用高,决定 jit 优化这段代码,结果循环中把 int a 塞到某个寄存器里,不再从内存中读了。纯猜测
|
12
chengyiqun 1 天前 1
@ccpp132 说的不够准确,jvm 不是看 cpu 占用高去 JIT 优化的,而是看代码执行次数。
while (su.getA() <= 100) 这个自旋操作内部没有 sleep ,的执行次数是非常多的,会轻易达到 JIT 优化阈值。 |
13
960930marui 1 天前
@ccpp132 这个是正解
|
14
sagaxu 1 天前 1
做多线程内存模型测试时,有几点要特别注意
1. 绝对不要用 System.out.println ,因为其实现内部有锁,输出时锁 System.out ,建立了 happens-before 关系。 2. 同理,绝对不要写 Thread.currentThread().getName(),因为 name 是经过 volatile 修饰的。 3. Thread.yield()也可能会影响内存可见性,因为上下文切换可能导致 CPU cache 被同步。 4. Thread.sleep()底层也涉及到上下文切换,同样不能用于观测内存可见性。 可见性涉及到很多层面, 编译器指令重排,VM 指令重排,CPU 指令重排,JIT 优化,CPU cache 一致性,都可能会影响到可见性,所以正儿八经的测试,都会使用 JMH 做预热,并且不调用任何可能影响可见性的方法。 研究可见性问题,却搞一堆影响可见性的观测手段,我不明白到底在研究什么。 |