如果我们需要深入的了解volatile这个关键字,我们首先需要了解关于Java的内存模型(JMM),所以我们首先来看一下Java的内存模型;

Java的内存区域,也就是JVM运行时候的区域,线程A和线程B都从内存区域读取一个C值为0,他们都会持有C值的一个副本,我们在A线程的修改,B线程是感知不到的,持有的这个副本我们叫做缓存,缓存就相当于CPU的缓存行,这块Java的内存区域是感知不到的,Java的内存模型基本上就是这样;
现在我们来介绍volatile关键字
volatile关键字也叫轻量级锁,为什么是轻量级的,从下面可以看出原因,因为他不保证原子性:
1.保证内存的可见性
2.不保证原子性
3.保证顺序性
现在我们来详细介绍这三个特性
一.内存的可见性
话不多说上代码:`在这里插入代码片
package com.chen;
import java.util.concurrent.TimeUnit;
public class VolatileDemo {
public static void main(String[] args) {
Demo demo = new Demo();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
demo.increment();
System.out.println(Thread.currentThread().getName() +“current number” + demo.anInt);
},“A”).start();
while (demo.anInt == 0){
}
System.out.println("Main thread is stopped");
}
}
class Demo{
int anInt;
public void increment(){
anInt++;
}
}
上面是运行的截图我们会发现主线程迟迟没有退出,就是因为感知不到A线程已经修改了anint的值
我们面对这个问题我们该如何解决呢,最简单的方法就是给anint这个属性加上volatile就可以解决内存的不可见性;
这是我加上volatile之后的运行结果;
二、不保证原子性
原子性,我们可以想成数据库的一个事务,数据库的事务具有ACID四中特性,A就是Atomic原子的意思,原子性说明这个操作是一个整体,不可分割;
那么我为什么说不保证原子性呢!首先我们来看看i++翻译成字节码指令后,指令是怎么样的;
简单的说i++是get add 和put三条指令,所以不具有原子性,我们用代码来验证一下;
public class VolatileDemo {
public static void main(String[] args) {
Demo demo = new Demo();
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j <200 ; j++) {
demo.increment();
}
}).start();
}
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(“demo.anInt=” + demo.anInt);
}
}
class Demo {
volatile int anInt;
public void increment() {
anInt++;
}
}如volatile保证原子性,那么我们可以得出结论,上面20个线程我们每次做自增操作,最好的值应该是4000;
不管你运行多少次代码,你会发现结果都少于4000,也可能真的出现随机时间,最好的结果是4000;
为什么会出现这种情况呢,我们知道volatile保证了内存的可见性,但是volatile在写的时候容易造成写丢失;
我们如何保证原子性呢!简单的方法是使用Atomic类
我们来测试一下:
public class VolatileDemo {
public static void main(String[] args) {
AtomicInteger demo = new AtomicInteger(0);
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j <200 ; j++) {
demo.getAndIncrement();
}
}).start();
}
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(“demo.number” + demo.get());
}
}
最后我们来看看结果
我们发现值果然是按照我们的预期结果来的,那么AtomicInteger是如何保证操作的原子性呢,简单来说就是使用CAS方法,CAS即compare and set比较赋值;我们来看看源码
从这里我们可以看到是操作了一个unsafe的类,this值的是当前对象,valueOffset是对象的地址
使用静态代码块直接初始化;
我们最后来看看compareAndSwapInt的方法
这是一个native方法,说明不是使用JAVA代码实现的,我们来详细解释一下,var5就是直接读取当前内存地址的值,然后使用while又去比较当前地址的值,如果和var5一样,就将值设置成var5+var4,为什么从当前地址获取到值了,又要去和当前地址的值去比较呢?因为在多线程环境下,可能又其他的线程抢到资源修改值,这个时候在次判断就能防止更新错误,使用do while可以保证在这里一直自旋,知道完成我们的修改;
这样保证了原子性,现在我们来讨论他的缺点:
1.首先我们无法解决ABA的问题,就是一个线程将一个值从A状态改到B状态,在从B改回A ,其他的线程无法感知到中间值其实已经被修改过;
2.这里的自增都是使用自旋,自旋如果阻塞,会导致CPU的使用率增大;
3.频繁的比较赋值会浪费时间
最后我们来说如何解决ABA的问题,大多数人应该都使用过MySql的乐观锁,我们在做一个更新的操作的时候,首先会读取,当前行的一个version,然后使用version做为更新条件去更新;
所以在JUC下面又一个AtomicStampedReference类,这个类的修改,每次会同步修改时间戳;
三、顺序性
这就牵扯到一个东西叫做指令重排;
指令重排简单来说就是,程序运行的方式可能不是更具,你编写的顺序来的;
什么时候会发生指令重排呢?就是当我执行的结果的顺序在单线程下,不会影响最后的结果;
比如
int a =5;
int c= 6;
这个赋值指令谁在前在后其实都没有影响;但是又结果依赖就不会发生指令重排;
比如我们都写过单例模式,现在我写一个最简单的单例模式
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo(){
System.out.println(“init------------”);
}
public static SingletonDemo getInstance(){
if (instance == null){
instance = new SingletonDemo();
}
return instance;
}
}
class Test{
public static void main(String[] args) {
for (int i = 0; i <10 ; i++) {
new Thread(()->{
SingletonDemo.getInstance();
}).start();
}
}
}
如果在单线程情况下我们可能就看见一次,但是我们在多线程情况下看看
我们看到构造器打印了多次,最简单的方法当然是在方法上加入synchronized,但是这样会把整个对象都锁住,所以我们可以加入DCL的单例模式
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo(){
System.out.println(“init------------”);
}
public static SingletonDemo getInstance(){
if (instance == null){
synchronized (SingletonDemo.class){
if (instance == null){
instance = new SingletonDemo();
}
}
}
return instance;
}
}
这样,我们可能很多次的运行下都不会出错,首先我们要知道创建一个对象的过程,首先在堆开辟出一片空间,然后初始化,最后引用指向空间,加入初始化在指向空间后面,那么这时候instance就返回了一个null值,因为对象没有完成初始化;
这就是指令重排,那么volatile是如何禁止指令重排呢?他会在volatile修饰的变量的前面加入内存屏障
读是load屏障,保证volatile后面的读不发生指令重排,写是store屏障,保证写前面不发生指令重排!