Java并发编程 - volatile 怎么保障内存可见性 & 防止指令重排序?

简介: Java并发编程 - volatile 怎么保障内存可见性 & 防止指令重排序?

内存可见性

首先,要明确一下这个内存的含义,内存包括共享主存和高速缓存(工作内存),Volatile关键字标识的变量,是指CPU从缓存读取数据时,要判断数据是否有效,如果缓存没有数据,则再从主存读取,主存就不存在是否有效的说法了。而内存一致性协议也是针对缓存的协议。

内存可见性意思是一个CPU核心对数据的修改,对其他CPU核心立即可见,这句话拆开了理解:

1、CPU修改数据,首先是对工作内存的修改,也有人说被volatile修饰的变量不会拷贝副本到工作内存,而是直接修改主存,我觉得这个说法是不对的,CPU对数据的修改总是先修改工作内存,然后再同步回主内存,只不过是对被volatile修饰变量的修改,会立刻同步回主内存,假如只有一个线程修改volatile变量,那么这个变量在工作内存的副本会一直有效,CPU也不会每次修改都从主存读取volatile变量,只是每次修改后都会及时更新主存罢了。

2、对其他核心立即可见,这个的意思是,当一个CPU核心A修改完volatile变量,并且立即同步回主存,如果CPU核心B的工作内存中也缓存了这个变量,那么B的这个变量将立即失效,当B想要修改这个变量的时候,B必须从主存重新获取变量的值。

说了这么多,volatile有什么用呢?哎,这个作用一定要说清楚,不然很容易忘记!

举个例子:


public class VolatileTest implements Runnable {
    static boolean flag = true;
    @Override
    public void run() {
        while (flag) {
        }
        System.out.println("end......");
    }
    public static void main(String[] args) {
        new Thread(new VolatileTest()).start();
        try {
            Thread.sleep(1000);
        }
        catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = false;
        System.out.println("end main......");
    }
}
// 输出
end main......  

上面这个例子,子线程会一直卡住,原因就是flag不具备可见性,主线程和子线程刚开始都缓存了flag,且值是true,后来主线程把flag改成了false,但是子线程并不知道,仅此而已,仅此而已!!!!如果把flag用volatile修改,那么主线修改成false后,子线程再次while循环的时候,就会发现它缓存的flag已经失效了,它会去主存重新读取flag的值。

实现的原理一般都是基于CPU的MESI协议(缓存一致性协议),其中E表示独占Exclusive,S表示Shared,M表示Modify,I表示Invalid,如果一个核心修改了数据,那么这个核心的数据状态就会更新成M,同时其他核心上的数据状态更新成I,这个是通过CPU多核之间的嗅探机制实现的。

但是,这样是否就能保证多线程操作一个共享变量的时候,保证线程安全呢?其实不然,否则我怎么说是仅此而已呢!

volatile限定的是从缓存读取时刻的校验,如果两个CPU同时从各自缓存读取一个变量n=1(此时,变量n在各个CPU缓存上都是有效的),并且同时修改了变量n=n+1,再写回缓存,这个时候n的值等于2,而不是等于3。因此,在多线程操作共享变量(例如:计数器)的时候,正确的方式是使用同步或者Atomic工具类。

 

指令有序性

这个涉及到内存屏障(Memory Barrier),内存屏障有两个能力:

a、就像一套栅栏分割前后的代码,阻止栅栏前后的没有数据依赖性的代码进行指令重排序,保证程序在一定程度上的有序性。

b、强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效,保证数据的可见性。

首先,指令并不是代码行,指令是原子的,通过javap命令可以看到一行代码编译出来的指令,当然,像int i=1;这样的代码行也是原子操作。

在单例模式中,Instance inst = new Instance();   这一句,就不是原子操作,它可以分成三步原子指令:

  1. 分配内存地址
  2. new一个Instance对象
  3. 将内存地址赋值给inst

