一、volatile是什么?
volatile是Java虚拟机提供的轻量级(乞丐版synchronized)的同步机制(synchronized是重量级)
PS:杀鸡不用宰牛刀,synchronized轻易不要使用,每次只能跑一个线程效率太低。
volatile三大特性:
1、保证可见性
2、不保证原子性(因为不保证原子性,所以为轻量级)
3、禁止指令重排
Java代码演示volatile保证可见性和不保证原子性:
class MyData { // MyData.java ===> MyData.class ===> jvm字节码
volatile int number = 0;
public void addTo60() {
this.number = 60;
}
// 请注意,此时number前面是加了volatile关键字修饰的,volatile不保证原子性
public void addPlusPlus() {
number++;
}
// 使用AtomicInteger来保证原子性,具体内容下一篇博文中详细讲解
AtomicInteger atomicInteger = new AtomicInteger();
public void addMyAtomic() {
atomicInteger.getAndIncrement();
}
}
/**
* 1 验证volatile的可见性
* 1.1 假如int number = 0;number变量之前根本没有添加volatile关键字修饰,没有可见性
* 1.2 添加了volatile,可以解决可见性问题
* <p>
* 2 验证volatile不保证原子性
* 2.1 原子性是指什么意思?
* 不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。需要整体完整,要么同时成功,要么同时失败。
* 2.2 volatile不保证原子性的案例演示
* 2.3 why(保证可见性却不保证原子性)博文最后给出解释
* 2.4 如何解决原子性?
* 2.4.1 加sync
* 2.4.2 使用juc下AtomicInteger
*/
public class VolatileDemo {
public static void main(String[] args) {// main是一切方法的运行入口
MyData myData = new MyData();// 资源类
for (int i = 1; i <= 20; i++) {
new Thread(() -> {
for (int j = 1; j <= 1000; j++) {
myData.addPlusPlus();// 不保证原子性
myData.addMyAtomic();// 保证原子性
}
}, String.valueOf(i).start();
}
// 需要等待上面20个线程都全部计算完成后,再用main线程取得最终的结果值看是多少?
// 为什么>2?
// 因为默认后台两个线程,一个main,一个GC线程,只要是大于2说明还有线程需要执行,直到上面的线程执行完。
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "\t int type, finally number value: "
+ myData.number);
System.out.println(Thread.currentThread().getName() + "\t AtomicInteger type, finally " +
"number value: " + myData.atomicInteger);
}
// volatile可以保证可见性,及时通知其他线程,主物理内存的值已经被修改。
public static void seeOkByVolatile() {
MyData myData = new MyData();// 资源类
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in");
// 暂停一会儿线程
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.addTo60();
System.out.println(Thread.currentThread().getName() + "\t updated number value: " +
myData.number);
}, name:"AAA").start();
// 第二个线程就是我们的seeOkByVolatile线程
while (myData.number == 0) {
// seeOkByVolatile线程就一直在这里等待循环,知道number不再等于0;
}
System.out.println(Thread.currentThread().getName() + "\t mission is over,seeOkByVolatile" +
" get number value: " + myData.number);
}
}
其实在了解volatile之前,应该先了解JMM(Java内存模型),所以我们现在只是对volatile做一个初步的了解,下面我们说说Java内存模型到底是个什么玩意儿。把JMM搞懂了,volatile自然也就明白了~
二、谈谈JMM(Java内存模型):
JMM本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式
PS:就好比十二生肖里龙并不真实存在一样,只是一种概念一种规范。
JMM关于同步的规定:
1、线程解锁前,必须把共享变量的值刷新回主内存
2、线程加锁前,必须读取主内存的最新值到自己的工作内存
3、加锁解锁是同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方叫栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
Java内存模型可见性总结:
1、将要操作的共享变量从主内存读取到工作内存
2、修改共享变量并写回主内存
3、操作完成后第一时间通知其他线程
JMM三大特性(大多数多线程开发需要遵守的规范):线程安全性获得保证
1、工作内存与主内存同步延迟现象导致的可见性问题
可以使用synchronized或volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。
2、对于指令重排导致的可见性问题和有序性问题
可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化。
1、可见性
通过对JMM的介绍,我们知道:
各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后再写回到主内存中的。
这就可能存在一个线程A修改了共享变量X的值但还未写回主内存时,另外一个线程B又对主内存中同一个共享变量X进行操作,但此时A线程工作内存中共享变量X对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题。
2、原子性
number++在多线程下是非线程安全的,如何不加synchronized解决?
使用juc下AtomicInteger
为什么使用AtomicInteger能解决原子性?
因为CAS
PS:atomic原子引用和CAS的内容将在下一篇博文中详细讲解。
3、有序性
通俗点说就是高考做试卷,碰到不会的题先跳过,你做题的顺序跟高考老师出题的顺序不一定是一样的。
3.1 指令重排1
3.2 指令重排2
指令重排2案例
禁止指令重排小结:
volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的线下
先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:
一、保证特定操作的执行顺序,
二、保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用就是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
三、你在哪些地方用到过volatile?
1 单例模式;2 读写锁手写缓存时用到;3 JUC包中大规模使用volatile
3.1 单例模式DCL代码
public class Singleton {
private Singleton() {
System.out.println("单例对象");
}
// 不加volatile可能会出问题
private static volatile Singleton singleton = null;
// DCL(Double Check Lock双端检索机制)
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
单例模式volatile分析
DCL(双端检锁)机制不一定线程安全,原因是有指令重排序的存在,加入volatile可以禁止指令重排
原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没完成初始化。
instance = new SingletonDemo();可以分为一下3步完成(伪代码)
memory= allocate(); // 1.分配对象内存空间
instance(memory); // 2.初始化对象
instance = memory; // 3.设置instance指向刚分配的内存地址,此时instance!=null
步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。
memory= allocate(); // 1.分配对象内存空间
instance = memory; // 3.设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成!
instance(memory); // 2.初始化对象
但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。
所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。
最后,我们思考一个问题
为什么volatile保证可见性,却不保证原子性?
这里涉及到JVM字节码的内容,举个例子,ABC三个线程操作共享内存0,由于计算机底层JVM字节码将操作分为三步,1.getfield读取,从主内存将共享变量读取到工作内存;2.iadd执行+1操作;3.putfiled写入,将结果写回主内存并通知其他线程。这就会发生一种情况,A线程在执行完+1操作正准备将结果写回到主内存时突然被挂起了。这时B线程也执行完了+1操作并将结果写回了主内存,在通知其他线程时,由于线程操作太快(纳秒级别),A线程在接到通知之前也将结果写回了主内存将B线程写回的值覆盖了,这时就出现了丢失写值的情况。