volatile修饰List能否保证内部元素的可见性?

21 篇文章 0 订阅

实验一:ArrayList多线程下的可见性

代码

 	private static List<Integer> list;

    static {
       list  = new ArrayList<Integer>(20);
       list.add(0);
    }


    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(1);
        new Thread(()->{
            Thread.sleep(200);
            list.set(0,1);
            System.out.println("set 1");
            
        }).start();
        new Thread(()->{

            while (list.get(0) == 0){

            }
            System.out.println("end");
            latch.countDown();

        }).start();
        latch.await();
    }

现象

结果打印出"set 1"后,程序死循环无法退出。线程1中对list的操作对线程2不可见,导致while死循环无法退出。在多线程环境下,List具备可见性问题

实验二:对ArrayList添加Volatile修饰

代码

 	private volatile static List<Integer> list;

    static {
       list  = new ArrayList<Integer>(20);
       list.add(0);
    }


    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(1);
        new Thread(()->{
            Thread.sleep(200);
            list.set(0,1);
            System.out.println("set 1");
            
        }).start();
        new Thread(()->{

            while (list.get(0) == 0){

            }
            System.out.println("end");
            latch.countDown();

        }).start();
        latch.await();
    }

仅添加了volatile关键字修饰,其他没有任何变化。

现象

结果打印出"set 1"和“end”后,主线程退出。线程1对list做的操作貌似对线程2是可见的。

实验三:取消List的volatile并增加额外volatile字段

代码

	private static List<Integer> list;
	private static volatile boolean flag;
    static {
       list  = new ArrayList<Integer>(20);
       list.add(0);
    }

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(1);
        new Thread(()->{
            Thread.sleep(200);
            list.set(0,1);
            System.out.println("set 1");
            
        }).start();
        new Thread(()->{

            while (list.get(0) == 0){
				boolean v = flag;
            }
            System.out.println("end");
            latch.countDown();

        }).start();
        latch.await();
    }

现象

结果打印出"set 1"和“end”后,主线程退出。线程1对list做的操作貌似对线程2是可见的。

分析

对于实验一,我们很容易理解,在多线程下产生的可见性问题。面对这种问题,我们往往会通过volatile关键字来保证可见性,从而解决该问题。

对于普通的基本类型如int等没有什么疑问,但在这种引用类型,如数组List,添加该关键字到底保证的是List自己还是同样包含了其内部元素一起的可见性?

通过实验二,我们在List前增加了Volatile,从效果上来看好像真的是对内部元素有用,线程2感知到了线程1中对List的操作,但事实并没有这么简单。

我们通过实验三,去掉了List前的Volatile,增加了看似毫无用处的Volatile修饰的flag,然后在线程2中每次都去访问这个flag,结果竟然也能感知到线程1的操作。

volatile的全局可见性保证

我们平时一直所知的,是对于Volatile修饰的变量,禁止指令重排序,且在修改时会立即刷到主存,访问也是直接去主存拿。

对于引用类型的,Volatile不能保证其内部元素的可见性,仅仅能保证该引用指向的对象本身的可见性。

实际上从Java5开始,Volatile保证的已经不仅仅是被修饰的变量了,而是所有变量。

  1. 读取一个Volatile变量时,会去主存中读取该变量的值,同时也会将在该行为之后的变量的值一并从主存中拿取
  2. 写入一个Volatile变量时,会将该操作之前发生的修改统统刷新到主存中去。

举个栗子:

private int a;
private volatile int b;

// Volatile保证了指令重排序的规则,不会改变下面两个操作的顺序
public void read(){
    int bb = b;//从主存中读取b的最新值的同时,也会将a读取进来
    int aa = a;
}
public void write(){
    a = 1;
    b = 1;//将b的更新操作写入主存的同时,也会将a的更新写入主存
}

实验二和三的解释

在实验三中,while循环中会对volatile修饰的flag读取,然后由于循环又去访问list,因此在读取flag时会从主存中读取,同时会将此时的list数据从主存中读进来。因此造成了线程2成功看到了线程1对list的操作。

同理,在实验二中,list本身是volatile的,在访问list之前必然会先访问list引用,因此此时会去主存中找,顺带把后面的操作即list内部元素一并从主存拿过来,从而造成了线程2成功看到了线程1对list的操作。

实验二的分析中我们可以再次猜测一下,在访问list引用将此时list内部元素从主存中拿过来后,在下次访问list引用之前的这段时间,线程1对list做到操作应该仍然是对线程2不可见的。

我们基于该假设再次做个实验

再次实验

代码

	private volatile static List<Integer> list;

    static {
       list  = new ArrayList<Integer>(20);
       list.add(0);
       list.add(0);
    }


    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(1);
        new Thread(()->{
            Thread.sleep(200);
            list.set(0,1);
            System.out.println("set 1");
            Thread.sleep(200);
            list.set(1,1);
        }).start();
        new Thread(()->{
            List<Integer> l;
            while ((l=list).get(0) == 0){
            }
            System.out.println("end");
            while (l.get(1) == 0){
            }
            latch.countDown();

        }).start();
        latch.await();
    }

现象

打印出了"set 1"和"end",但主线程陷入死循环没有退出。

分析

可以看到当线程2通过访问list引用感知到线程1的变化后,使用新的引用l指向了数组,并在第二个while循环中对数组进行监听。

由于l没有可见性的保证,因此线程1中的操作是不可见的,从而验证了我们之前的想法。

因此如果要让每次都能感知到变化,需要修改list底层的数组类型为volatile的才行。

解决

在JUC包下的并发容器中,底层元素容器都采用了volatile修饰来保证可见性,但这会对性能造成影响。

同时我们可以使用Atomic包下的原子操作类型来保证原子性和可见性,如AtomicReferenceArrayAtomicIntegerArray

  • 10
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值