#
本文要看啥
先不细谈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.
测试结果就是
- 有volatile 修饰的情况下, 子线程能拿到false值
- 没有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;
实际测试时发现:
- 无论ObjectA前有没有 volatile, 调用stop()方法都并不能正确终止子线程
- 成员变量ObjectB前添加volatile,同样不能正确终止子线程
- ObjectB的flag前加volatile,可以终止子线程 (这是当然的啦…)
- 如果循环里用的是
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的语义,而不是它的元素 , 暂时只能记录下来,待以后再深入原理去理解.
#
留下更多的疑惑
如上面所说,一个volatile引用的域或者元素并不具备volatile特性,因为对于该域的写入并不会触发StoreLoad屏障,就不会强迫该域值立刻回写主存。 如何证明?
spring里用置换的方式真的不会出问题么? 多个线程同时读取了一个list,然后各自加一个元素进去,刷新,这样不就出了问题?
如果说这样并不安全,那么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 貌似是安全了, 但是为什么这里也用了置换数组啊???