神出鬼没java的println方法,导致的问题

网友给出的 迷幻问题一 没有println方法,主线程无法读变量



public class ThreadTest {
    public static void main(String[] args) {
        Num num = new Num(0);
        new Thread(()->{
            while (true){
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                num.setI(num.getI()+1);
            }
        }).start();

    //问题代码
//        while (true){
//            //System.out.println注释掉 就不能打印到3s了..为什么?
//            //System.out.println(num.getI());
//            if (num.getI()==3) {
//                System.out.println("到3s了!!!!");
//                break;
//            }
//        }

     //解决代码
        while (true){
            synchronized (num){
                if (num.getI()==3) {
                    System.out.println("到3s了!!!!");
                    break;
                }
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }




    private static class Num{
        private  int  i;

        public Num(int i) {
            this.i = i;
        }

        public int getI() {
            return i;
        }

        public void setI(int i) {
            this.i = i;
        }
    }


}

先看下println方法

public void println(String x) {
        if (getClass() == PrintStream.class) {
            writeln(String.valueOf(x));
        } else {
            synchronized (this) {
                print(x);
                newLine();
            }
        }
    }

再说问题:

如果删掉解决代码,实际上num并没有锁,那么为什么在问题代码中不能读到想要的值然后顺利的输出?
第一轮测试,认为是锁的问题,但没有解决。
第二轮测试,认为是锁的持有和释放过程会产生时间差,所以主线程(main)加了线程等待。目标达成了,但还是有疑惑,因为当println() 任意字符串的时候,也是能正常达到目标的。
作为能手撸JVM的大佬,肯定不会就不能这么轻易放过这个问题。
想起CPU的内存和JVM的内存模型的对应关系,哪是不是JVM的工作内存并没有把A线程修改的num变量的值,更新到主内存,或者说B线程没有读到num变量在主内存的最新值呢?
那如何确定JVM是把一个变量是真的把主内存的num拷贝到工作内存呢?
如果JVM拷贝了num变量到A线程和B线程,那拷贝的是变量的副本,还是这个变量的地址呢?
带着这些疑惑重新看了一遍代码,发现num的值是int基本类型,注意,他是值。。而不是Integer类型,所以,JVM是不是把int值直接拷贝到了A B线程,然后在工作内存中修改后,导致 A B 线程不能及时更新数据?

我把num值改成500(让num的值成为引用类型),然后在主线程main的while循环中这样写

if (num.getI()==520) {
                System.out.println("到3s了!!!!");
                break;
            }

结果目标还是没有达成,由此可见,就算是引用类型的数据,A B线程拿到的也只是这个变量的地址,根据我写JVM的经验,这种操作,拿到变量地址,再工作内存中计算好结果,才会写主内存,而不是拿着地址,直接修改值。
但网友又问了,如果不是直接修改?那为什么打断点的时候就直接能看到最新值?这个和工作内存与主内存同步有关,后面再讲。
得想办法实现可见性才行!不想用JAVA的关键字,还有什么办法?

如果我自己实现JVM,除了关键字,可以提供哪些办法实现内存可见性?顺着这个思路理出:
加锁:因为加锁,就要确保加锁代码块中的变量是最新的,

 while (true){
        
            synchronized (ThreadTest.class){
            }
            if (num.getI()==3) {
                System.out.println("到3s了!!!!");
                break;
            }
        }

目标达成!注意,因为我只需要一个更新主线程工作内存的动作就行,所以给谁加锁无所谓...

还有没有其他方法?试试线程的睡眠和唤醒。
Thread.sleep(100);
测试后,都能达成目标。

线程休眠,加锁,都能刷新(提交工作内存的改动和读取主内存的最新数据)工作内存。
那刚才提到的断点是不是可以把主内存的数据刷到工作内存?根据写调式器的经验,如果开发者下了断点,一般情况下都会把最新数据刷给调试器。

回到问题本身

println中使用了加锁的操作,导致调用println的线程(也就是这个main)更新了主内存的数据到工作内存..

posted @ 2023-02-10 23:24  方东信  阅读(90)  评论(0编辑  收藏  举报