之前看关于volatile的文章好多都没有讲到JMM,在并发编程中了解JMM对我们开发有很大帮助,故自己了总结一下volatile与JMM那密不可分的关系。
Java内存模型(Java Memory Model,JMM)是Java语言中用于描述多线程并发访问共享变量时的规范。它定义了线程如何与主内存和工作内存进行交互,以及对共享变量的访问和操作应该遵循的规则。在此之前,主流程序语言(如 C/C++等)直接使用物理硬件和操作系统的内存模型,因此,会由于不同平台上内存模型的差异,有可能导致程序在一套平台上并发完全正常,而在另外一套平台上并发访问却经常出错,这导致在某些场景下必须针对不同的平台来编写不同的代码。 JMM 屏蔽了不同处理器内存模型的差异,它在不同的处理器平台之上为 Java 程序员呈现了一个一致的内存模型。通过JMM的规范,Java程序员可以利用各种同步机制(如synchronized、volatile等)来控制线程之间的互动和数据共享,从而编写正确且高效的多线程程序。
Java 内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中“将变量存储到内存”和“从内存中取出变量”这样的底层细节。此处的变量与 Java 编程中所说的变量有所区别,在这里变量包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,也就不存在竞争问题。 Java 内存模型规定了所有的变量都存储在主内存中(此处的主内存与介绍物理硬件时的主内存名字一样,两者可以互相类比,但此处仅是虚拟机内存的一部分)。每条线程还有自己的工作内存(Working Memory,可与前面讲的处理器高速缓存类比),线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。 不同的线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者的交互关系如图
**内存间的交互:**一个变量如何从主内存拷贝到工作内存、以及如何从工作内存同步回主内存之类的实现。 Java 内存模型中定义了以下 8 种操作来完成内存间交互操作,虚拟机实现时必须保证下面每一个操作都是原子的、不可再分的(对于 64 位的类型,即 double 和 **long **类型的变量来说允许有例外情况,不过商业虚拟机实现一般可保证其原子性):
**Java **内存模型还规定了执行上述 8 种基本操作时必须满足如下规则:
volatile是Java中的一个关键字,用于修饰变量。它具有以下两个主要作用:
缺点:虽然volatile关键字可以保证可见性和有序性,但它并不能保证原子性。对于复合操作,如i++,volatile关键字无法保证原子性,仍然需要使用其他手段来确保操作的原子性,比如使用synchronized关键字或者使用Atomic类提供的原子操作类。
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。 使用volatile必须具备以下2个条件:
单例作为我们最常用的模式想必大家没少见过下面这种写法,这个方法中我们就有用到volatile关键字。
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
首先,双重检查锁模式通常用于延迟初始化变量或创建单例实例。它通过在加锁前后进行两次检查来减少锁的开销,从而提高性能。 然而,双重检查锁模式在多线程环境下可能会出现问题,其中一个主要问题是指令重排序。在Java中,对象的创建过程可能会发生指令重排序,这可能导致另一个线程获取到尚未完全初始化的对象。这种情况下,当另一个线程访问该对象时,可能会出现异常或不正确的行为。为了解决指令重排序的问题,需要使用volatile关键字。
public class LoopTask extends Thread {
private volatile boolean keepRunning = true;
public void stopLoop() {
keepRunning = false;
}
public void startLoop() {
keepRunning = true;
}
@Override
public void run() {
while (keepRunning) {
// 执行循环任务
}
}
}
在上面的示例中,一个线程通过调用stopLoop方法来修改keepRunning的值为false,从而停止循环。由于keepRunning被声明为volatile,其他线程在读取该变量时会及时看到修改后的值,从而正确地停止循环。
public class SharedData {
private volatile int counter = 0;
private volatile boolean a;
public int getCounter() {
return counter;
}
public void setCounter(int counter) {
this.counter = counter;
}
public boolean isA() {
return a;
}
public void setA(boolean a) {
this.a = a;
}
}
SharedData sharedData = new SharedData();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("getCounter" + sharedData.getCounter());
System.out.println("isA" + sharedData.isA());
}
}
}).start();
在上面的示例中,多个线程可以同时访问counter变量,a变量,但由于它被声明为volatile,任何一个线程对counter的修改或变量a的修改都会立即被其他线程看到。这种写法一般在SDK中比较常见。