Java线程可见性——加一句System.out.println后运行结果不一样?

今天突然想起一个以前有人提到过的问题,大概就是A线程持有一个引用类型b变量(不加valotile或者final),A通过检查b的状态来控制A线程的循环退出,然后主线程通过引用修改了b的值,按理说因为A线程的b变量(真正的b实际上还在堆里面)被拷贝到线程内存里面,无法察觉到主线程对b的修改,运行结果的确是这样,只要主线程不结束(阻塞住),A线程就会一直阻塞住。然后问题来了,如果在A线程的循环里面加一个System.out.print/println,随便输出什么都好,A居然可以察觉到主线程对b的修改了!

测试代码如下:

@Test
public void test() throws InterruptedException {
    A a = new A();
    new Thread(a).start();
    Thread.sleep(3000);
    a.b = 2;
    //阻塞住主线程
    while (true){}
}

private class A implements Runnable{

    public Integer b = 1;

    @Override
    public void run() {
        while (true){
//                System.out.println(b);
            if(b.equals(2))
                break;
        }

        System.out.println("A is finished!");
    }
}

如果把注释掉的那行System.out.println应用上,就会发现A可以结束。

我一开始以为是因为输出b,控制台有特别的操作(例如会去主内存看一下)?后来再换个变量输出,发现输出什么A依然可以结束,无奈之下去stackoverflow提问一下,结果被人标注问题重复了(゜▽゜*),然后给了我那个问题的链接

Boann回答得很详细,原因是System.out.print里面有加锁!而jvm对于这个加锁操作,会做一件事,不缓存线程变量!这样一切都说得通了,不拷贝就不存在可见性问题了。

public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

根据这个说明,修改一下原来的代码,把输出语句换成对当前对象加锁

while (true){
            synchronized (this){
                if(b.equals(2))
                    break;
            }
}

果然A可以结束了(察觉到b的修改)。


到这里其实已经挺不错了,但是好奇心重,又试了下其他操作,结果一发不可收拾.....

不获取线程对象的锁,加个c,获取c的锁

