volatile是并发编程里面最轻量的同步机制,保证了可见性,经常出现在一些多读的情况.
volatile是一个特殊的修饰符,只有成员变量才能使用它。在Java并发程序缺少同步类的情况下,多线程对成员变量的操作对其它线程是透明的。volatile变量可以保证下一个读取操作会在前一个写操作之后发生.
Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器和运行时都会注意到这个变量是共享的,因此不会将变量上的操作和其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的时候总会返回最新写入的值。
在访问volatile变量时不会执行加锁操作,因此也不会使执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制。
加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。
voliate 的实现原理
volatile 可以保证线程可见性和禁止指令重排序,但是无法保证原子性。在 JVM 底层 volatile 是采用 “内存屏障” 来实现的。加入 volatile 关键字时,汇编后会多出一个 lock 前缀指令。lock 前缀指令其实就相当于一个内存屏障。
volatile可以保证线程可见性且禁止指令重排序,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的, 加入volatile关键字时,汇编后会多出一个lock前缀指令。lock前缀指令其实就相当于一个内存屏障。。
happen-before原则保证了程序的“有序性,对volatile变量的写操作 happen-before 后续的读操作.
当读取一个被volatile修饰的变量时,会直接从共享内存中读,而非线程专属的存储空间中读。
当volatile变量写后,线程中本地内存中共享变量就会置为失效的状态,因此线程B再需要读取从主内存中去读取该变量的最新值。
对该变量的写操作之后,编译器会插入一个写屏障。对该变量的读操作之前,编译器会插入一个读屏障。
线程写入,写屏障会通过类似强迫刷出处理器缓存的方式,让其他线程能够拿到最新数值。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
通过对OpenJDK中的unsafe.cpp源码的分析,会发现被volatile关键字修饰的变量会存在一个“lock:”的前缀指令(cpu提供的)。
Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。
同时该指令会将当前处理器缓存行的数据直接写会到主内存中,且这个写回内存的操作会使在其他CPU里缓存了该地址的数据无效(工作内存要想使用改地址的数据必须要重新去主内存里面获取数据)。
“Lock:”前缀有两个功能:
- 会对cpu总线和缓存进行加锁
2. 会把当前缓存行的数据写回到系统内存,同时使其它cpu里面缓存行的数据的相关缓存无效,强迫其它cpu重新从系统内存中进行读取,一旦执行lock指令,其它cpu对这个缓存行的或者相关的主内存的数据的读写请求都会被阻塞,直到lock锁释放为止
volatile的作用
轻量级的同步机制,乞丐版的synchronizid<br /> 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)<br /> volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。<br /> 禁止指令重排,保证特定操作的执行顺序。<br />
我们已经知道可见性、有序性及原子性问题,通常情况下我们可以通过Synchronized关键字来解决这些个问题,不过如果对Synchronized原理有了解的话,应该知道Synchronized是一个比较重量级的操作,对系统的性能有比较大的影响,所以,如果有其他解决方案,我们通常都避免使用Synchronized来解决问题。
而volatile关键字就是Java中提供的另一种解决可见性和有序性问题的方案。对于原子性,需要强调一点,也是大家容易误解的一点:对volatile变量的单次读/写操作可以保证原子性的,如long和double类型变量,但是并不能保证i这种操作的原子性,因为本质上i是读、写两次操作。
线程读写volatile的过程
用法 volatile string = “a”;
线程写volatile变量的过程:
1.改变线程本地内存中volatile变量副本的值
2.将改变后的副本的值从本地内存刷新到主内存
线程读volatile变量的过程:
1.从主内存中读取volatile变量的最新值到线程的本地内存中
2.从本地内存中读取volatile变量的副本
为何volatile不是线程安全的
如果两个线程同时对volatile变量做递增的话,现在主内存中还是1,当要对volition count进行操作的时候会强制从主内存里面读取,此时线程a的工作内存count是1,而线程b的工作内存的count也是1,它们两个同时进行操作,改成了2,然后它们又强制的给count =2 又刷回了主内存里面上.此时线程2写回主内存是2,线程1写回的主内存也是2 ,最后主内存的count 值就是2 了.
volatile特性
1.保证可见性
2.不保证原子性
3.禁止指令重排
可以把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步
可以看成
volatile操作对于单个读写是原子性的,但是一旦是有复合操作(i++;)就不是原子性的,
所以volatile变量自身具有下列特性:
可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。
volatile与synchronized区别
Ø volatile轻量级,只能修饰变量。主要作用是让各个线程获得最新的值。它强制线程每次从主内存中讲到变量,而不是从线程的私有内存中读取变量,从而保证了数据的可见性。不能用来同步,因为多个线程并发访问volatile修饰的变量不会阻塞。
Ø Synchronized重量级,还可修饰方法。是通过加对象锁或类锁的方式,保证同一时刻只有一个线程对此对象进行操作,操作完成后,归还锁,下一个线程才能继续执行。不仅保证可见性,而且还保证原子性。多个线程争抢synchronized锁对象时,会出现阻塞。
仅靠volatile不能保证线程的安全性。(原子性)
①volatile轻量级,只能修饰变量。synchronized重量级,还可修饰方法
②volatile只能保证数据的可见性,不能用来同步,因为多个线程并发访问volatile修饰的变量不会阻塞。
synchronized不仅保证可见性,而且还保证原子性,因为,只有获得了锁的线程才能进入临界区,从而保证临界区中的所有语句都全部执行。多个线程争抢synchronized锁对象时,会出现阻塞。
线程安全性
线程安全性包括两个方面,①可见性。②原子性。
从上面自增的例子中可以看出:仅仅使用volatile并不能保证线程安全性。而synchronized则可实现线程的安全性。
使用场景
valatile使用场景就是一个线程更新了一个值,其它线程需要立马察觉到这个值发生变化.
并发专家建议我们远离volatile是有道理的,这里再总结一下:
1.volatile是在synchronized性能低下的时候提出的。如今synchronized的效率已经大幅提升,所以volatile存在的意义不大。
2.如今非volatile的共享变量,在访问不是超级频繁的情况下,已经和volatile修饰的变量有同样的效果了。
3.volatile不能保证原子性,这点是大家没太搞清楚的,所以很容易出错。
4.volatile可以禁止重排序。
所以如果我们确定能正确使用volatile,那么在禁止重排序时是一个较好的使用场景,否则我们不需要再使用它。