volatile介绍
volatile 的作用是保证变量在多线程之间的可见性。
synchronized是阻塞式同步,会在线程竞争激烈的情况下,会升级为重量级锁,还可能会死锁;而volatile是一种轻量级的同步机制。
在理解这个volatile可见性之前,需要先了解一下CPU高速缓存、Java内存模型的知识。
主内存:java虚拟机规定所有的变量(不是程序中的变量)都必须在主内存中产生,为了方便理解,可以认为是堆区。可以与前面说的物理机的主内存相比,只不过物理机的主内存是整个机器的内存,而虚拟机的主内存是虚拟机内存中的一部分。
工作内存:java虚拟机中每个线程都有自己的工作内存,该内存是线程私有的为了方便理解,可以认为是虚拟机栈。可以与前面说的高速缓存相比。线程的工作内存保存了线程需要的变量在主内存中的副本。虚拟机规定,线程对主内存变量的修改必须在线程的工作内存中进行,不能直接读写主内存中的变量。不同的线程之间也不能相互访问对方的工作内存。如果线程之间需要传递变量的值,必须通过主内存来作为中介进行传递。
CPU的运算速度和内存的运算速度不一样,多核情况下各个处理器(核),每个核都有自己的工作内存,工作内存之间又不能直接交互,必须通过主内存,而且不同硬件的操作系统更不一样,于是在CPU和内存之间增加高速缓存。
在多线程环境下,就会有缓存一致性问题,也就是说,在多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致。
其实说的缓存一致性,就是我们说的可见性。
缓存一致性,是通过MESI协议(缓存一致性协议)进行的。当某个CPU在写数据时,如果发现操作的变量是共享变量,则会通知其他CPU告知该变量的缓存行是无效的,因此其他CPU在读取该变量时,发现其无效会重新从主存中加载数据。
Modified(被修改)、Exclusive(独享)、 Share (共享的)、Invalid(无效的)。
Java内存模型的主要目标是定义程序中变量的访问规则,规范了Java虚拟机与计算机内存是如何协同工作:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。
**
* @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;
}
}
参考:
https://www.javazhiyin.com/71807.html
https://blog.csdn.net/chenxiaoti/article/details/82776128