private String c = "123";

    @Override
    public void run() {
        while (true){
            synchronized (c){
                if(b.equals(2))
                    break;
            }
}

这样也可以,再换个操作

    @Override
    public void run() {
        synchronized (this) {
            while (true) {
                if (b.equals(2))
                    break;
            }
        }
        System.out.println("A is finished!");
    }

结果出人意料的是, 这样就不行了~

如果单看源代码,上面这种和一开始我们修改的没什么区别,前者和后者的唯一区别就是while的获取锁的顺序不一样,也看不出有什么不同的地方。回顾一下,加System.out.println(加锁)使jvm不cache局部变量,那先加锁再while肯定是cache变量b了。这里我们从字节码上分析下执行过程,因为源代码和编译后的字节码差距是很大的,这里通过javap命令查看两个文件的字节码的区别(对虚拟机不太了解的同学可以去看一下深入理解JVM了)。

首先是先while再获取锁的字节码,也就是A可以结束,即b对于A可见(只关注run方法)

public void run();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: aload_0
         5: getfield      #3                  // Field b:Ljava/lang/Integer;
         8: iconst_2
         9: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        12: invokevirtual #4                  // Method java/lang/Integer.equals:(Ljava/lang/Object;)Z
        15: ifeq          23
        18: aload_1
        19: monitorexit
        20: goto          36
        23: aload_1
        24: monitorexit
        25: goto          33
        28: astore_2
        29: aload_1
        30: monitorexit
        31: aload_2
        32: athrow
        33: goto          0
        36: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
        39: ldc           #6                  // String A is finished!
        41: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        44: return
      Exception table:
         from    to  target type
             4    20    28   any
            23    25    28   any
            28    31    28   any
      LineNumberTable:
        line 17: 0
        line 18: 4
        line 19: 18
        line 20: 23
        line 22: 36
        line 23: 44

接着是先获取锁再while的字节码

 public void run();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: aload_0
         5: getfield      #3                  // Field b:Ljava/lang/Integer;
         8: iconst_2
         9: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        12: invokevirtual #4                  // Method java/lang/Integer.equals:(Ljava/lang/Object;)Z
        15: ifeq          4
        18: goto          21
        21: aload_1
        22: monitorexit
        23: goto          31
        26: astore_2
        27: aload_1
        28: monitorexit
        29: aload_2
        30: athrow
        31: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
        34: ldc           #6                  // String A is finished!
        36: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        39: return
      Exception table:
         from    to  target type
             4    23    26   any
            26    29    26   any
      LineNumberTable:
        line 16: 0
        line 18: 4
        line 19: 18
        line 21: 21
        line 22: 31
    line 23: 39

注意这两个的Code部分15字节码ifeq,就算不了解这些字节码指令是什么意思也大概能猜到。照顾一下没看过JVM的同学,简单说明一下,对于每个方法来说,一个方法的执行对应着都是一个方法栈的入栈出栈,例如i++就是把i压入栈,i出栈加1后再压入栈,最后i出栈赋给原来的i。因此基本所有的操作都是基于栈来进行。

这里先说一下我们关注的几个指令

  • aload_n:把索引为n的变量从主内存中并放入工作内存的变量的副本中(cache),索引为0的是this,所以aload_0是把this压入栈
  • ifeq:弹出栈顶元素并判断是否等于0,如果等于0跳到后面指定的指令
  • goto:知道c语言和java的goto的话,这个指令意思一样,跳到后面指定的指令

JVM指令

Oracle的JVM指令说明

关于aload

为了说明方便,我们定义先while后加锁的是Code1,先加锁后while的是Code2,指令序号n对应的指令是Pn

先看Code1。在P8,9,12执行Integer.equals方法后,把比对结果(java底层true和false也是用1和0表示)压入栈,P15ifeq判断栈顶元素是否为0(if条件运算符判断结果为false,即b!=2),Code1中ifeq后跳P23aload_1,P23从本地变量b(对于JVM来说,b是一个引用)压入栈中(不cache b),下一条指令P19释放锁后,P20goto跳到P33,P33又跳回了P0,重新执行while。相对应的Code2也按上面的步骤看,总结一下Code1和Code2

Code1指令 Code1 Code2指令 Code2
P8,P9,P12 执行equals方法 P8,P9,P12 执行equals方法
ifeq 只关注false的情况 ifeq 只关注false的情况
aload_1 从局部变量获取引用b后压入栈 P4 aload_0 把this压入栈
monitorexit 释放锁 P5 getfield 获取this.b的值后压入栈顶 (cached b)
P25,P33 goto 最后跳到P0 P8,P9,P12 while循环继续
P2,P3,P4,P5,P8,P9... astore_1后获取锁继续while循环

对比一下,Code1和Code2在if判断失败后继续循环前,Code1多了一个aload_1,这里就是重新检肃了b引用,甚至在循环尾部都还astore_1一次,所以Code1并没有cache b,而Code2始终都没有重新检索b,所以Code1能看到b的变化,Code2就不能。至于JVM为什么会分别处理,这就不知道了- -

水平有限,若有错误地方望指出

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,169评论 11 349
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,560评论 18 399
  • Java8张图 11、字符串不变性 12、equals()方法、hashCode()方法的区别 13、...
    Miley_MOJIE阅读 3,690评论 0 11
  • 相关概念 面向对象的三个特征 封装,继承,多态.这个应该是人人皆知.有时候也会加上抽象. 多态的好处 允许不同类对...
    东经315度阅读 1,917评论 0 8
  • 壹| 在路上的那些人 据说你只要通过六个人就能认识全世界的任意一个人 我喜欢在路上 因为我要与你们相遇、相识......
    海螺里的海姑娘阅读 800评论 0 1