本文共 3827 字,大约阅读时间需要 12 分钟。
线程安全需要同时满足三个条件:
volatile能保证其修饰的变量的线程可见性但无法保证操作原子性,只能用于"多个变量之间或者某个变量的当前值与修改后值之间没有约束"的场景。在实现计数器(++count)和由多个变量组成的不变表达式方面,volatile无法胜任。
volatile的底层实现机制是什么?被volatile修饰的变量在进行写操作时,处理器会插入一条lock前缀的汇编代码,做了层"内存屏障",其作用为:
通过处理器之间的缓存一致性协议,当(处理器本)地缓存过期后会失效本地缓存,当更新该缓存时处理器重新从主存load数据到本地缓存并执行计算逻辑。
自增操作并不是原子的,比如"++count"操作就是三个原子操作的集合:
假设thread1和thread2均执行"++count"计数操作,thread1和thread2均执行完2但未执行3,此时thread1和thread2先后将count新值刷回主存,这就产生了线程不安全的现象。
只有在状态真正独立于程序内其他内容时才能使用volatile。
// 引用volatile操作不会像锁一样造成阻塞,因此,在能够安全使用volatile的情况下,volatile 可以提供一些优于锁的可伸缩特性。如果读操作的次数要远远超过写操作,与锁相比,volatile 变量通常能够减少同步的性能开销。
// 代码来源于Brian Goetz的《正确使用 Volatile 变量》一文,本文稍作修改@NotThreadSafe public class NumberRange { private volatile int lower, upper; public int getLower() { return lower; } public int getUpper() { return upper; } public void setLower(int value) { if (value > upper) throw new IllegalArgumentException(...); lower = value; } public void setUpper(int value) { if (value < lower) throw new IllegalArgumentException(...); upper = value; }}
假设NumberRange初始化后lower/upper分别为0和4,即数值范围为[0,4]。此时thread1和thread2分别执行setLower(3)和setUpper(2),最终lower/upper分别被thread1和thread2设置为3和2,数值范围被更新为[3,2],某种意义上看是一个无效的数值范围。
1、对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。2、该变量没有包含在具有其他变量的不变式中。
这种类型的状态标记的一个公共特性是:通常只有一种状态转换。
// 代码来源于Brian Goetz的《正确使用 Volatile 变量》一文,本文稍作修改volatile boolean shutdownRequested;public void shutdown() { shutdownRequested = true; }public void doWork() { while (!shutdownRequested) { // do stuff }}
// 代码来源于Brian Goetz的《正确使用 Volatile 变量》一文,本文稍作修改public class Floor { public Floor() { // initialization... }}public class Loader { public volatile Floor floor; public void init() { // this is the only write to floor floor = new Floor(); // initialize floor object here ... } public Floor getFloor() { return this.floor; }} public class SomeOtherClass {private Loader loader; public void doWork() { while (true) { // use "floor" only if it is ready if (loader != null) // 步骤1 doSomething(loader.getFloor()); // 步骤2 } }}
这是一个典型的读写冲突例子,引文中提到"如果Floor引用不是 volatile 类型,doWork() 中的代码在解除对Floor的引用时,将会得到一个不完全构造的Floor"。我对这句理解的是:thread2执行完步骤2后释放对Floor的引用时,thread1可能正在调用init方法初始化floor,此时thread2拿到的是还未被thread1完全初始化的Floor对象。如果doWork内部主动解除对floor对象的引用,则可能拿到未初始化完全的floor对象的引用。
即使floor对象完成初始化,对floor成员域的修改仍然是线程不可见的。
volatile引用可以保证任意线程都可以看到这个对象引用的最新值,但不保证能看到被引用对象的成员域的最新值。
因为volatile修饰的是floor对象的引用,如果thread1执行到步骤1时,thread3修改了floor成员域,其修改对thread1并不可见。思考:如果floor成员域均被volatile锁修饰,其成员域的修改是否对thread3可见?
// 代码来源于Brian Goetz的《正确使用 Volatile 变量》一文,本文稍作修改public class UserManager { public volatile String lastUser; public boolean authenticate(String user, String password) { boolean valid = passwordIsValid(user, password); if (valid) { User u = new User(); activeUsers.add(u); lastUser = user; } return valid; }}
这个模式要求被发布的值(lastUser)是有效不可变的 —— 即值的状态在发布后不会更改。与Case1中更新floor对象成员域不同,对String类的操作是在新的String实例上进行的,String对象本身的状态并未改变。String类的这种实现方式天然地提供了线程隔离性。
volatile并非用来解决高并发场景下数据竞争冲突的方案,它只是实现线程可见性的一种方式!如果多个线程同时更新volatile变量,需要采用同步机制解决数据竞争,如CAS或者锁等。
该模式中,Java Bean成员变量均被volatile修饰,且引用类型的成员变量也必须是有效不可变(Collection的子类如List, Set, Queue等有数组值的成员变量,volatile修饰的是数组引用并非数组元素!)。
转载地址:http://zofll.baihongyu.com/