【并发编程】(一)Java并发机制的底层实现原理——volatile关键字

volatile是java的三个关键字之一,实现了可见性和有序性。
其可见性是基于java内存模型JMM实现的,原理为写操作之后将线程的本地内存刷新到主内存,读操作将本地内存的缓存置为无效;
至于其有序性是通过控制编译器和处理器的指令重排序实现的:对于volatile变量的写,happends-before于对此变量的读。

volatile定义

Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。

在多线程编程中synchronize和volatile都扮演着重要的角色,volatile是轻量级的synchronize,在某些情况下比锁更方便。

Java并发编程出现问题的根源就在于三个方面:

  • 原子性:一个或者一系列操作是不可被中断的(java中的实现有:锁、循环CAS、JUC中提供的并发类)
  • 可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
  • 有序性:通过禁止进某种类型的重排序,让一个操作执行的结果对另一个操作可见;(有的资料中也会统一地称此特性为可见性)

volatile关键字实现了其中的:可见性、有序性。

volatile——可见性

CPU级别volatile可见性

例如:

instance = new Singleton();//instance是volatile变量

将其转变为汇编码:

0x01a3de1d: movb $0x0,0x1104800(%esi);0x01a3de24: lock add1 $0x0,(%esp);

当有 volatile修饰的变量进行写操作 时,将会多出一行LOCK指令的编码,这个指令所做的事情:

  1. 将当前处理器缓存行的数据写回到系统内存
  2. 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效

JMM级别volatile可见性

java内存模型

我们知道线程之间的通信机制有两种:共享内存和消息传递,java并发采用的是 共享内存 模型。

在java中所有的实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享。
因此共享内存问题主要指这部分的数据。java线程之间的通信由JMM控制,它决定了一个线程对共享变量的写入何时对另一个线程可见。

JMM抽象结构示意图如下。
在这里插入图片描述
JMM定义了线程和主内存之间的抽象关系:

  • 线程之间的共享变量存储在主内存中
  • 每个线程都有一个私有的本地内存,本地内存中存储了该线程以读写共享变量的副本。
    本地内存是JMM定义的一个抽象概念,其实是涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化

volatile内存语义

  • volatile写 的内存语义:
    当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。

  • volatile读 的内存语义:
    当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。

volatile——有序性

重排序与Happens-before规则

在执行程序时,为了提高性能,编译器和处理器通常会对指令做 重排序
重排序分为三种类型:

重排序步骤类型JMM对此排序的策略
1. 编译器优化的重排序编译器禁止特定类型的编译器排序
2. 指令级并行的重排序处理器插入指定类型的内存屏障
3. 内存系统的重排序处理器插入指定类型的内存屏障

从JDK5开始,java使用新的JSR-133内存模型,它使用 happens-before 的概念来阐述两个操作之间的有序性。
这里的两个操作,既可以是指同一个线程内,也可以是多线程之间。

  • 我们在此只关注其中的volatile变量规则:
    对一个volatile域的写,happends-before于任意后续对这个volatile域的读。
    此规则的实现也分为了两个部分:JMM针对于编译器制定的volatile重排序规则、基于保守策略的JMM内存屏障插入策略。

happens-before规则定义

JMM通过happens-before关系向程序员提供跨线程的内存可见性及有序性保证。

如果A线程的写操作a与B线程的读操作b之间存在着happens-before关系,尽管a和b在不同的线程中执行,但JMM向程序员保证:

  1. a写操作将对b读操作可见。
  2. a写操作的执行顺序将排在b读操作之前
  3. 并不意味着java平台的具体实现要按照happens-before关系指定的顺序来执行,只要重排序之后的执行结果是一致的,这种重排序是允许的。
    与as-if-serial语义类似:保证单线程内程序的执行结果不被改变,happens-before保证了多线程的结果不被改变

happens-before规则意义

一个happens-before规则对应了多个编译器和处理器的重排序规则,对于程序员的意义在于,避免了我们去学习具体的重排序规则与可见性的保证,取而代之只需要理解其提供的几个简单易懂的规则。

附.volatile应用——DCL双重检查锁单例模式

我们从头开始实现几种单例模式,循序渐进分析volatile在DCL中所发挥的作用。

1.饿汉模式

public class Singleton{
   
    private static final Singleton instance = new Singleton();
    
    private Singleton(){}
    
    public static Singleton getInstance(){
        return instance;
    }
}

这种方式的单例对象在类加载时就被初始化,当然不存在线程安全的问题。
但如果我们希望在用到此实例的时候再进行对象的创建和初始化呢?

2. 懒汉模式

public class Singleton {
    
    private static Singleton instance;
    
    private Singleton (){}
    
    public static Singleton getInstance() {
         if (instance == null) {
             instance = new Singleton();
         }
     return instance;
    }
}

这种方式解决了上述问题,但是也触发了线程安全问题:
如果线程A和线程B同时请求了getInstance方法,那么它们将都进行new Singleton()对象的创建,不能保证单例模式。

3. 线程安全懒汉模式

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

与第2种方式的区别就在于,getInstance方法之前加上了synchronized关键字。
这种方式的问题:如果有大量线程同时请求,那么将串行地执行getInstance方法,进行大量不必要的等待。

4. 双重锁校验懒汉模式

public class Singleton {
    
    private static Singleton instance;
    
    private Singleton (){}
    
    public static Singleton getSingleton() {
        if (instance == null) { //1                        
            synchronized (Singleton.class) {//2
                if (instance == null) {//3          
                    instance = new Singleton();
                }
            }
        }
        return instance ;
    }
}

与第3种方式的区别为:synchronized关键字并非加到了整个方法上,而是方法内。

  • 当instance不为空时,线程直接跳出了代码1的判断返回实例;
  • 当instance为空时,假如线程A和线程B同时通过了代码1的判断,将串行地执行代码2中的片段;
  • 假如线程A先进入同步区,通过new Singleton()创建实例;
  • 此时线程B再进入,通过代码3的double check,保证了不会重复地创建实例,而是直接返回已有实例;

但是,这种方式也并不是完美的,如果我们考虑JVM的指令重排序的情况的话。

instance = new Singleton()这行代码,其实在JVM中并不是一个原子性的操作,而是有3个步骤:

  1. 给instance分配内存空间
  2. new Singleton()初始话实例
  3. 将创建的实例对象指向分配的内存空间;

当线程A进入在执行这个操作时,如果指令被重排序为1-3-2,那么在执行完3时instance就不再是null

线程B此时如果正在进行代码1中(instance == null)的判断,将会直接返回一个未初始化完成的instance,导致错误。

5. 双重锁校验懒汉模式+volatile关键字

public class Singleton {
    
    private static volatile Singleton instance;
    
    private Singleton (){}
    
    public static Singleton getSingleton() {
        if (instance == null) { //1                        
            synchronized (Singleton.class) {//2
                if (instance == null) {//3          
                    instance = new Singleton();
                }
            }
        }
        return instance ;
    }
}

与方式4唯一的区别就是给instance变量增加了一个volatile关键字修饰。

针对这种情况,我们不必关注它是如何进行指令的排序的,JMM为我们提供了一个happens-before原则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。

所以这种方式中,instance被volatile修饰,那么线程A对于instance赋值,一定是先于线程B对于instance的读取操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值