一道诡异的volatile影响线程间可见性的问题

来源:13-1 volatile-

qq_凛冬将至_9

2023-01-14

老师小年好,翻看Java多线程面试复习资料时,想到了之前整理的一个问题,不清楚造成的原因是什么,真的很诡异。。。
主程序

package com.pf.java.thread.learning.c004_visibility;

import com.pf.java.thread.learning.util.SleepHelper;

public class Test_05_volatile_obj {

    static class Counter {
        /*volatile*/ int i; // 1

        void increment() {
            i++;
        }

        public int get() {
            return i;
        }
    }

    // 2
    /*volatile*/ Counter counter = new Counter();

    void increment() {
        counter.increment();
    }

    int get() {
        return counter.get();
    }

    public static void main(String[] args) {
        Test_05_volatile_obj c = new Test_05_volatile_obj();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                c.increment();
                System.err.println("count " + c.get()); // 3
                SleepHelper.sleepMilliSeconds(1000); // 4
            }
        }, "t1").start();

        new Thread(() -> {
            while (true) {
                if (c.get() == 5) {
                    break;
                }
            }
            System.err.println("t2 结束");
        }, "t2").start();
    }

}

辅助工具类:

package com.pf.java.thread.learning.util;

import java.util.concurrent.TimeUnit;

/**
 * Sleep帮助类,这里不考虑休眠被打断的情况下可以使用,仅测试目的,不可用于生产环境
 */
public class SleepHelper {

    public static void sleepSeconds(int s) {
        try {
            TimeUnit.SECONDS.sleep(s);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void sleepMicroSeconds(int s) {
        try {
            TimeUnit.MICROSECONDS.sleep(s);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void sleepMilliSeconds(int s) {
        try {
            TimeUnit.MILLISECONDS.sleep(s);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

这个程序的意图很简单:两个线程t1、t2分别对共享的计数器变量count进行操作和监听,t1线程循环10次对count中的i执行加1,t2线程不停的监听count中的i,监听到值为5则结束线程。现在发现程序的运行有一个诡异的现象:t1线程每次循环会打印计数器的值并休眠1秒,在1、2两处注释掉的情况下,t2线程监听不到,也就是计数器变量对t2是不可见的。如果1、2两处放开一处的注释,t2就能监听到!也就是volatile修饰对象引用或者对象中的字段都能确保该字段的可见性,这是我不理解的一个地方!另外一个问题,如果3、4两处都注释掉,而放开1、2两处,发现t2还是监听不了,volatile不是本身能确保可见性吗,为啥还一定要配合控制台打印(底层用到synchronized)或者sleep结束后从主存刷新这样的机制,这是我不理解的另一个地方!

老师按照我的表示运行下程序,然后帮我看看问题出在哪里,感谢啦(●’◡’●)

写回答

2回答

张婧仪

5天前

我来解答你的疑问。

第一个问题,如果Volatile修饰i,肯定保证可见性。如果修饰Counter,因为i属于Counter ,所以i变化,Counter 也会变,也会保证可见性。

第二个问题,受处理器调度影响,t2线程并不是一直监听,所以让t1加入睡眠走的慢点,可以监听到。如果不加睡眠,t1走的太快,t2就可能监听不到。

0
0

悟空

2023-01-26

0
0

线程八大核心+Java并发原理及企业级并发解决方案

完整的并发知识网络+丰富的工作内容分享+50余道并发高频面试题

2512 学习 · 940 问题

查看课程