请教 JDK21、JDK17 在关于《线程调度》的差异 和《子线程何时刷新工作内存》的问题

2 天前
 kandaakihito

在看了 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 中不会。


研究了老半天没搞懂,菜鸡真心求教。

1236 次点击
所在节点    Java
14 条回复
kandaakihito
2 天前
代码:

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();

}

}
kandaakihito
2 天前
补一张代码截图,方便路过的看:
zizon
2 天前
getA -> redis.getA
incr -> redis.incr
i++ -> redis.getA , ++ , redis.setA
sagaxu
2 天前
研究这种 UB 没有任何意义,内存模型解决的是正确性问题,对未做正确保证的执行,行为是未定义的
yearliny
2 天前
问题一:

默认情况下,主线程会等待所有用户线程执行完毕,程序才会终止。主线程和通过 new Thread() 创建的线程默认是用户线程。

而每个线程在自己的条件内运行(% 3 == 0, % 3 == 1, % 3 == 2 ),但由于没有协调机制:

1. 某个线程可能持续运行,而其他线程无法推进 a 的值,使条件永远无法满足。
2. 线程 t1 、t2 和 t3 可能互相等待某个状态,但无法确定谁应该推进 a ,从而导致卡住状态。

问题二:

按你描述的情况,应该是不成立的,我怀疑还是上面的原因导致的,出自于同一原因。
chengyiqun
2 天前
你这逻辑有问题, 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();

}

}
```

这是修改后的代码
kandaakihito
2 天前
@chengyiqun #6 感谢你愿意指点问题所在。

但是,包括原帖 op ,就是刻意在代码中规避所有可见性的操作,研究为什么会卡死。。。

重点在于,为什么会卡死,而不是这么做有没有数据正确性问题。
chengyiqun
2 天前
a++ 是一个复合操作,读取 a 的值、增加值、写回值,这个操作本身不是原子性的(这个你反编译字节码可以看到)
为了保证多线程环境下的准确性, 请务必使用原子变量自增,或者在 incr 方法加上 synchronized 关键字
chengyiqun
2 天前
@kandaakihito #7 线程 1 执行的时候,永远读取到旧值,while (su.getA() <= 100) 这个自旋操作,其实是一个很耗费 CPU 的操作,你要是在循环里加一个 Thread.sleep(1),就不会卡死了
kandaakihito
2 天前
@chengyiqun #9 是的,我之前也试过,在每个线程里面睡一下确实能不卡死。这一点我前面没提到。

之所以前面没提到,是因为我认为:线程每次唤醒的时候,是会从主存刷新数值到缓存的。这么做和直接给变量 a 加 volatile 没啥区别。同理还有 sout 等 synchronized 的操作。

然而,“线程 1 执行的时候,永远读取到旧值” 这句话是有条件的。变量 a 没有 volatile 不代表子线程永远不会去刷新缓存。实际上只要主线程不睡觉或者不获取当前线程名称,程序虽然有数据正确性问题,但是并不会卡死!

<br/>

简单概括:我知道这段代码的变量可见性无法保证,但是我实在是想不通,为什么主线程唤醒会导致子线程不再主动刷新工作区内存?
ccpp132
2 天前
问题一感觉更像等了一会之后 jvm 发现这段程序 cpu 占用高,决定 jit 优化这段代码,结果循环中把 int a 塞到某个寄存器里,不再从内存中读了。纯猜测
chengyiqun
2 天前
@ccpp132 说的不够准确,jvm 不是看 cpu 占用高去 JIT 优化的,而是看代码执行次数。
while (su.getA() <= 100) 这个自旋操作内部没有 sleep ,的执行次数是非常多的,会轻易达到 JIT 优化阈值。
960930marui
2 天前
@ccpp132 这个是正解
sagaxu
2 天前
做多线程内存模型测试时,有几点要特别注意

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 做预热,并且不调用任何可能影响可见性的方法。

研究可见性问题,却搞一堆影响可见性的观测手段,我不明白到底在研究什么。

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

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

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

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

© 2021 V2EX