使volatile字段具有原子性操作,volatile字段
使volatile字段具有原子性操作,volatile字段
简介
对于volatile字段的期望目标是它能够在单线程和多线程程序之间保持一致行为。它们不是不可能保持行为一致,但是它们并没有保证行为一定一致。
在Java5以上版本中,这一问题的解决方法是使用一类以Atomic为开头命名的Java类。但是这种方式的性能是十分低下的。在内存上它添加了一个头标记和填充物,在性能上它添加了引用和对相对位置的控制,在语法上它变得不那么容易去使用。
在我看来,使volatile字段表现的和我们预期方式一致的最简单办法是JVM必须支持原子字段,这种方式并不会被当前的Java内存模型所禁止但也不会得到保障。
为什么使用volatile字段?
使用volatile字段的好处是,它可以在多个线程间可视化。此外编译器的一些对数据重读的优化也会被消除。因此,你就会经常在数据没有被改变的情况下重新检测它们的值。
如:在没有volatile修饰的情况下
Thread 2: int a = 5; Thread 1: a = 6;
运行后:
Thread 2: System.out.println(a); // prints 5 or 6
使用volatile
Thread 2: volatile int a = 5; Thread 1: a = 6;
运行后:
Thread 2: System.out.println(a); // prints 6 given enough time
为什么不频繁使用volatile?
Volatile字段的读写访问本质上是比较慢的。当你写volatile字段时,它占据整个CPU流水线来确认数据已经被写到缓存中。如果没有上述操作,可能会导致下次读该数据时获得一个旧值,即使是在同一个线程中(可以看看AtomicLong.lazySet()是如何避免占据CPU流水线的)。
使用volatile大概会导致10倍的开销代价,这可能是你不想每次访问都发生的。
使用volatile的局限是什么?
一个明显的局限是对volatile字段的操作并不是原子的,即使当你认为可能是这样的。比这更糟糕的是,经常结果是一样的,没有看出它不是原子性操作。如:可能volatile字段在多线程下正常运行了很多年,一次偶然的改变可能就会导致突然的崩溃。它可能是Java版本的改变或对象加载到内存的位置改变。如在运行程序之前你加载的其它jar包。
示例:更新一个值
Thread 2: volatile int a = 5; Thread 1: a += 1; Thread 2: a += 2;
运行后:
Thread 2: System.out.println(a); // prints 6, 7 or 8 even given enough time.
出现这种结果的原因是读和写变量a的操作是相互分离的,这样你就导致了一个竞争条件。可能99%的时间里,程序会表现的正常,但是有时并不会。
对于这种情况你可以怎么做?
你需要使用AtomicXxxx类。这些类使用一些方法来封装volatile字段,使得它们的行为和预期一致。
Thread 2: AtomicInteger a = new AtomicInteger(5); Thread 1: a.incrementAndGet(); Thread 2: a.addAndGet(2);
运行后:
Thread 2: System.out.println(a); // prints 8 given enough time.
我的建议?
JVM提供了使程序和我们预期相同的方法,只不过令人惊讶的是你需要使用特殊类来实现Java内存模型不保证的事情。我的建议是Java内存模型应该改变以支持现在由并发AtomicClasses类实现的功能。
在任何情况下,单线程的表现是不会变的。不会出现竞争条件的多线程程序也是表现一致的。唯一的区别是多线程程序不一定需要检测竞争条件但是底层实现已经改变了。
当前方法 | 建议语法 | 注释 |
x.getAndIncrement() | x++ or x += 1 | |
x.incrementAndGet() | ++x | |
x.getAndDecrment() | x– or x -= 1 | |
x.decrementAndGet() | –x | |
x.addAndGet(y) | (x += y) | |
x.getAndAdd(y) | ((x += y)-y) | |
x.compareAndSet(e, y) | (x == e ? x = y, true : false) | 需要添加其它语言中的逗号语法 |
这些新添加的特性应当支持所有的基本类型,如boolean,byte,short,int,long,float和double类型。
其它的一些赋值操作也应当被支持,如:
当前操作 | 建议语法 | 注释 |
原子乘法 | x *= 2; | |
原子减法 | x -= y; | |
原子除法 | x /= y; | |
原子取余 | x %= y; | |
原子移位 | x <<= y; | |
原子移位 | x >>= z; | |
原子移位 | x >>>= w; | |
原子与 | x &= ~y; | 清除bit位 |
原子或 | x |= z; | 设置bit位 |
原子与或 | x ^= w; | 异或bit位 |
风险是什么?
这可能会导致依赖这些操作的代码偶尔会因为竞争条件而出错。
在线程安全的角度考虑,不应该出现支持过于复杂表达式的可能性。因为这会导致哪些看起来会正常工作的代码出现意想不到的错误。只有最小的改动才会使更改后的结果优于更改前。
JEP193-加强的volatiles
JEP193向Java中添加了下面功能。示例如下:
class Usage { volatile int count; int incrementCount() { return count.volatile.incrementAndGet(); } }
在我看来,这种方式有一点小局限:
- 语法有了较大改变。修改Java内存模型的实现可以使得Java语法不用太大改变,同时可能编译器基本都不需改变。
- 它是一种小众的普通方案。它应该支持像volime+=quantity这种操作,即使变量类型为double。
- 这种方案给了开发者更多的疑惑,他们需要学习为什么应该使用上述语法而不是用x++。
我不确定这种笨拙的语法是否使得发生的事情变得更加清晰。考虑如下的示例:
volatile int a, b; a += b;
或者
a.volatile.addAndGet(b.volatile);
或者
AtomicInteger a, b; a.addAndGet(b.get());
对于单独一行代码,这些操作中哪一些是原子操作?答案是一个也没有。然而,intel的TSX系统可以让它们变成原子操作。如果我编写a+=b的表达式,你是否是去改变上述代码的表现行为;而不是发明一种在大多数时间做同样事情的新语法,但同时其中一种是有保证而另外的则没有。
结论
如果Java内存模型保证相同的单线程代码和多线程代码操作结果一致,那么使用AtomicInteger和AtomicLong所带来的大部分语法和性能开销是能够被消除的。
这种特性应当在Java的早期版本中通过字节码实现的方式加入进来。
原文链接: javacodegeeks 翻译: Wld5.com - jessenpan译文链接: http://www.wld5.com/12685.html
[ 转载请保留原文出处、译者和译文链接。]
用户点评