(十四)volatile
# volatile介绍
volatile 的作用是保证变量在多线程之间的可见性。
synchronized是阻塞式同步,会在线程竞争激烈的情况下,会升级为重量级锁,还可能会死锁;而volatile是一种轻量级的同步机制。
在理解这个volatile可见性之前,需要先了解一下CPU高速缓存、Java内存模型的知识。
- 主内存:java虚拟机规定所有的变量(不是程序中的变量)都必须在主内存中产生,为了方便理解,可以认为是堆区。可以与前面说的物理机的主内存相比,只不过物理机的主内存是整个机器的内存,而虚拟机的主内存是虚拟机内存中的一部分。
- 工作内存:java虚拟机中每个线程都有自己的工作内存,该内存是线程私有的为了方便理解,可以认为是虚拟机栈。可以与前面说的高速缓存相比。线程的工作内存保存了线程需要的变量在主内存中的副本。虚拟机规定,线程对主内存变量的修改必须在线程的工作内存中进行,不能直接读写主内存中的变量。不同的线程之间也不能相互访问对方的工作内存。如果线程之间需要传递变量的值,必须通过主内存来作为中介进行传递。
CPU的运算速度和内存的运算速度不一样,多核情况下各个处理器(核),每个核都有自己的工作内存,工作内存之间又不能直接交互,必须通过主内存,而且不同硬件的操作系统更不一样,于是在CPU和内存之间增加高速缓存。
在多线程环境下,就会有缓存一致性问题,也就是说,在多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致。
其实说的缓存一致性,就是我们说的可见性。
缓存一致性,是通过MESI协议(缓存一致性协议)进行的。当某个CPU在写数据时,如果发现操作的变量是共享变量,则会通知其他CPU告知该变量的缓存行是无效的,因此其他CPU在读取该变量时,发现其无效会重新从主存中加载数据。
MESI协议即:Modified(被修改)、Exclusive(独享)、 Share (共享的)、Invalid(无效的)。
Java内存模型的主要目标是定义程序中变量的访问规则,规范了Java虚拟机与计算机内存是如何协同工作: 规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。
初学线程,会经常把volatile和synchronized的用法混淆,上个例子解释一下,例子有点长,但希望你可以看完:
demo:
/**
* @author HaC
* @date 2020/6/23
* @Description
*/
public class VolatileTest extends Thread {
public static Counter1 counter1 = new Counter1();
public static Counter2 counter2 = new Counter2();
public static Counter3 counter3 = new Counter3();
public static Counter4 counter4 = new Counter4();
public static void main(String[] args) {
//100个线程去访问
VolatileTest[] mythreadArray = new VolatileTest[100];
for (int i = 0; i < 100; i++) {
mythreadArray[i] = new VolatileTest();
}
for (int i = 0; i < 100; i++) {
mythreadArray[i].start();
}
while (Thread.activeCount() > 4) //主线程和守护线程,只剩下主线程和守护线程 就退出。
Thread.yield();
//保证执行完毕,如果无法理解上面的,可以设置休眠时间,如下
/* try {
Thread.sleep(10 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
System.out.println(Thread.currentThread().getName() + " counter1 =" + counter1.getCount());
System.out.println(Thread.currentThread().getName() + " counter2 =" + counter2.getCount());
System.out.println(Thread.currentThread().getName() + " counter3 =" + counter3.getCount());
System.out.println(Thread.currentThread().getName() + " counter4 =" + counter4.getCount());
}
private void addCount() {
for (int i = 0; i < 100; i++) {
// synchronized (VolatileTest.class) {
counter1.setCount();
// }
counter2.setCount();
counter3.setCount();
counter4.setCount();
}
}
@Override
public void run() {
addCount();
}
public static class Counter1 {
private volatile int count = 0;
public void setCount() {
count++;
}
public int getCount() {
return count;
}
}
//synchronized
public static class Counter2 {
private int count = 0;
public synchronized void setCount() {
count++;
}
public int getCount() {
return count;
}
}
//Lock的ReentrantLock
public static class Counter3 {
private int count = 0;
Lock lock = new ReentrantLock();
public void setCount() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
//java并发包中的原子操作类,原子操作类是通过CAS循环的方式来保证其原子性的
public static class Counter4 {
private AtomicInteger count = new AtomicInteger();
public void setCount() {
count.getAndIncrement();
}
public AtomicInteger getCount() {
return count;
}
}
}
输出:
main counter1 =9993
main counter2 =10000
main counter3 =10000
main counter4 =10000
# volatile不能确保原子性
当多个线程访问一个变量时,没有对 count 进行加锁,因为count++不是原子性的, 虽然每次count++修改了都把本地内存刷新到主内存,但是对其他内存来说是无效的。
ThreadA、ThreadB 同时往主内存读值,拿到count是1,ThreadA修改count的值为2,然后根据volatile,立马往主内存回写(保证了可见性),但是ThreadB 又自增,但是ThreadB 一开始读到的count也是1,回写2,这样子就会导致两个线程都执行了,但是count的值是2,而不是3。(不能保证原子性)
count ++,count +=1的原子过程是这样的:
- count ++ 可以拆分为 count = count +1
- 先从堆(主内存)拿到 count
- 然后写入栈(私有内存)
这种情形在《Effective JAVA》中称之为“安全性失败”。
所以volatile不能当计时器。
综上,要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
- 对变量的写操作不依赖于当前值。
- 该变量没有包含在具有其他变量的不变式中。
上面的例子提到了三种方法保证原子性:
- synchronized
- ReentrantLock
- AtomicInteger
# volatile使用场景
用的最广泛的就是 单例模式:
public class Singleton {
private static volatile Singleton _instance; // volatile variable
public static Singleton getInstance() {
//双重检查加锁,只有在第一次实例化时,才启用同步机制,提高了性能。
if (_instance == null) {
synchronized (Singleton.class) {
if (_instance == null)
_instance = new Singleton();
}
}
return _instance;
}
}
# 最后
来到这里,来总结一下:
synchronized和volatile其实不是一个东西,volatile不能确保原子性,自然就不能锁住资源了。
# 1、 volatile到底做了什么:
- 1、 禁止了指令重排
- 2、保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量值,这个新值对其他线程是立即可见的 不保证原子性(线程不安全)
# 2 、synchronized关键字和volatile关键字比较:
volatile
关键字是线程同步的轻量级实现,所以volatile
性能肯定比synchronized
关键字要好。
但是volatile
关键字只能用于变量而synchronized
关键字可以修饰方法以及代码块。
synchronized
关键字在JavaSE1.6
之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized
关键字的场景还是更多一些。
多线程访问volatile关键字不会发生阻塞,而 synchronized
关键字可能会发生阻塞
volatile
关键字能保证数据的可见性,但不能保证数据的原子性。 synchronized
关键字两者都能保证。
volatile
关键字主要用于解决变量在多个线程之间的可见性,而 synchronized
关键字解决的是多个线程之间访问资源的同步性。
注意:
可见性的意思:
进程中的内存分为工作内存(线程内存)和主内存,普通变量的读写依赖于当前工作内存,直到线程结束,才会把值更新到主内存,
当有多线程存在时,就无法保证数据的真实性(可见性),其他线程读到的数据可能旧的。(这里需要知道Java内存模型(JMM) (opens new window))
volatile
修饰的变量每次获取的值都是从主内存中直接读的,写完之后也会直接更新到主内存,实现方式以机器指令(硬编码)的方式实现
jkd之后的版本在设计线程安全上都是基于volition和显示锁的方式,很少有用同步块和同步方法的方式,因为同步块方法的来讲,线程以串行的方式经过,效率太低.容易阻塞,而且保持原子性,只要线程进去就无法被打断,而volatile
不会阻塞.不保证原子性.
有序性的意思:
jvm和处理器在编译Java代码的时候,出于性能考虑,会对原有的代码进行重排序,(也就是指令重排)我们写好的代码都有顺序,在我们执行的时候由JVM 内存模型里的程序计数器标记的,保证线程安全的时候,一般都会禁止指令重排即保证有序性.说是并发环境下指令重排会有很多问题.
但是volatile
和 synchronized
的有序是不同的:
volatile
关键字禁止JVM编译器已及处理器对其进行重排序
synchronized
保证顺序性是串行化的结果,但同步块里的语句是会发生指令从排。
# 3、总结
synchronized 保证三大性,原子性,有序性,可见性,
volatile 保证有序性,可见性,不能保证原子性
参考:
- https://blog.csdn.net/chenxiaoti/article/details/82776128