谈谈 synchronized 和 volatile 的区别
Posted 小羊子说
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了谈谈 synchronized 和 volatile 的区别相关的知识,希望对你有一定的参考价值。
本文从以下三个方面总结synchronized和volatile的区别:
- 原子性
- 内存可见性
- 重排序
文章目录
1. 什么是原子性
原子(Atomic)操作指相应的操作是单一不可分割的操作。
如对 int型变量 count 执行 count++ 时就不是原子性操作。该操作分为三个操作:
1)读取变量 count 的当前值 ;
2)拿 count 的当前值 和 1做加法运算;
3) 将 count 的值 加1后的值赋值给 count 变量。
在多线程环境中,非原子性操作可能会受其他线程的干扰。 如此环境中对count 读取时,可能得到的值是“过期”值,该值可能已经被其他线程修改了。
解决办法之一,引入 synchronized 关键字,可以实现原子性操作。其本质是通过该关键字所包含的临界区的排他性 保证在任何一个时刻只有一个线程能够执行临界区的代码。这使得临界区的代码代表了一个原子性操作。
换个角度理解:用 synchronized 加锁后, 谁得到这把锁 就执行谁的代码,执行完后 释放锁。
2.什么是内存可见性
CPU在执行代码时,为了减少变量访问的时间消耗,可能将代码中访问的值缓存到该 CPU 的缓存区中。因此相应的代码再次访问某个变量时,相应的值 可能是从 CPU的缓存 而不从主内存中读取的。 同样地,代码对这些缓存过的变量的值修改也可能仅是被写入 CPU 缓存区,而没有被写入主内存。
由于每个 CPU 都有自己的缓存区,因此一个CPU缓存区中的内容对于其他 CPU 是不可见的。
于是 在其他CPU 上运行的其他线程 可能无法 “看到”该线程对某个变量的值所做的更改。 这就是“内存可见性”。
换个角度理解:CPU为了效率,有个缓存区,同时有个主内存,一个CPU上运行的线程可能在不同的缓存区中修改值,而没有同步到主内存中,其他线程无法看到(无法访问)该线程修改的值。数据没有同步,于是不可见。
小结:synchronized 可实现原子性,同时也能保证内存的可见性(Memory visibility)。
除了synchronized 能保证内存的可见性,还有谁?
于是volatile联想到了,怎么想到的,深入学习Java线程的同学可能都知道。此时应该放到一起比较一下了,便于了解深入同步机制。
volatile 关键字 也能保证内存的可见性,即一个线程对一个采用了 volatile 关键字修饰的变量的值 的更改对于其他线程访问该变量的线程总是可见。
volatile 实现内存可见性的 核心: 当一个线程修改了一个 volatile 修饰的变量时,该值会被写入主内存(RAM),而不仅仅是当前 CPU 的缓存区。而其他CPU 缓存区中存储 该变量的值会因些失效。这就保证了其他线程访问该volatile 修饰的变量时总是可以获取该变量的最新值。
换个角度理解:volatile 会把值写入缓存区和主内存中,既然都写入主内存了,其他线程当然可以访问了,也顺其自然的可见了。
补充一点:volatile 并不能像 synchronized 那样 能够保证操作的原子性。
3. 重排序
volatile 关键字 另一个作用是:禁止 指令的重排序(Re-order)。
(注:此处需要深入理解Java内存模型后才好理解)
一切为了效率,编译器和CPU为了执行效率可能会进行指令的重排序。
举例说明,实例变量的初始语句:
SomeClass someObject = new SomeClass();
我们自然想到执行顺序:
- 创建类 SomeClass的实例;
- 将 SomeClass的实例引用 赋值给 变量 someObject。
还是为了效率,指令的重排序可能得到的执行顺序:
- 分配一段存储 SomeClass 的实例内存空间 (堆空间);
- 将 SomeClass的实例引用 赋值给 变量 someObject;
- 创建类 SomeClass的实例。
于是问题来了,当其他线程访问someObject 时,得到的仅仅是一段存储 SomeClass的内存空间的引用而已,此时的实例化尚未完成。异常就出现了。
解决思路也很简单,由于时指令的重排序闯祸,禁止就行了。而volatile 就是这个作用。
扩展:在双重检查加锁(DCL)单例模式中也是同样的解决办法。
4. 总结
synchronized和volatile的区别:
-
synchronized 能保证原子性和可见性
-
volatile 仅能保证内存可见性
-
synchronized 会导致上下文切换,volatile不会。
(注:上下文切换是线程状态的切换,会给CPU带来额外的开销)
参考资料:
《Java 多线程编程实战指南(设计模式篇)》 黄文海 (著)
以上是关于谈谈 synchronized 和 volatile 的区别的主要内容,如果未能解决你的问题,请参考以下文章