JMM:Java内存模型
要想学习volatile,就不得不了解JMM。JVM运行程序的实体是线程,每个线程在被创建时JVM都会为其创建一个自己私有的工作内存。而Java内存模型规定所有的变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但是线程对这些变量的操作只能在自己的工作内存中进行,不能直接操作主内存中的变量,要先将变量从主内存中拷贝到线程自己的工作内存中再对其进行操作,操作完成后再将操作后的变量写回主内存当中,因此不同的线程也无法访问对方的工作内存,线程间的通信必须通过主内存来完成。
关于JMM需要知道的点:
-
工作内存和Java内存模型并不是真实存在于java虚拟机中,而是一种规范和定义。
-
共享变量存储于主内存之中,每个线程都可以访问。这里的变量指的是实例变量和类变量。局部变量是线程私有的,不存在共享。
-
每个线程都有私有的工作内存或者称为本地内存,工作内存存储的是共享变量的副本。
-
线程不能直接操作主内存,只有先操作了工作内存之后才能写入主内存。
-
不同的线程不能直接访问对方工作内存中的变量,线程间变量的值传递需借助主内存作为中转来完成。
volatile
volatile是java虚拟机提供的轻量级同步机制。其三大特性为保证可见性、不保证原子性、保证有序性(禁止指令重排)。
volatile保证可见性
如果A线程和B线程同时获取主内存中的同一个变量,之后A线程修改了这个变量,但是此时B线程并不知道A线程已经对数据进行了修改,所以要具有可见性让线程之间进行通讯。当线程A修改完以后线程B也能知道此时该变量的值已经变为A修改后的数据,实现可见性
public class VolatileTest {
public static void main(String[] args) throws InterruptedException {
Demo demo = new Demo();
new Thread(() -> {
System.out.println(Thread.currentThread());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
demo.add();
}).start();
while (demo.i == 0) {
//
}
}
}
class Demo {
int i = 0;
public void add() {
i ++;
}
}
volatile不保证原子性
volidate保证有序性(即:禁止指令重排序)
volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。
如果不使用volatile,在多线程环境中线程交替执行,由于编译器优化重排,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
计算机在执行程序时,为了提高性能,编译器和处理器通常会对指令做出重排。为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
总结
适用场景
- 某个属性被多个线程共享,其中一个线程修改了此属性,其他线程可以立即获得修改后的值,比如线程循环标识boolean flag;
- volatile还可以用于单例模式,可以解决单例双重检查对象初始化代码执行乱序问题。
public class Singleton {
private volatile static Singleton singleton = null;
public Singleton() {
System.out.println(Thread.currentThread().getName() + "生成singleton");
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
上述单例模式中使用了双重检验,如果不用volatile关键字修饰属性又会怎样?
单线程情况下,属性singleton不加volatile关键字也不会出现任何问题;但是多线程情况下,会出现指令重排序的情况,就有可能出现空指针问题。首先需要了解的是对象创建包含下面三个过程:1.分配内存空间;2.调用构造函数,初始化对象;3.返回地址给引用。由于步骤2和步骤3不存在数据依赖关系,而且无论是重排前还是重排后的执行结果在单线程中并没有发生改变,因此这种重排优化是允许的。若此时先执行步骤3,步骤2还未执行完,另一个线程来执行if (singleton == null)会返回false,此时对象未完全生成,是个半成品,当访问对象方法或属性时,就会抛出空指针异常。使用volatile避免指令重排序,同时保证写回主存中的对象只有一个,实现真正意义上的单例。