[Java]volatile关键字

简介: 本文介绍了Java中volatile关键字的原理与应用,涵盖JMM规范、并发编程的三大特性(可见性、原子性、有序性),并通过示例详细解析了volatile如何实现可见性和有序性,以及如何结合synchronized、Lock和AtomicInteger确保原子性,最后讨论了volatile在单例模式中的经典应用。

【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
https://developer.aliyun.com/article/1631540
出自【进步*于辰的博客

启发博文:《Java volatile关键字最全总结:原理剖析与实例讲解(简单易懂)》(转发)。

参考笔记二,P73、P74.1;笔记三,P61。

在学习此关键字之前,我们先了解一下JMM规范和并发编程中的三个概念。

1、JMM规范

JMM(Java module memory,Java内存模型)是一个抽象概念,并不真实存在于内存。它是用于定义程序中各个变量(成员变量、类变量、数组元素等)的一组规范和规则,指定变量的访问方式。

规定

  1. 线程解锁之前必须将共享变量刷新回主内存。
  2. 线程加锁之前必须读取主内存中变量的最新值到工作空间。
  3. 解锁和加锁必须是同一把锁。

大家可能不解其意,这就需要涉及另一个概念:线程空间.。

什么是线程空间?程序执行JMM规范的实体是线程,当线程创建时,JMM会为其创建一个私有内存(也称为工作内存、本地内存或栈空间)。JMM规定所有变量都保存在主内存,线程访问变量时需为变量创建一个副本至工作内存进行操作,完成后将变量值返回主内存,且线程通信在主内存进行。

注意:JMM作为抽象概念,规定中的“主内存”与“私有内存”等概念同样是抽象概念,并不一定真实对应CPU中的缓存和物理内存。

2、并发编程的三个概念

1:可见性
可见性指线程对变量的修改,其他线程可见,即由volatile修饰的变量,其私有内存失效。(具体说明见下文)

2:原子性
原子性指线程对变量的操作的整个过程不会被阻塞或分割。

原子性表示“拒绝多线程并发操作”,即同一时刻只能有一个线程进行操作。因此,在整个操作过程中不会被线程调度中断。

如:a = 1是原子操作,而a++不是,因为它分为读取、计算和赋值三个步骤。

在Java中,原子操作包括:

  1. 基本数据类型的读取与赋值,但限于将值赋给变量,变量间赋值不是原子操作。
  2. 引用赋值。
  3. java.util.concurrent.atomic包中所有类的一切操作。

3:有序性
有序性也称为“指令重排”,指程序运行时,编译器基于提高性能需要,以指令间的数据依赖性作为依据对指令进行重新排列。执行顺序:编译器重排 → 指令并行重排 → 内存系统重排

单线程环境下,无论指令如何重排,结果都不变,但多线程时可能会出现问题。

3、volatile

3.1 介绍

volatile是一种轻量级的同步机制,与synchronized有一些通性,但synchronized属于重量级(“级”是指对变量访问的限制程度)。

volatile遵循JMM规范实现了可见性和有序性,但不保证原子性。因此,限制线程在访问由volatile修饰的变量时,从主内存获取数据,而不是从工作内存。在数据操作完成后再刷新回主内存,故在保证原子性的情况下,线程安全。

如何保证原子性?
两种方法:

  1. 程序中不存在多线程对变量进行非原子性操作。
  2. 见下文。

    3.2 volatile原理

    在JVM底层,volatile是采用“内存屏障”来实现的。在所生成的汇编代码中可见,在volatile前多出了一条Lock前缀指令,这相当于内存屏障(也称为“内存栅栏”),其提供三项功能:
  3. 屏蔽指令重排。
  4. 强制私有内存的修改立即写入主内存。
  5. 执行写操作时,致使CPU中其他线程的私有内存无效。

这就是为何volatile可实现可见性和有序性,但不保证原子性的原因。

3.3 如何保证原子性?

大家先看个示例。

static volatile int x = 0;
private static void add() {
   
    x++;
}

