test_volatile_object

# 本文要看啥


先不细谈volatile的基本原理,在读(写)这篇文章时,都是假设我们已经粗略了解了一点volatile的原理和作用的,主要就是 “读写都走主内存,保证任意线程对这个变量的可见性

在查看spring源码的时候,注意到spring在处理并发的操作List时, 虽然对list使用了volatile, 然而向list里面添加元素时,用的还是新建一个list,复制全部旧值,增加新元素,然后将旧的list地址指向新的list.

1
2
3
4
 	List<String> updatedDefinitions = new ArrayList(this.beanDefinitionNames.size() + 1);
    updatedDefinitions.addAll(this.beanDefinitionNames);
    updatedDefinitions.add(beanName);
    this.beanDefinitionNames = updatedDefinitions;

这么麻烦的操作,第一反应就是,volatile修饰的list, 直接添加元素依然不安全么?

去网上搜了一下相关问题, 参考博文地址 ,发现不止list, 对象也是一样的.

本文就是要来用代码直观地看看volatile 到底有什么效果,怎么用才有效果.

# 开始代码吧


# 线程共享对象里的boolean

注意代码要以**-server模式**运行,强制虚拟机开启优化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class VolatileObjectTest implements Runnable {
    // 加上volatile 就可以正常结束While循环了
    private ObjectA a;

    public VolatileObjectTest(ObjectA a) {
        this.a = a;
    }

    public void stop() {
        a.setFlag(false);
    }

    @Override
    public void run() {
        long i = 0;
        while (a.isFlag()) {
            i++;
          /**
         	 注意这里的sysout,如果有调用的话,即使没有volatile,子线程也经常能拿到a.flag,
         	 结合后面的测试,发现sysout 或者 sysout(a.isFlag())之前有"---"之类字符串
         	 都可能让a去从主内存去获取值,影响我们测试的结果
         	 所以测试的时候不要乱打sysout了,感兴趣的话可以自己去各种测试一遍
          */
            // System.out.println();  
        }
        System.out.println("子线程正常结束");
    }

    public static void main(String[] args) throws InterruptedException {
        // 注意代码要以-server模式运行,强制虚拟机开启优化
        // 如果启动的时候加上-server 参数则会 输出 Java HotSpot(TM) Server VM
        System.out.println(System.getProperty("java.vm.name"));
        VolatileObjectTest2 test = new VolatileObjectTest2(new ObjectA());
        new Thread(test).start();
        Thread.sleep(200);
        test.stop();
        Thread.sleep(200);
        System.out.println("主线程结束");
    }

    static class ObjectA {
        private boolean flag = true;

        public boolean isFlag() {
            return flag;
        }

        public void setFlag(boolean flag) {
            this.flag = flag;
        }
    }
}

这个代码还是很简单,

主线程将a的flag改为false,

子线程能正常结束的话, 说明子线程里 a 的 flag值获取到了false,

不能正常结束的话, 说明子线程a一直都是用其本地内存里的flag值,一直都是true.

测试结果就是

  1. 有volatile 修饰的情况下, 子线程能拿到false值
  2. 没有volatile ,子线程无法正常结束

在初步了解volatile 的可见性的情况下, 我会觉得这个结果很正常, 觉得自己掌握了volatile , 但是我们继续往下看…

# 线程共享对象里的对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class VolatileObjectTest implements Runnable {
    // 加上volatile 就可以正常结束While循环了
    private volatile ObjectA a;

    public VolatileObjectTest(ObjectA a) {
        this.a = a;
    }

    public void stop() {
        a.getObjectB().setFlag(false);
    }

    @Override
    public void run() {
        long i = 0;
        ObjectB b = a.getObjectB();
        while (b.isFlag()) {
            i++;
        }
        System.out.println(b.isFlag());
        System.out.println(a.getObjectB().isFlag());
        System.out.println("子线程正常结束");
    }

    public static void main(String[] args) throws InterruptedException {
        // 如果启动的时候加上-server 参数则会 输出 Java HotSpot(TM) Server VM
        System.out.println(System.getProperty("java.vm.name"));
        VolatileObjectTest test = new VolatileObjectTest(new ObjectA());
        new Thread(test).start();
        Thread.sleep(200);
        test.stop();
        Thread.sleep(200);
        System.out.println("主线程结束");
    }

    static class ObjectA {
        private boolean flag = true;
        private ObjectB objectB = new ObjectB();

        public boolean isFlag() {
            return flag;
        }

        public void setFlag(boolean flag) {
            this.flag = flag;
        }

        public ObjectB getObjectB() {
            return objectB;
        }
    }

    static class ObjectB {
        private boolean flag = true;

        public boolean isFlag() {
            return flag;
        }

        public void setFlag(boolean flag) {
            this.flag = flag;
        }
    }
}  

