转载

Java关键字之volatile

首先,volatile是什么?他是Java提供的一个内置的关键字。被此关键字修饰的变量有两种特性

  • 变量对所有的线程是可见的。即有线程A和B,存在被volatile修饰的关键字temp,当线程A对temp进行修改之后,修改之后的值在对线程B而言,是可见的,即线程B中获取到的值是最新的。
  • 被volatile修饰的变量还可以避免指令重排序。在编译之后,会为该变量建立一个内存屏障,即内存屏障后面的代码不能提前到内存屏障前面执行。 这里需要先弄懂两个知识点:JMM内存结构和指令重排序

JMM内存模型

请注意,JMM内存结构并不是指JVM虚拟的内存结构,及堆栈等。

Java关键字之volatile

(画的有点丑,将就看一下)

每次线程读取数据的时候,并不是直接去主内存中读取,会先在工作内存中去查找,变量是否存在,存在的话就直接使用,如果不存在才会去主内存中查找。在Java的内存模型中定义了一下八种操作来完成变量从主存拷贝到工作内存,从工作内存写回到主存。

  • lock:作用于主内存的变量,他把一个变量标识为线程独占状态
  • unlock:作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  • read:作用于主内存,把一个变量的值从主内存传输到工作内存,以便随后的load使用
  • load:作用于工作内存,把read操作从主内存得到的变量值放入到工作内存变量副本中
  • use:作用于工作内存,把工作内存的一个变量的值传递给一个执行引擎,每当虚拟机遇到一个需要使用
  • assign:赋值,作用于工作内存的变量。他把一个从执行引擎接收到的值赋值给工作内存的变量
  • store:作用于工作内存的变量,他把工作内存中的一个变量的值传送到主内存中
  • write:作用于主内存中,把store操作从工作内存中获取到的值写入到主内存

Java虚拟机对volatile关键字有一些特殊的规则。

  • 只有当线程对变量执行的前一个动作是load的时候,线程才能对变量执行use操作。并且只有当线程对变量的吼一个操作是use的时候,线程才能只能load操作。(总的理解就是load和use之间必须连续出现,中间不允许插入其他的指令,这个规则要求在工作内存中,每次使用变量前,都需要先去主存中查找,刷新变量的值)
  • 只有当线程对变量的执行的前一个动作是assign的时候,线程才能对变量执行store,只有当线程的后一个动作是store的时候,才能对变量执行assign。(其实与上一条类似,只不过这个是正对与写,上一个是针对读)
  • 假设动作A和B均是对变量执行use操作,P和Q都是对变量执行load操作(A和P是一组操作,B和Q是一组操作)。如果存在有A比B先执行,那么一P比Q先执行。这个可以实现下面提到的防止指令重排序。

指令重排序

现代的操作系统为了对程序的运行进行优化,会在不改变程序运行的结果下,改变线程内部代码的执行顺序。例如以下代码

int i = 1;
int j = 2;
int z = 3;
int x = i + j;
复制代码

在类似于这种情况下,如果根据程序代码的顺序,i会先存储,然后再读取,在进行运算。指令重排序后,可能会是i和j在z之后定义,这样可以减去一次加载变量的过程,会在一定程度上提高效率(当然,这只是一个简单的例子,还有很多更复杂的情况)

为什么

为什么需要这么一个关键字呢?当使用多线程的时候,难免会出现线程安全问题,即A线程修改的变量的值,B线程中使用该变量的时候,获取到的值并不是最新的,导致B线程处理的数据成为脏数据,导致一些未知的情况。

可能有的朋友会说为什么不使用锁呢。可以说关键字volatile是Java虚拟机提供的最轻量的一个同步机制相对于synchronized而言,是更轻一点的锁。synchronized在只有一个线程的时候,是轻量级锁,当出现多个线程同时竞争的时候,会变成重量级锁,会消耗较多的系统资源。

怎么做

我们以一个单例模式的代码来举例说明。

public class Singleton {

    private volatile static Singleton singleton;

    private Singleton() {

    }

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

    public static void main(String[] args) {
        Singleton.getInstance();
    }

}
复制代码

这是一个很简单的懒汉式单例模式。当多个线程同时去获取的时候,只有一个线程能够创建这个对象。如果我们不添加volatile,则在线程创建好一个对象,释放锁之后,同时在等待获取锁的对象就会获取锁,然后判断,发现已经创建,然后再释放,其他竞争线程也是类似的。(这里的竞争线程是指与创建对象的那个线程同时竞争锁的线程,由于获取锁失败,会等待)。如果添加了volatile,会在使用的时候重新去加载这个对象,发现已经初始化,则放弃获取锁,减少了锁的竞争。

有什么问题

在学习了Java的内存模型之后,我们发现其实Java内存模型就是在围绕着并发过程中如何处理原子性、可见性和有序性。那volatile都可以保证这些特性吗?

  • 原子性:原子性是指一次操作不能再细分。Java内存模型中的read、load、assign、use、store和write都可以保证原子性。那volatile可以保证原子性吗?答案是不可以。例如以下代码
volatile int i = 1;
i++;
复制代码

对于这个来讲,是很常见的一个例子,由于i++他不是一个原子性操作,他会先读取,然后再运算,最后赋值。如果他想要实现原子性操作的话,需要借助于其他的锁机制。

  • 可见性:可见性是指当一个线程修改了共享变量的值之后,其他线程能够立即得知这个修改。上文也讲解到了volatile是可以支持可见性的。
  • 有序性:即是否与代码定义的顺序一致(说的比较书面)。之前也提到volatile可以实现指令重排,保证线程执行的有序性。
原文  https://juejin.im/post/5de708afe51d45583034f77a
正文到此结束
Loading...