public static void main(String[] args) throws Exception {
   
    Thread t1 = new Thread(() -> {
   
        int i = 100000;
        while (i-- > 0) {
   
            add();
        }
    });
    Thread t2 = new Thread(() -> {
   
        int i = 100000;
        while (i-- > 0) {
   
            add();
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(x);
}

请问最后的x200000吗?绝大概率不是,因为volatile不能保证原子性,而x++又不是原子操作,可谓必然会出现并发问题。

那么,volatile如何保证原子性?从上文【volatile原理】可得,volatile本身是无法保证原子性的,故需要采取其他方案。如下:

1:synchronized。

static int x = 0;
private synchronized static void add() {
   
    x++;
}

public static void main(String[] args) throws Exception {
   
    Thread t1 = new Thread(() -> {
   
        int i = 100000;
        while (i-- > 0) {
   
            add();
        }
    });
    Thread t2 = new Thread(() -> {
   
        int i = 100000;
        while (i-- > 0) {
   
            add();
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(x);
}

2:Lock。

static int x = 0;
// Lock的原理类似synchronized
static Lock lock = new ReentrantLock();

private static void add() {
   
    lock.lock();// 可视为内存屏障”,当然实际不是
    x++;
    lock.unlock();
}

public static void main(String[] args) throws Exception {
   
    Thread t1 = new Thread(() -> {
   
        int i = 100000;
        while (i-- > 0) {
   
            add();
        }
    });
    Thread t2 = new Thread(() -> {
   
        int i = 100000;
        while (i-- > 0) {
   
            add();
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(x);
}

3:AtomicInteger。

// 原子操作类是通过CAS循环的方式来保证原子性
static AtomicInteger x = new AtomicInteger();

private static void add() {
   
    x.getAndIncrement();
}

public static void main(String[] args) throws Exception {
   
    Thread t1 = new Thread(() -> {
   
        int i = 100000;
        while (i-- > 0) {
   
            add();
        }
    });
    Thread t2 = new Thread(() -> {
   
        int i = 100000;
        while (i-- > 0) {
   
            add();
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(x);
}

4、volatile的一个经典运用

从文章《[Java]单例模式》中截取这段代码:

public static Singleton newInstance() {
   
    if (instance == null) {
   --------------------A
        synchronized (Singleton.class) {
   -------B
            if (instance == null) {
   ------------C
                instance = new Singleton();----D
            }
        }
    }
    return instance;---------------------------E
}

DCL,可解决“懒汉式”存在的线程安全问题。不过,仍有不足,问题在于D。

因为,实例化分为三步:

  1. 创建实例,分配内存。
  2. 实例初始化。
  3. instance指向此实例。

其中,2和3都依赖于1,而2与3之间没有依赖关系,故指令重排可能会将2与3调换。

调换有什么后果?假设一种情况,线程x调用newInstance(),执行D,但还未进行实例初始化(已执行了1、3),此时线程y调用newInstance(),判断A为false,直接执行E,此时返回的instance未初始化,导致异常。

异常出现的原因就是指令重排,用volatile禁止指令重排即可解决(用volatile修饰instance)。

最后

本文中的例子,是为了阐述volatile关键字和方便大家理解而简单举出的,不一定有实用性,仅是抛砖引玉。

本文完结。

目录
相关文章
|
10天前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
Java 并发编程——volatile 关键字解析
|
10天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
11天前
|
缓存 安全 Java
Java volatile关键字:你真的懂了吗?
`volatile` 是 Java 中的轻量级同步机制,主要用于保证多线程环境下共享变量的可见性和防止指令重排。它确保一个线程对 `volatile` 变量的修改能立即被其他线程看到,但不能保证原子性。典型应用场景包括状态标记、双重检查锁定和安全发布对象等。`volatile` 适用于布尔型、字节型等简单类型及引用类型,不适用于 `long` 和 `double` 类型。与 `synchronized` 不同,`volatile` 不提供互斥性,因此在需要互斥的场景下不能替代 `synchronized`。
2100 3
|
2月前
|
JavaScript 前端开发 Java
java中的this关键字
欢迎来到我的博客,我是瑞雨溪,一名热爱JavaScript与Vue的大一学生。自学前端2年半,正向全栈进发。若我的文章对你有帮助,欢迎关注,持续更新中!🎉🎉🎉
55 9
|
2月前
|
设计模式 JavaScript 前端开发
java中的static关键字
欢迎来到瑞雨溪的博客,博主是一名热爱JavaScript和Vue的大一学生,致力于全栈开发。如果你从我的文章中受益,欢迎关注我,将持续分享更多优质内容。你的支持是我前进的动力!🎉🎉🎉
56 8
|
2月前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
49 4
|
3月前
|
Java 程序员
在Java编程中,关键字不仅是简单的词汇,更是赋予代码强大功能的“魔法咒语”。
【10月更文挑战第13天】在Java编程中,关键字不仅是简单的词汇,更是赋予代码强大功能的“魔法咒语”。本文介绍了Java关键字的基本概念及其重要性,并通过定义类和对象、控制流程、访问修饰符等示例,展示了关键字的实际应用。掌握这些关键字,是成为优秀Java程序员的基础。
36 3
|
3月前
|
算法 Java
在Java编程中,关键字和保留字是基础且重要的组成部分,正确理解和使用它们
【10月更文挑战第13天】在Java编程中,关键字和保留字是基础且重要的组成部分。正确理解和使用它们,如class、int、for、while等,不仅能够避免语法错误,还能提升代码的可读性和执行效率。本指南将通过解答常见问题,帮助你掌握Java关键字的正确使用方法,以及如何避免误用保留字,使你的代码更加高效流畅。
46 3
|
3月前
|
存储 安全 Java
了解final关键字在Java并发编程领域的作用吗?
在Java并发编程中,`final`关键字不仅用于修饰变量、方法和类,还在多线程环境中确保对象状态的可见性和不变性。本文深入探讨了`final`关键字的作用,特别是其在final域重排序规则中的应用,以及如何防止对象的“部分创建”问题,确保线程安全。通过具体示例,文章详细解析了final域的写入和读取操作的重排序规则,以及这些规则在不同处理器上的实现差异。
了解final关键字在Java并发编程领域的作用吗?
|
3月前
|
Java 编译器
在Java中,关于final、static关键字与方法的重写和继承【易错点】
在Java中,关于final、static关键字与方法的重写和继承【易错点】
36 5