这次在ObjectA内添加了一个成员变量ObjectB,我们在子线程中跳出循环需要ObjectB中的flag变为false;

实际测试时发现:

  1. 无论ObjectA前有没有 volatile, 调用stop()方法都并不能正确终止子线程
  2. 成员变量ObjectB前添加volatile,同样不能正确终止子线程
  3. ObjectB的flag前加volatile,可以终止子线程 (这是当然的啦…)
  4. 如果循环里用的是 while (a.getObjectB().isFlag()) , ObjectA前又有volatile的话, 这样还是可以终止子线程.

测到这里,感觉这个博主给的例子是不是有问题啊,

1
2
3
4
 ObjectB b = a.getObjectB();
        while (b.isFlag()) {
            i++;
        }

问题是出在这里提前从a里面取出了b么,b已经指向不同的内存地址了么?不应该吧…

稍加思考,第三种结果里,b.flag用volatile修饰后,就可以正常退出,说明b还是指向的a里面的b的地址啊,没毛病啊.

突然感觉更晕了,把原博的评论翻到底,发现还真有人评论到这个,从评论里又学到了很多东西.

# 原博评论区的解疑

Q: 为什么sysout影响结果?

A: 如果在循环体内加一些语句,比如sysout或者new对象之类的稍微复杂而耗时的操作,就会发现就算没有volatile,线程同样可能被正常中断.因为经过高耗时操作之后,CPU会"怀疑人生",单心自己对b.flag的缓存不是最新的,而去从主存获取.在这种情况下,线程会结束,只不过不及时而已。

Q: 为什么 while (b.isFlag()) 和while(a.getObjectB.isFlag()) 结果有区别,后者就可以拿到最新的flag值?

A: 一个volatile引用的域或者元素并不具备volatile特性,因为对于该域的写入并不会触发StoreLoad屏障,就不会强迫该域值立刻回写主存。不过其读特性并没有问题,对volatile的读操作一定是去主存当中读取的

所以a.getObjectB 在这里a就已经去从主存中读取了.

这一点因此也就解释了第一个例子中,我们修改a.flag,可以正常地读到flag值.

Q: 但是问题又来了,写入不能保证刷新到主存的话,岂不是即使while(a.getObjectB.isFlag()) 也是仍然很有可能失败的?经过刚才例子的反复测试,依然很难碰到终止线程失败的情况.

A: 这个例子还是无法测出这种刷新主存不及时的情况,毕竟即使是不及时刷新,最终刷新了还是可以让子线程结束的.

另外一篇博客 从汇编语句的角度分析了volatile的数组只针对数组的引用具有volatile的语义,而不是它的元素 , 暂时只能记录下来,待以后再深入原理去理解.

# 留下更多的疑惑

  1. 如上面所说,一个volatile引用的域或者元素并不具备volatile特性,因为对于该域的写入并不会触发StoreLoad屏障,就不会强迫该域值立刻回写主存。 如何证明?

  2. spring里用置换的方式真的不会出问题么? 多个线程同时读取了一个list,然后各自加一个元素进去,刷新,这样不就出了问题?

  3. 如果说这样并不安全,那么concurrent包里是怎么实现安全的list的呢?

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
        public boolean add(E e) {
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
                Object[] elements = getArray();
                int len = elements.length;
                Object[] newElements = Arrays.copyOf(elements, len + 1);
                newElements[len] = e;
                setArray(newElements);
                return true;
            } finally {
                lock.unlock();
            }
        }
    

    ReentrantLock 貌似是安全了, 但是为什么这里也用了置换数组啊???