当前位置: 首页 > news >正文

Java 并发编程:volatile 关键字介绍与使用

大家好,我是栗筝i,这篇文章是我的 “栗筝i 的 Java 技术栈” 专栏的第 026 篇文章,在 “栗筝i 的 Java 技术栈” 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验,并希望进一步完善自己对整个 Java 技术体系来充实自己的技术栈的同学。与此同时,本专栏的所有文章,也都会准备充足的代码示例和完善的知识点梳理,因此也十分适合零基础的小白和要准备工作面试的同学学习。当然,我也会在必要的时候进行相关技术深度的技术解读,相信即使是拥有多年 Java 开发经验的从业者和大佬们也会有所收获并找到乐趣。

在现代多线程编程中,确保数据的一致性和正确性是至关重要的。Java 作为一种广泛使用的编程语言,为多线程编程提供了丰富的工具和机制,其中 volatile 关键字是一个关键的概念。volatile 关键字在 Java 中被用来修饰变量,以确保它们在多线程环境下的可见性和有序性,但它并不保证操作的原子性。

理解 volatile 的工作原理及其应用场景,对于编写高效和可靠的多线程程序至关重要。在本文中,我们将深入探讨 volatile 关键字的核心特性,解释它如何确保变量的可见性和有序性,以及它在解决多线程问题中的局限性。我们还将通过示例展示如何在实际编程中使用 volatile,以及如何通过其他同步机制来弥补 volatile 的不足。

通过对 volatile 的详细分析,我们希望读者能够更好地理解在多线程环境中变量访问的复杂性,并掌握在实际开发中如何正确使用 volatile 关键字,以编写出更加健壮和高效的并发程序。


文章目录

      • 1、volatile 关键字简介
      • 2、volatile 保证可见性
        • 2.1、什么是可见性问题
        • 2.2、volatile 如何保证可见性
      • 3、volatile 保证有序性
        • 3.1、什么是指令重排序
        • 3.2、volatile 如何保证有序性
      • 4、volatile 不保证原子性的详细介绍
        • 4.1、什么是原子性问题
        • 4.2、volatile 的局限性
        • 4.3、解决方法


1、volatile 关键字简介

volatile 关键字在 Java 中用于修饰变量,使其具有可见性和有序性。

  • 可见性:在多线程环境下,当一个线程修改了 volatile 变量的值,新值对于其他线程是立即可见的。通常情况下,线程之间对变量的读写操作是不可见的,这意味着一个线程修改了变量的值,另一个线程可能看不到这个修改,仍然使用旧值。使用 volatile 关键字可以确保所有线程看到的是变量的最新值;
  • 有序性:volatile 关键字还可以防止指令重排序优化。编译器和处理器通常会对指令进行重排序,以提高性能,但这种重排序可能会破坏多线程程序的正确性。volatile 变量的读写操作不会被重排序,也不会与前后的读写操作发生重排序。

需要注意的是 volatile 仅能保证可见性和有序性,不能保证原子性。例如,volatile int count 的递增操作 count++ 仍然不是线程安全的,因为它包含了读和写两个操作,可能会被其他线程打断。

在复杂的同步场景中,可能需要使用 synchronized 或其他并发工具来确保线程安全。


2、volatile 保证可见性

在多线程编程中,线程之间共享变量的访问可能会出现可见性问题,即一个线程对变量的修改可能不会被其他线程立即看到。Java 提供了 volatile 关键字来解决这种可见性问题。

2.1、什么是可见性问题

当一个线程修改了某个变量的值,如果这个修改对其他线程是不可见的,可能会导致程序出现非预期的行为。例如,一个线程修改了变量 flag 的值,但其他线程仍然读取的是旧值:

public class VisibilityProblem {private boolean flag = true;public void stop() {flag = false;}public void run() {while (flag) {// 执行任务}}
}

在这个例子中,如果 flag 变量没有被声明为 volatile,当一个线程调用 stop 方法将 flag 设置为 false 后,另一个正在运行 run 方法的线程可能无法立即看到这个变化,仍然会在 while (flag) 循环中继续执行。

2.2、volatile 如何保证可见性

volatile 关键字通过以下机制确保变量的可见性:

  1. 内存可见性协议:

    • 每个线程都有自己的本地缓存,当一个线程对变量进行读写操作时,实际上是从本地缓存中读取或写入的,而不是直接操作主内存中的变量。
    • 当一个变量被声明为 volatile 时,所有线程对该变量的读写操作都将直接操作主内存,而不是使用本地缓存。
    • 当一个线程修改了 volatile 变量的值,这个新值会立即刷新到主内存中。
    • 任何线程在读取 volatile 变量时,都会从主内存中读取最新的值,而不是从本地缓存中读取旧值。
  2. 内存屏障:

