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 貌似是安全了, 但是为什么这里也用了置换数组啊???

评论和共享

  • 第 1 页 共 1 页
作者的图片

heeexy

世上是不是就没有你不认识的字了?


JAVA


南京