V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
kandaakihito
V2EX  ›  Java

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

  •  
  •   kandaakihito · 1 天前 · 1189 次点击

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


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

    14 条回复    2025-01-06 20:29:02 +08:00
    kandaakihito
        1
    kandaakihito  
    OP
       1 天前
    代码:

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

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

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

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

    问题二:

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

    }

    }
    ```

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

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

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

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

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

    <br/>

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

    研究可见性问题,却搞一堆影响可见性的观测手段,我不明白到底在研究什么。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1096 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 22:44 · PVG 06:44 · LAX 14:44 · JFK 17:44
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.