    • volatile 关键字在底层实现中,会在变量的读写操作前后插入内存屏障(Memory Barrier)。
    • 内存屏障确保了指令的执行顺序,防止编译器和处理器对 volatile 变量的读写操作进行重排序。
    • 写内存屏障:确保在写 volatile 变量之前的所有写操作都已经完成,并且结果对其他线程可见。
    • 读内存屏障:确保在读 volatile 变量之后的所有读操作都能读取到最新的值。

示例代码:

public class VolatileExample {private volatile boolean running = true;public void stop() {running = false;}public void run() {while (running) {// 执行任务}}public static void main(String[] args) {VolatileExample example = new VolatileExample();Thread thread = new Thread(example::run);thread.start();try {Thread.sleep(1000); // 让线程运行一段时间} catch (InterruptedException e) {e.printStackTrace();}example.stop(); // 停止线程}
}

在这个例子中,running 变量被声明为 volatile,确保 stop 方法对 running 的修改能够立即被 run 方法中的循环检测到。


3、volatile 保证有序性

在多线程编程中,指令重排序(Instruction Reordering)可能会导致程序的执行顺序与代码的书写顺序不一致,从而引发不可预测的问题。volatile 关键字通过内存屏障(Memory Barrier)机制,防止指令重排序,确保代码执行的有序性。

3.1、什么是指令重排序

为了优化程序的执行速度,编译器和处理器会对指令进行重排序。重排序包括以下三种类型:

  1. 编译器重排序:编译器在生成机器指令时,可以重新安排代码的执行顺序。
  2. 处理器重排序:处理器可以在运行时对指令进行重排序,以充分利用处理器流水线。
  3. 内存系统重排序:由于缓存、写缓冲区等原因,内存操作的顺序可能与程序代码的顺序不同。

尽管重排序不会改变单线程程序的语义,但在多线程环境下,重排序可能会导致线程间的操作顺序不一致,从而引发数据竞争和线程安全问题。

3.2、volatile 如何保证有序性

volatile 关键字通过插入内存屏障,确保指令的执行顺序。内存屏障是一种同步机制,防止特定类型的指令在重排序时被移动到屏障的另一侧。volatile 变量的读写操作前后会插入内存屏障,确保有序性:

  1. 写内存屏障(Store Barrier):在写 volatile 变量之前插入,确保在此屏障之前的所有写操作都已完成,并且结果对其他线程可见;
  2. 读内存屏障(Load Barrier):在读 volatile 变量之后插入,确保在此屏障之后的所有读操作能读取到最新的值。

具体而言,volatile 保证了以下两点:

  1. volatile 变量之前的所有写操作不会被重排序到 volatile 写之后;
  2. volatile 变量之后的所有读操作不会被重排序到 volatile 读之前。

示例代码:

public class VolatileOrderingExample {private volatile boolean flag = false;private int a = 0;public void writer() {a = 1;         // 写普通变量flag = true;   // 写volatile变量}public void reader() {if (flag) {    // 读volatile变量int i = a; // 读普通变量// `i` 将是 1,因为 `flag` 为 true 时,`a` 必定已经被写为 1}}
}

在这个例子中,writer 方法中对 a 的写操作不会被重排序到 flag 之后,因此在 reader 方法中,一旦检测到 flagtrue,就能确保读取到的 a 的值是最新的 1


4、volatile 不保证原子性的详细介绍

在多线程编程中,volatile 关键字可以保证变量的可见性和有序性,但不能保证操作的原子性。原子性(Atomicity)指的是操作在执行过程中不可分割,要么全部执行,要么全部不执行。

4.1、什么是原子性问题

在多线程环境下,非原子操作可能会导致数据不一致。例如,自增操作 i++ 看似简单,但它实际上由三步组成:

  1. 读取变量 i 的当前值;
  2. i 的值加 1;
  3. 将新值写回 i

这三步操作在多线程环境下可能会被打断,从而导致数据竞争问题。假设两个线程同时执行 i++ 操作:

  1. 线程 A 读取 i 的值为 5。
  2. 线程 B 读取 i 的值为 5。
  3. 线程 A 将 i 的值加 1 并写回,i 的值变为 6。
  4. 线程 B 将 i 的值加 1 并写回,i 的值变为 6。

最终结果是,虽然两个线程都执行了 i++ 操作,但 i 的值只增加了 1。这就是因为 i++ 操作不是原子的。

4.2、volatile 的局限性

volatile 仅能确保变量的可见性和有序性,但不能确保操作的原子性。换句话说,使用 volatile 修饰的变量虽然可以在多个线程之间及时同步,但多个线程对该变量的复合操作(如自增、自减)仍然会存在数据竞争问题。

以下是一个例子,说明了 volatile 不保证原子性的问题:

public class VolatileNonAtomic {private volatile int count = 0;public void increment() {count++;}public static void main(String[] args) throws InterruptedException {VolatileNonAtomic example = new VolatileNonAtomic();Runnable task = () -> {for (int i = 0; i < 1000; i++) {example.increment();}};Thread t1 = new Thread(task);Thread t2 = new Thread(task);t1.start();t2.start();t1.join();t2.join();System.out.println("Final count: " + example.count);}
}

在这个例子中,尽管 count 变量被声明为 volatile,但由于 increment 方法中的 count++ 操作不是原子的,最终的 count 值可能小于 2000。

4.3、解决方法

为了确保操作的原子性,可以使用以下方法:

  1. 使用 synchronized 关键字:将操作包装在同步块中,确保操作的原子性。

    public class SynchronizedExample {private int count = 0;public synchronized void increment() {count++;}
    }
    
  2. 使用原子类:Java 提供了 java.util.concurrent.atomic 包中的原子类(如 AtomicIntegerAtomicLong)来确保操作的原子性。

