关于volatile和同步相关的东西,网上有太多错误和解释不清的东西, 所以查阅相关书籍和文章后总结如下, 如果还是也存在不正确的内容,请一定要指出来, 以免误人子弟:)
1. 原子性与可视性
原子性是指操作不能被线程调度机制中断, 除long和double之外的所有基本类型的读或写操作都是原子操作,注意这里说的读写, 仅指如return i, i = 10, 对于像i++这种操作,包含了读,加1,写指令,所以不是原子操作。 对于long和double的读写,在64位JVM上会把它们当作两个32位来操作,所以不具备原子性。
在定义long和double类型变量时,如果使用volatile来修饰,那么也可以获得原子性,除此以外,volatile与原子性没有直接关系。
可视性,volatile的主要作用就是确保可视性,那么什么是可视性?
在系统中(多处理器更加明显),对某一变量的修改有时会暂时保存在本地处理器的缓存中,还没有写入共享内存,这时候有另外一个线程读取变量在共享内存的值,那么这个修改对这个线程就是不可视的。
Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值直接写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。 原子操作不一定就有可视性, 比如赋值,i = 10, 如果i没有被特别修饰, 那么因为缓存的原因, 它仍然可能是不可视的
所以原子性和可视性是完全不同的两个概念
2. volatile的应用场景
详细可以参考java语言架构师Brain Geotz的文章
Java 中volatile 变量可以被看作是一种 “程度较轻的 synchronized”;与 synchronized 块相比,volatile 优点是所需的编码较少,并且运行时开销也较少, 不会引起线程阻塞。Volatile 变量具有 synchronized 的可视性特性,但是不具备原子特性。
这就导致Volatile 变量可用于提供线程安全,但是应用场景非常有限,在一些经典java书里,基本都不推荐使用volatile替代synchronized来实现同步,因为风险较大, 很容易出错。
Brain给出的使用volatile实现线程安全的条件:
对变量的写操作不依赖于当前值。 (count++这种就不行了)
该变量没有包含在具有其他变量的不变式中(Invariants,例如 “start <=end”)。
我的理解是, 这两个条件都是因为volatile不能提供原子性导致的, 如果多线程执行的一个操作不是原子性的, 使用volatile时就一定要慎重。
如果满足这两个条件, 多线程执行的操作是原子性的, 那就是可以使用,如:
将 volatile 变量作为状态标志使用
volatile boolean shutdownRequested; . ... public void shutdown() { shutdownRequested = true; } //不依赖当前值,原子操作 public void doWork() { while (!shutdownRequested) { // do stuff } }
文章中的其他几种模式, 也都差不多这个意思。
还有一种情况,如果读操作远远超过写操作,可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。
结合使用 volatile 和 synchronized 实现 “开销较低的读-写锁”
@ThreadSafe public class CheesyCounter { // Employs the cheap read-write lock trick // All mutative operations MUST be done with the 'this' lock held @GuardedBy("this") private volatile int value; public int getValue() { return value; } //使用volatile替代synchronized public synchronized int increment() { return value++; }
然而,你可以在读操作中使用 volatile 确保当前值的可见性,因此可以使用锁进行所有变化的操作,使用 volatile 进行只读操作。其中,锁一次只允许一个线程访问值,volatile 允许多个线程执行读操作,因此当使用 volatile 保证读代码路径时,要比使用锁执行全部代码路径获得更高的共享度 —— 就像读-写操作一样。
3. synchronized
class AtomTest implements Runnable { private volatile int i = 0; public int getVal() {return i;} public synchronized void inc() {i++; i++;} @Override public void run() { while (true) { inc(); } } } public class TestThread { public static void main(String[] args) throws InterruptedException { ExecutorService exec = Executors.newCachedThreadPool(); AtomTest at = new AtomTest(); exec.execute(at); while (true) { int val = at.getVal(); if (val % 2 != 0) { System.out.println(val); System.exit(0); } } } }
结果会输出奇数, 退出程序, 原因是getVal读到了inc的中间值。 这种情况只能在getVal方法前加synchronized
在读取的时候也加锁, 这样在读的时候如果正在写, 那么等待, 所以就不会读到inc的中间值。
关于synchronized值得注意的几个点:
1) 所有对象都含有一个锁,当调用到synchronized(object)块时,先检测obj有没有加锁,如果有, 阻塞, 如果没有, 对object加锁, 执行完后释放锁。
2) synchronized void f() {//...} 等价于 void f() { synchronized(this) {//...} }, 在当前对象上加锁
3) synchronized 提供原子性和可视性, 被它完全保护的变量不需要用volatile
4) synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法。
注意第一点, 非常重要:虽然sync块可以包裹一段代码,但是锁是加对象上,不是加在代码上,它的工作机制如下:
对于synchronized(obj) {//...}: 先检测obj有没有加锁,如果有, 阻塞, 如果没有, 对obj加锁, 执行块中的代码,完毕后释放锁。这里只检测obj对象上的锁,不关注代码块里的代码或者对象。
所以, 加锁的范围由obj决定,理解了这一点, 下面的很多种情况就会很容易理解:
1. 当两个并发线程访问同一个对象object中的这个相同synchronized(this)同步代码块时,一个时间内针对该对象的操作只能有一个线程得到执行。另一个线程必须等待。
- 如果同一对象已经加锁, 另一线程执行到sync块,检测到有锁挂起。
2. 然而,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
- 非sync代码,不关注对象是否加锁
3. 当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对该object中所有其它synchronized(this)同步代码块的访问将被阻塞。
- synchronized(this) 只是关注this对象, 只要this已加锁,执行到同一对象中不同方法的sync块时,也会阻塞。
4. 不同的对象实例的synchronized(this)方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法。
- 可以同时访问不同对象中的sync块, 原因很简单, 因为synchronized(this),关注的是this对象,不同对象的this是不一样的。
5. 同理,也可以对其他对象加锁,
1) 对于类中的成员对象: private Integer i = new Integer(0); synchronized(i) {//...} i创建在堆上,每个对象有一个i,所以效果与synchronized(this)一样。
2) 对于静态成员对象: private static Integer i = new Integer(0); synchronized(i) {//...}: i创建在静态区,属于类, 所以效果与synchronized(ClassName.class)一样。
6.对类对象加锁时,对该类的所有对象都起作用:synchronized(ClassName.class) {//...}
最后我在测试代码时,发现对于private Integer i = 0; synchronized(i) {//...}
锁的效果是全局的,推测可能是Integer对0进程打包时,自动生成的这个对象可能在常量区。 后来查了下资料才发现并非如此:
Integer实现中有一个IntegerCache类,它包含一个静态的Integer数组,在类加载时就将-128 到 127 的Integer对象创建了,并保存在cache数组中,一旦程序调用valueOf 方法,如果i的值是在-128 到 127 之间就直接在cache缓存数组中去取Integer对象。 所以这里的i引用的是整个全局数组里值, 所以锁也是全局的了。。。
如果改成private Integer i = 300, 然后加锁就只在本对象有效了。 原因是i不在缓存范围,所以创建在了堆上。
refer http://blog.csdn.net/xiaohai0504/article/details/6885137