360度无死角认识volatile机制|Java 开发实战

简介: 360度无死角认识volatile机制|Java 开发实战

前提概要


我们都知道synchronized关键字的特性:原子性、可见性、有序性、可重入性,虽然,JDK在不断的尝试优化这个内置锁,一文中有提到:无锁 -> 偏向锁 -> 轻量锁 -> 重量锁 一共四种状态,但是,在高并发的情况下且大量冲突出现的时候,最终都还是会膨胀到重量锁


本篇文章主要讲解volatile关键字,它与synchronized 的区别是:volatile 不具备原子性!注:不具备原子性不代表它没有原生性!




为何这么说?


那是因为,synchronized是同步代码块通过monitor监视器,对整个代码块(方法是通过判断 ACC_SYNCHRONZED 标志位对整个方法)进行了整体原子性操作。而 volatile 对单一操作是原子性的,非单一操作则是非原子性的





基本用法


Java语言里的volatile关键字是用来修饰变量的,方式如下入所示。表示:该变量需要直接存储到主内存中

public class SharedClass {
    public volatile int counter = 0;
}
复制代码



被volatile关键字修饰的 int counter 变量会直接存储到主内存中并且所有关于该变量的读操作,都会直接从主内存中读取,而不是直接从CPU缓存。(关于主内存和CPU缓存的区别,如果不理解也不用担心,下面会详细介绍


这么做解决什么问题呢?主要是两个问题:


  • 多线程见可见性的问题
  • CPU指令重排序的问题



注:为了描述方便,我们接下来会把 volatile 修饰的变量简称为“volatile 变量”,把没有用 volatile 修饰的变量建成为“non-volatile”变量。





理解 volatile 关键字


变量可见性问题(Variable Visibility Problem) : volatile可以保证变量变化在多线程间的可见性


一个多线程应用中,出于计算性能的考虑,每个线程默认是从主内存将该变量拷贝到线程所在CPU的缓存中,然后进行读写操作的。现在电脑基本都是多核CPU,不同的线程可能运行的不同的核上,而每个核都会有自己的缓存空间。如下图所示(图中的 CPU 1,CPU 2 大家可以直接理解成两个核)

image.png

这里存在一个问题,JVM既不会保证什么时候把 CPU 缓存里的数据写到主内存,也不会保证什么时候从主内存读数据到 CPU 缓存。也就是说,不同 CPU 上的线程,对同一个变量可能读取到的值是不一致的,这也就是我们通常说的:线程间的不可见问题


比如下图,Thread 1 修改的 counter = 7 只在 CPU 1 的缓存内可见,Thread 2 在自己所在的 CPU 2 缓存上读取 counter 变量时,得到的变量 counter 的值依然是 0。

image.png


而volatile出现的用意之一,就是要解决线程间不可见性,通过 volatile 修饰的变量,都会变得线程间可见


其解决方式就是文章开头提到的:


  • 通过 volatile 修饰的变量,所有关于该变量的读操作,都会直接从主内存中读取,而不是 CPU 自己的缓存。而所有该变量的写操都会写到主内存上。
  • 因为主内存是所有 CPU 共享的,理所当然即使是不同 CPU 上的线程也能看到其他线程对该变量的修改了。volatile不仅仅只保证 volatile变量的可见性,volatile 在可见性上所做的工作,实际上比保证 volatile 变量的可见性更多


当 Thread A 修改了某个被 volatile 变量 V,另一个 Thread B 立马去读该变量 V。一旦 Thread B 读取了变量 V 后,不仅仅是变量 V 对 Thread B 可见, 所有在 Thread A 修改变量 V 之前 Thread A 可见的变量,都将对 Thread B 可见。


当 Thread A 读取一个 volatile 变量 V 时,所有对于 Thread A 可见的其他变量也都会从主内存中被读取。




特性及原理


可见性


任意一个线程修改了 volatile 修饰的变量,其他线程可以马上识别到最新值。实现可见性的原理如下。


  • 步骤 1:修改本地内存,强制刷回主内存。

image.png


步骤 2:强制让其他线程的工作内存失效过期。(此部分更多的属于MESI协议)


image.png



单个读/写具有原子性


单个volatile变量的读/写(比如 vl=l)具有原子性,复合操作(比如 i++)不具有原子性,Demo 代码如下:


public class VolatileFeaturesA {
   private volatile long vol = 0L;
    /**
     * 单个读具有原子性
     * @date:2020 年 7 月 14 日 下午 5:02:38
     */
    public long get() {
        return vol;
    }
    /**
     * 单个写具有原子性
     * @date:2020 年 7 月 14 日 下午 5:01:49
     */
    public void set(long l) {
        vol = l;
    }
    /**
     * 复合(多个)读和写不具有原子性
     * @date:2020 年 7 月 14 日 下午 5:02:24
     */
    public void getAndAdd() {
        vol++;
    }
}
复制代码




互斥性


同一时刻只允许一个线程操作 volatile 变量,volatile 修饰的变量在不加锁的场景下也能实现有锁的效果,类似于互斥锁。上面的 VolatileFeaturesA.java 和下面的 VolatileFeaturesB.java 两个类实现的功能是一样的(除了 getAndAdd 方法)。


public class VolatileFeaturesB {
  private volatile  long vol = 0L;
    /**
     * 普通写操作
     * @date:2020 年 7 月 14 日 下午 8:18:34
     * @param l
     */
    public synchronized void set(long l) {  
        vol = l;
    }
    /**
     * 加 1 操作
     * @author songjinzhou
     * @date:2020 年 7 月 14 日 下午 8:28:25
     */
    public void getAndAdd() {
        long temp = get();
        temp += 1L;
        set(temp);
    }
    /**
     * 普通读操作
     * @date:2020 年 7 月 14 日 下午 8:33:00
     * @return
     */
    public synchronized long get() {
        return vol;
    }
}
复制代码




部分有序性


JVM 是使用内存屏障来禁止指令重排,从而达到部分有序性效果,看看下面的 Demo 代码分析自然明白为什么只是部分有序

//a、b 是普通变量,flag 是 volatile 变量
int a = 1;            //代码 1
int b = 2;            //代码 2
volatile boolean flag = true;  //代码 3
int a = 3;            //代码 4
int b = 4;            //代码 5
复制代码


因为 flag 变量是使用 volatile 修饰,则在进行指令重排序时,不会把代码 3 放到代码 1 和代码 2 前面,也不会把代码 3 放到代码 4 或者代码 5 后面。 但是指令重排时代码 1 和代码 2 顺序、代码 4 和代码 5 的顺序不在禁止重排范围内,比如:代码 2 可能会被移到代码 1 之前。


内存屏障类型分为四类。


  1. LoadLoadBarriers

指令示例:LoadA —> Loadload —> LoadB

此屏障可以保证 LoadB 和后续读指令都可以读到 LoadA 指令加载的数据,即读操作 LoadA 肯定比 LoadB 先执行

  1. StoreStoreBarriers



指令示例:StoreA —> StoreStore —> StoreB

此屏障可以保证 StoreB 和后续写指令可以操作 StoreA 指令执行后的数据,即写操作 StoreA 肯定比 StoreB 先执行

  1. LoadStoreBarriers



指令示例: LoadA —> LoadStore —> StoreB

此屏障可以保证 StoreB 和后续写指令可以读到 LoadA 指令加载的数据,即读操作 LoadA 肯定比写操作 StoreB 先执行

  1. StoreLoadBarriers



指令示例:StoreA —> StoreLoad —> LoadB

此屏障可以保证 LoadB 和后续读指令都可以读到 StoreA 指令执行后的数据,即写操作 StoreA 肯定比读操作 LoadB 先执行。



实现有序性的原理:


如果属性使用了 volatile 修饰,在编译的时候会在该属性的前或后插入上面介绍的 4 类内存屏障来禁止指令重排,比如


  • volatile 写操作的前面插入 StoreStoreBarriers 保证volatile写操作之前的普通读写操作执行完毕后再执行 volatile 写操作。
  • volatile 写操作的后面插入 StoreLoadBarriers 保证 volatile 写操作后的数据刷新到主内存,保证之后的 volatile 读写操作能使用最新数据(主内存)。
  • volatile 读操作的后面插入 LoadLoadBarriersLoadStoreBarriers 保证 volatile 读写操作之后的普通读写操作先把线程本地的变量置为无效,再把主内存的共享变量更新到本地内存,之后都使用本地内存变量



volatile 读操作内存屏障:

image.png


volatile 写操作内存屏障:

image.png


状态标志,比如布尔类型状态标志,作为完成某个重要事件的标识,此标识不能依赖其他任何变量,Demo 代码如下:

public class Flag {
    //任务是否完成标志,true:已完成,false:未完成
    volatile boolean finishFlag;
    public void finish() {
        finishFlag = true;
    }
    public void doTask() { 
        while (!finishFlag) { 
            //keep do task
        }
    }
复制代码



一次性安全发布,比如:著名的 double-checked-locking,demo 代码上面已贴出。 开销较低的读,比如:计算器,Demo 代码如下。

/**
 * 计数器
 */
public class Counter {
    private volatile int value;
    //读操作无需加锁,减少同步开销提交性能,使用 volatile 修饰保证读操作的可见性,每次都可以读到最新值 
    public int getValue() {
        return value; 
    }
    //写操作使用 synchronized 加锁,保证原子性
    public synchronized int increment() {
        return value++;
    }
}





相关文章
|
9天前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的服装商城管理系统
基于Java+Springboot+Vue开发的服装商城管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的服装商城管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
31 2
基于Java+Springboot+Vue开发的服装商城管理系统
|
6天前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的大学竞赛报名管理系统
基于Java+Springboot+Vue开发的大学竞赛报名管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的大学竞赛报名管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
20 3
基于Java+Springboot+Vue开发的大学竞赛报名管理系统
|
7天前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的蛋糕商城管理系统
基于Java+Springboot+Vue开发的蛋糕商城管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的蛋糕商城管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
20 3
基于Java+Springboot+Vue开发的蛋糕商城管理系统
|
7天前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的美容预约管理系统
基于Java+Springboot+Vue开发的美容预约管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的美容预约管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
21 3
基于Java+Springboot+Vue开发的美容预约管理系统
|
9天前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的房产销售管理系统
基于Java+Springboot+Vue开发的房产销售管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的房产销售管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
24 3
基于Java+Springboot+Vue开发的房产销售管理系统
|
10天前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的反诈视频宣传系统
基于Java+Springboot+Vue开发的反诈视频宣传系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的反诈视频宣传管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
41 4
基于Java+Springboot+Vue开发的反诈视频宣传系统
|
7天前
|
存储 网络协议 Java
Java NIO 开发
本文介绍了Java NIO(New IO)及其主要组件,包括Channel、Buffer和Selector,并对比了NIO与传统IO的优势。文章详细讲解了FileChannel、SocketChannel、ServerSocketChannel、DatagramChannel及Pipe.SinkChannel和Pipe.SourceChannel等Channel实现类,并提供了示例代码。通过这些示例,读者可以了解如何使用不同类型的通道进行数据读写操作。
Java NIO 开发
|
10天前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的医院门诊预约挂号系统
基于Java+Springboot+Vue开发的医院门诊预约挂号系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的门诊预约挂号管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
31 2
基于Java+Springboot+Vue开发的医院门诊预约挂号系统
|
2天前
|
Java 程序员 开发者
深入理解Java中的异常处理机制
【9月更文挑战第31天】在Java编程中,异常处理是维护程序健壮性的关键。本文将通过浅显易懂的语言和生动的例子,带你了解Java异常处理的基本概念、分类以及如何优雅地处理它们。从初学者到资深开发者,每个人都能从中获得新的洞见和技巧,让你的代码更加健壮和易于维护。
10 4
|
1天前
|
Java 编译器 开发者
Java中的异常处理机制:从基础到进阶
本文深入探讨Java编程语言中的异常处理机制,从基础知识出发,逐步解析异常的分类、捕获和处理方法。通过实际案例分析,展示如何在开发过程中有效利用异常处理提高代码的稳定性和可维护性。进一步探讨了自定义异常的创建和使用场景,以及在Java中进行异常处理的最佳实践。文章旨在为Java开发者提供一个全面而详细的异常处理指南,帮助开发者更好地理解和运用Java的异常处理机制。
下一篇
无影云桌面