    import java.util.concurrent.atomic.AtomicInteger;public class AtomicExample {private AtomicInteger count = new AtomicInteger(0);public void increment() {count.incrementAndGet();}
    }
    

相关文章:

Java 并发编程:volatile 关键字介绍与使用

大家好&#xff0c;我是栗筝i&#xff0c;这篇文章是我的 “栗筝i 的 Java 技术栈” 专栏的第 026 篇文章&#xff0c;在 “栗筝i 的 Java 技术栈” 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验&#xff0c;并希望进…...

【Spark计算引擎----第三篇(RDD)---《深入理解 RDD:依赖、Spark 流程、Shuffle 与缓存》】

前言&#xff1a; &#x1f49e;&#x1f49e;大家好&#xff0c;我是书生♡&#xff0c;本阶段和大家一起分享和探索大数据技术Spark—RDD&#xff0c;本篇文章主要讲述了&#xff1a;RDD的依赖、Spark 流程、Shuffle 与缓存等等。欢迎大家一起探索讨论&#xff01;&#xff0…...

四、日志收集loki+ promtail+grafana

一、简介 Loki是受Prometheus启发由Grafana Labs团队开源的水平可扩展&#xff0c;高度可用的多租户日志聚合系统。 开发语言: Google Go。它的设计具有很高的成本效益&#xff0c;并且易于操作。使用标签来作为索引&#xff0c;而不是对全文进行检索&#xff0c;也就是说&…...

xdma的linux驱动编译给arm使用(中断检测-测试程序)

1、驱动链接 XDMA驱动源码官网下载地址为&#xff1a;https://github.com/Xilinx/dma_ip_drivers 下载最新版本的XDMA驱动源码&#xff0c;即master版本&#xff0c;否则其驱动用不了&#xff08;xdma ip核版本为4.1&#xff09;。 2、驱动 此部分来源于博客&#xff1a;xd…...

探索之路——初识 Vue Router:构建单页面应用的完整指南

目录 1. Vue Router 简介 2. 安装与配置 Vue Router 安装步骤 配置路由 3. 在 Vue 应用中使用路由 4. 进阶使用 路由守卫 懒加载 高级路由技术 嵌套路由 动态路由匹配 编程式的路由导航 路由懒加载 路由元信息 在现代前端开发中,单页面应用(SPA)因其出…...

传输层_计算机网络

文章目录 运输层UDPTCPTCP连接管理TCP三次握手TCP四次挥手 可靠机制流量控制拥塞控制 QUIC 运输层 网络层提供了主机之间的逻辑通信 运输层为运行在不同主机上的进程之间提供了逻辑通信 UDP(用户数据报协议)提供一种不可靠、无连接的服务&#xff0c;数据报 TCP(传输控制协议)…...

自动驾驶的六个级别是什么?

自动驾驶汽车和先进的驾驶辅助系统&#xff08;ADAS&#xff09;预计将帮助拯救全球数百万人的生命&#xff0c;消除拥堵&#xff0c;减少排放&#xff0c;并使我们能够在人而不是汽车周围重建城市。 自动驾驶的世界并不只由一个维度组成。从没有任何自动化到完整的自主体验&a…...

深度学习复盘与论文复现F

文章目录 1、Environment construction1.1 macos conda1.2 macos PyTorch1.3 iTerm settings1.4 install jupyter 2、beam search2.1 greedy search2.2 exhaustive search2.3 beam search 3、Attention score3.1 Masking softmax operation3.2 Additive attention3.3 Zoom dot …...

如何学习自动化测试工具!

要学习和掌握自动化测试工具的使用方法&#xff0c;可以按照以下步骤进行&#xff1a; 一、明确学习目标 首先&#xff0c;需要明确你想要学习哪种自动化测试工具。自动化测试工具种类繁多&#xff0c;包括但不限于Selenium、Appium、JMeter、Postman、Robot Framework等&…...

短信接口被恶意盗刷

短信接口被恶意盗刷是指攻击者通过各种手段&#xff0c;大量发送短信请求&#xff0c;导致短信资源被浪费&#xff0c;服务提供商可能面临经济损失&#xff0c;正常用户的服务也可能受到影响。以下是一些可能导致短信接口被恶意盗刷的原因和相应的解决方案&#xff1a; 原因&a…...

实验4-2-1 求e的近似值

//实验4-2-1 求e的近似值 /* 自然常数 e 可以用级数 11/1!1/2!⋯1/n!⋯ 来近似计算。 本题要求对给定的非负整数 n&#xff0c;求该级数的前 n1 项和。 输入格式:输入第一行中给出非负整数 n&#xff08;≤1000&#xff09;。 输出格式:在一行中输出部分和的值&#xff0c;保留…...

内网穿透--LCX+portmap转发实验

实验背景 通过公司带有防火墙功能的路由器接入互联网&#xff0c;然后由于私网IP的缘故&#xff0c;公网 无法直接访问内部web服务器主机&#xff0c;通过内网其它主机做代理&#xff0c;穿透访问内网web 服务器主机 实验设备 1. 路由器、交换机各一台 2. 外网 kali 一台&…...

缓存一致性问题

1. 引言 1.1 数据库与缓存的工程实践 在软件工程领域&#xff0c;数据库&#xff08;Database&#xff09;和缓存&#xff08;Cache&#xff09;是两种常见的数据存储解决方案&#xff0c;它们在系统架构中扮演着至关重要的角色。数据库是数据持久化的后端存储&#xff0c;它…...

【MYSQL】MYSQL逻辑架构

mysql逻辑架构分为3层 mysql逻辑架构分为3层 1). 连接层&#xff1a;主要完成一些类似连接处理&#xff0c;授权认证及相关的安全方案。 2). 服务层&#xff1a;在 MySQL据库系统处理底层数据之前的所有工作都是在这一层完成的&#xff0c;包括权限判断&#xff0c;SQL接口&…...

【Python】数据类型之字符串

本篇文章将继续讲解字符串其他功能&#xff1a; 1、求字符串长度 功能&#xff1a;len(str) &#xff0c;该功能是求字符串str的长度。 代码演示&#xff1a; 2、通过索引获取字符串的字符。 功能&#xff1a;str[a] str为字符串&#xff0c;a为整型。该功能是获取字符…...

c++编写java模式的线程类

在 C11 中&#xff0c;我们可以使用 <thread> 标准库来创建和管理线程。然而&#xff0c;C 不像 Java 那样提供一个内置的 Thread 类&#xff0c;而是提供了一个更底层的 API。下面是一个模拟 Java 中 Thread 类功能的 C11 实现。 我们将创建一个名为 SimpleThread 的类…...

vcpkg install libtorch[cuda] -allow-unsupported-compiler

在vcpkg中不懂如何使用 nvcc 的 -allow-unsupported-compiler, 所以直接注释了CUDA中对版本的检查代码. C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.8\include\crt\host_config.h 奇了怪了,我是用的是vs2022,但是还是被检查为不支持的编译器!!! 可以试一下改这…...

Flink的DateStream API中的ProcessWindowFunction和AllWindowFunction两种用于窗口处理的函数接口的区别

目录 ProcessWindowFunction AllWindowFunction 具体区别 ProcessWindowFunction 示例 AllWindowFunction 示例 获取时间不同&#xff0c;一个数据产生的时间一个是数据处理的时间 ProcessWindowFunction AllWindowFunction 具体示例 ProcessWindowFunction 示例 Al…...

MATLAB中dmperm函数用法

目录 语法 说明 dmperm函数的功能是完成Dulmage-Mendelsohn 分解。 语法 p dmperm(A) [p,q,r,s,cc,rr] dmperm(A) 说明 如果列 j 与行 i 匹配&#xff0c;p dmperm(A) 得到的结果为向量 p&#xff0c;这样 p(j) i&#xff0c;如果列 j 与其不匹配&#xff0c;得到的结…...

苹果折叠屏设备:创新设计与技术突破

本文首发于公众号“AntDream”&#xff0c;欢迎微信搜索“AntDream”或扫描文章底部二维码关注&#xff0c;和我一起每天进步一点点 苹果折叠屏设备&#xff1a;创新设计与技术突破 在科技迅速发展的今天&#xff0c;苹果公司以其一贯的创新精神和对产品质量的严格把控&#x…...

C#加班统计次数

C#加班统计次数 运行环境&#xff1a;vs2022 .net 8.0 社区版 1、用C#语言&#xff1b;2、有界面上传Excel文件; 3、对Excel列&#xff08;部门、人员姓名、人员编号、考勤时间 &#xff09;处理&#xff1a;&#xff08;1&#xff09;按人员编号、考勤日期分组且保留原来字段&…...

【资治通鉴】“ 将欲取之、必先予之 “ 策略 ① ( 魏桓子 割让土地 | 资治通鉴原文分析 | 道德经、周书、吕氏春秋、六韬 中的相似策略 )

文章目录 一、" 将欲取之、必先予之 " 策略1、魏桓子 割让土地2、资治通鉴原文分析 二、" 将欲取之、必先予之 " 类似的原理1、将欲败之&#xff0c;必姑辅之&#xff1b;将欲取之&#xff0c;必姑与之 - 周书2、将欲歙之&#xff0c;必固张之&#xff0c;…...

Spring5 的日志学习

我们在使用 Spring5 的过程中会出现这样的现像&#xff0c;就是 Spring5 内部代码打印的日志和我们自己的业务代码打印日志使用的不是统一日志实现&#xff0c;尤其是在项目启动的时候&#xff0c;Spring5 的内部日志使用的是 log4j2&#xff0c;但是业务代码打印使用的可能是 …...

python爬虫实践

两个python程序的小实验&#xff08;附带源码&#xff09; 题目1 爬取http://www.gaosan.com/gaokao/196075.html 中国大学排名&#xff0c;并输出。提示&#xff1a;使用requests库获取页面的基本操作获取该页面&#xff0c;运用BeautifulSoup解析该页面绑定对象soup&#x…...

【前端面试】七、算法-数组展平

目录 1.判断数组 2.二维数组展平 3.多维数组展平 1.判断数组 // 判断数组console.log([].constructor Array);console.log( Array.isArray([]));console.log( [] instanceof Array);console.log(Object.prototype.toString.call([]) [object Array]); 2.二维数组展平 const…...

Laravel php框架与Yii php 框架的优缺点

Laravel和Yii都是流行的PHP框架&#xff0c;它们各自具有独特的优点和缺点。以下是对这两个框架优缺点的详细分析&#xff1a; Laravel PHP框架的优缺点 优点 1、设计思想先进&#xff1a;Laravel的设计思想非常先进&#xff0c;非常适合应用各种开发模式&#xff0c;如TDD&…...

使用 addRouteMiddleware 动态添加中间

title: 使用 addRouteMiddleware 动态添加中间 date: 2024/8/4 updated: 2024/8/4 author: cmdragon excerpt: 摘要&#xff1a;文章介绍了Nuxt3中addRouteMiddleware的使用方法&#xff0c;该功能允许开发者动态添加路由中间件&#xff0c;以实现诸如权限检查、动态重定向及…...

Zookeeper未授权访问漏洞

Zookeeper未授权访问漏洞 Zookeeper是分布式协同管理工具&#xff0c;常用来管理系统配置信息&#xff0c;提供分布式协同服务。Zookeeper的默认开放端口是 2181。Zookeeper安装部署之后默认情况下不需要任何身份验证&#xff0c;造成攻击者可以远程利用Zookeeper&#xff0c;…...

【JavaEE】定时器

目录 前言 什么是定时器 如何使用java中的定时器 实现计时器 实现MyTimeTask类 Time类中存储任务的数据结构 实现Timer中的schedule方法 实现MyTimer中的构造方法 处理构造方法中出现的线程安全问题 完整代码 考虑在限时等待wait中能否用sleep替换 能否用PriorityBlo…...

2024带你轻松玩转Parallels Desktop19虚拟机!让你在Mac电脑上运行Windows系统

大家好&#xff0c;今天我要给大家安利一款神奇的软件——Parallels Desktop 19虚拟机。这款软件不仅可以让你在Mac电脑上运行Windows系统&#xff0c;还能轻松切换两个操作系统之间的文件和应用程序&#xff0c;让你的工作效率翻倍&#xff01; 让我来介绍一下Parallels Desk…...