CPU为了提高执行效率,这三步操作的顺序可以是123,也可以是132,如果是132顺序的话,当把内存地址赋给inst后,inst指向的内存地址上面还没有new出来单例对象,这时候,如果就拿到inst的话,它其实就是空的,会报空指针异常。这就是为什么双重检查单例模式中,单例对象要加上volatile关键字。

 

内存屏障有三种类型和一种伪类型

a、lfence:即读屏障(Load Barrier),在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据,以保证读取的是最新的数据。

b、sfence:即写屏障(Store Barrier),在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存,以保证写入的数据立刻对其他线程可见。

c、mfence,即全能屏障,具备ifence和sfence的能力。

d、Lock前缀:Lock不是一种内存屏障,但是它能完成类似全能型内存屏障的功能。

 

并发三特性总结


image.png

目录
相关文章
|
9月前
|
安全 Java 数据库连接
一把锁的两种承诺:synchronized如何同时保证互斥与内存可见性?
临界区指多线程中访问共享资源的代码段,需通过互斥机制防止数据不一致与竞态条件。Java用`synchronized`实现同步,保证同一时刻仅一个线程执行临界区代码,并借助happens-before规则确保内存可见性与操作顺序,从而保障线程安全。
338 11
|
存储 缓存 Java
【高薪程序员必看】万字长文拆解Java并发编程!(5):深入理解JMM:Java内存模型的三大特性与volatile底层原理
JMM,Java Memory Model,Java内存模型,定义了主内存,工作内存,确保Java在不同平台上的正确运行主内存Main Memory:所有线程共享的内存区域,所有的变量都存储在主存中工作内存Working Memory:每个线程拥有自己的工作内存,用于保存变量的副本.线程执行过程中先将主内存中的变量读到工作内存中,对变量进行操作之后再将变量写入主内存,jvm概念说明主内存所有线程共享的内存区域,存储原始变量(堆内存中的对象实例和静态变量)工作内存。
396 0
|
存储 缓存 Java
【JavaEE】——内存可见性问题
volatile,一个线程读,一个线程写,两个线程互相读,多个线程多把锁
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
256 0
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
997 6
|
设计模式 安全 Java
Java 多线程并发编程
Java多线程并发编程是指在Java程序中使用多个线程同时执行,以提高程序的运行效率和响应速度。通过合理管理和调度线程,可以充分利用多核处理器资源,实现高效的任务处理。本内容将介绍Java多线程的基础概念、实现方式及常见问题解决方法。
521 1
|
存储 缓存 安全
Java内存模型(JMM):深入理解并发编程的基石####
【10月更文挑战第29天】 本文作为一篇技术性文章,旨在深入探讨Java内存模型(JMM)的核心概念、工作原理及其在并发编程中的应用。我们将从JMM的基本定义出发,逐步剖析其如何通过happens-before原则、volatile关键字、synchronized关键字等机制,解决多线程环境下的数据可见性、原子性和有序性问题。不同于常规摘要的简述方式,本摘要将直接概述文章的核心内容,为读者提供一个清晰的学习路径。 ####
309 2
|
缓存 Java 编译器
【多线程-从零开始-伍】volatile关键字和内存可见性问题
【多线程-从零开始-伍】volatile关键字和内存可见性问题
276 1
|
Java 编译器 开发者
深入理解Java内存模型(JMM)及其对并发编程的影响
【9月更文挑战第37天】在Java的世界里,内存模型是隐藏在代码背后的守护者,它默默地协调着多线程环境下的数据一致性和可见性问题。本文将揭开Java内存模型的神秘面纱,带领读者探索其对并发编程实践的深远影响。通过深入浅出的方式,我们将了解内存模型的基本概念、工作原理以及如何在实际开发中正确应用这些知识,确保程序的正确性和高效性。
|
Java 开发者
深入探索Java中的并发编程
本文将带你领略Java并发编程的奥秘,揭示其背后的原理与实践。通过深入浅出的解释和实例,我们将探讨Java内存模型、线程间通信以及常见并发工具的使用方法。无论是初学者还是有一定经验的开发者,都能从中获得启发和实用的技巧。让我们一起开启这场并发编程的奇妙之旅吧!
184 5