Java 谈谈什么是 volatile

本文详细介绍了Java中的volatile关键字,包括它的三大特性:可见性、有序性和不保证原子性。volatile确保了多线程环境中的变量可见性,防止指令重排序,但无法保证原子性。通过例子展示了volatile在原子性方面的不足,并提出了解决方案如使用synchronized、Atomic原子类和Lock锁机制。此外,还讨论了如何通过内存屏障来禁止指令重排,以保证线程安全。

随着互联的飞速发展,互联网公司也越来越高,并发多线程,内存管理,JVM调优等成为面试必问题。

1. 什么是 volatile?

volatile 是 Java 虚拟机提供的轻量级同步机制

他的三大特性:

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

1.1 可见性

在解释可见性之前需要先看一下什么是 JMM?

JMM(Java 内存模型 Java Memory Model,简称 JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(实例字段,静态字段和构成数组对象的元素)的访问方式。

JMM 关于同步的规定:

  1. 线程解锁前,必须把共享变量的值刷新回主内存
  2. 线程加锁前,必须读取主内存的最新值到自己的工作内存
  3. 加锁解锁是同一把锁

JMM 保证:可见性、原子性、有序性

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方成为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作空间,然后对变量进行操作,操作完成再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存储存着主内存中的变量副本拷贝,因此不同的线程无法访问对方的工作内存,此案成间的通讯(传值) 必须通过主内存来完成,其简要访问过程如下图:

可见性:当一个线程修改主物理内存的值再写回主内存时,其他线程能都收到修改数据的通知。

如果看不懂上面的一段话,那么看看代码,然后再会过头看上面的文字。代码演示:

class MyData{
	//第一次不加 ,第二次运行加 volatile 
	int n = 0;
	public void change() {
		this.n=60;
	}
}

/**
 * 1.验证 volatile 的可见性
 * 2.假设 int n = 0; n 变量再之前根本没有添加volatile 关键字修饰,没有可见性
 * @author taotao
 *
 */
public class VolatileDemo {
	public static void main(String[] args) {
		MyData myData = new MyData();
		// 生成线程修改 n 
		new Thread(()-> {
			System.out.println(Thread.currentThread().getName()+" 进入程序");
			try {
				Thread.sleep(3000);//休眠3s
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			myData.change();
			System.out.println(Thread.currentThread().getName()+" 修改数据完成 "+myData.n);
		},"线程1").start();
		
		 while(myData.n==0) {
			 //主线程等待
		 }
		 
		 System.out.println(Thread.currentThread().getName()+" 完成工作,n 最新值" + myData.n);
	}
}

第一次运行结果:

第二次运行结果:

1.2 原子性

操作不可分割,又叫完整性,既某个线程正在做某个具体业务,中间不可以被加塞或者被分割,需要整体完整。要不同时成功,要不同时失败。volatile 是不保证原子性的。

先来代码验证:

class MyData{
	volatile int n = 0;
	// 此时 n 已经加 volatile 关键字,但是 volatile 不保证原子性。
	public void addPulsPuls() {
		n++;
	}
}

/* 验证 volatile 不保证原子性*/
public class VolatileDemo2 {
	public static void main(String[] args) {
		MyData myData = new MyData();
		// 生成 20 个线程 ,同时修改 n 值
		for (int i = 0; i < 20; i++) {
			new Thread(()-> {
				for (int j = 0; j < 1000; j++) {
					myData.addPulsPuls();
				}
			},"线程"+i).start();
		}
		// Java 默认两个线程:main线程和 GC线程
		while(Thread.activeCount()>2) {
			Thread.yield();  //当前线程停止,让出资源
		}
		
		System.out.println(Thread.currentThread().getName()+" 完成工作,n 最新值" + myData.n);
	}
}

第一次运行结果:

第二次运行结果:

根据代码大家可以知道正确的答案应该是1000*20=20000,其原因就是因为 volatile 不保证原子性。

虽然addPlusPuls执行的一行代码,但是使用 javap -c对class文件进行汇编的时候会发现,他的底层执行的是三行代码

附:JVM指令手册

执行 getfield 拿到原始 n
执行 iadd 进行加 1 操作
执行 putfield 写把累加后的值写回

那么,怎么解决这个问题?

  1. 加 synchronized 关键字(有一种杀鸡用牛刀的感觉)
  2. atomic 原子类(小问题,推荐使用)
  3. Lock 锁机制

这次我们使用 atomic 原子包下的 AtomicInteger 来解决这个问题

import java.util.concurrent.atomic.AtomicInteger;

class MyData{
	//使用 AtomicInteger 的原子类
	AtomicInteger ai = new AtomicInteger();
	
	public void addAtomic() {
		ai.getAndIncrement(); // 自增1
	}
}

/* 解决 volatile 原子性的方法 */
public class VolatileDemo2 {
	public static void main(String[] args) {
		MyData myData = new MyData();
		// 生成 20 个线程 ,同时修改 n 值
		for (int i = 0; i < 20; i++) {
			new Thread(()-> {
				for (int j = 0; j < 1000; j++) {
					myData.addAtomic();
				}
			},"线程"+i).start();
		}
		// Java 默认两个线程:main线程和 GC线程
		while(Thread.activeCount()>2) {
			Thread.yield();  //当前线程停止,让出资源
		}
		System.out.println(Thread.currentThread().getName()+" 完成工作,n 最新值" + myData.ai);
	}
}

运行结果:

进阶:为什么 Atomic 能解决原子性的问题?

CAS。CAS 讲解

1.3 有序性

volatile 实现禁止指令重排序的优化,从而避免了多线程环境下程序出现乱序的现象

先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个 CPU 指令,他的作用有两个:

  • 保证特定操作的执行顺序
  • 保证某些变量的内存可见性(利用该特性实现 volatile 的内存可见性)

由于编译器个处理器都能执行指令重排序优化,如果在指令间插入一条 Memory Barrier 则会告诉编译器和 CPU,不管什么指令都不能个这条 Memory Barrier 指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后执行重排序优化。内存屏障另一个作用是强制刷出各种 CPU 缓存数据,因此任何 CPU 上的线程都能读取到这些数据的最新版本。

下面是保守策略下,volatile写插入内存屏障后生成的指令序列示意图:

0e75180bf35c40e2921493d0bf6bd684_th

下面是在保守策略下,volatile读插入内存屏障后生成的指令序列示意图:

21ebc7e8190c4966948c4ef4424088be_th

线程安全性保证

  • 工作内存与主内存同步延迟现象导致可见性问题
    • 可以使用 synchronzied 或 volatile 关键字解决,它们可以使用一个线程修改后的变量立即对其他线程可见
  • 对于指令重排导致可见性问题和有序性问题
    • 可以利用 volatile 关键字解决,因为 volatile 的另一个作用就是禁止指令重排序优化

代码举例:

public void example(){
    int x = 5;     //语句1
    int y = 10;    //语句2
    x = x + 5;     //语句3
    y = x + x;     //语句4
}

观察 example 方法,方法体有四个语句,在多线程环境下,编译器会对这个四条语句执行的顺序进行调整,所以可能的执行顺序是:1->2->3->4;1->3->2>4;2->1->3->4 等。但是不会出现 语句4 在第一位,因为他的执行有依赖条件。正是因为在这种情况下,编译器会自动对代码执行的顺序进行一个优化调整,但是在多线程的情况下我们是希望他根据我们写好的顺序(1-2-3-4)依次执行,换句话说就是禁止编译器进行语句优化调整,也就是 volatile 的第三个特性“禁止指令重排”。

 

<think> 我们之前讨论过CAS,现在用户询问对volatile的理解。根据引用内容,volatile主要涉及内存可见性和禁止指令重排序。我们需要结合引用内容,系统地解释volatile。 注意:引用[1]提到volatile的作用是禁止编译器优化,如缓存到寄存器、常量合并等;引用[2]强调volatile作为触发器,保证读取时能获取最新值;引用[3]说明volatile每次从内存读取;引用[4]提到volatile用于解决工作内存与主内存同步延迟导致的可见性问题。 回答结构: 1. volatile的定义与作用 2. volatile的特性(可见性、禁止重排序) 3. 底层实现机制(内存屏障) 4. 应用场景 5. 与synchronized的区别(根据引用[2]) 6. 注意事项(非原子性) 7. 生成相关问题 注意:按用户要求,公式用$...$或$$...$$,引用标识自然添加。 </think> ### 对 `volatile` 关键字的深度解析 #### 1. **核心定义与作用** `volatile` 是编程语言中的**内存可见性关键字**,其核心作用是: 1. **强制内存可见性**:每次读写操作直接访问主内存,跳过CPU缓存(寄存器/L1-L3 Cache)[^3][^4] ```c volatile int flag = 0; // 所有线程直接读写主存中的flag ``` 2. **禁止指令重排序**:阻止编译器/CPU对操作指令的优化重排[^1][^2] $$ \text{普通变量:} \space \text{Load} \rightarrow \text{Store} \rightarrow \text{Other} \quad \xrightarrow{\text{重排序}} \quad \text{Other} \rightarrow \text{Load} \rightarrow \text{Store} $$ $$ \text{volatile变量:} \space \text{禁止重排} $$ #### 2. **底层实现机制** 通过**内存屏障(Memory Barrier)** 硬件指令实现: | 屏障类型 | 作用 | 对应指令(x86) | |----------------|-------------------------------|----------------| | **Load Barrier** | 刷新工作内存数据 | `lfence` | | **Store Barrier**| 同步工作内存到主存 | `sfence` | | **Full Barrier** | 同时包含Load/Store屏障 | `mfence` | 当访问 `volatile` 变量时: ``` 线程A写操作 → Store Barrier → 同步主存 → Load Barrier → 线程B读操作 ``` 保证写操作**对所有线程立即可见**[^4]。 #### 3. **关键特性** | 特性 | 说明 | |---------------------|----------------------------------------------------------------------| | **可见性** | 线程修改后,其他线程立即看到新值(解决工作内存同步延迟)[^2][^4] | | **禁止重排序** | 编译器和CPU不优化指令顺序(如单例模式的双重检查锁)[^1][^3] | | **非原子性** | 不保证复合操作(如`i++`)的原子性(需配合CAS或锁) | #### 4. **典型应用场景** 1. **状态标志位**(多线程协作) ```java volatile boolean shutdownRequested = false; // 线程A void shutdown() { shutdownRequested = true; } // 线程B while (!shutdownRequested) { ... } // 立即感知修改 ``` 2. **内存映射IO**(硬件寄存器访问) ```c volatile uint32_t *reg = (volatile uint32_t*)0xFFFF0000; *reg = 0x1; // 直接写入设备寄存器,禁用编译器优化[^1] ``` 3. **单例模式双重检查锁**(禁止指令重排) ```java class Singleton { private volatile static Singleton instance; public static Singleton getInstance() { if (instance == null) { // 第一次检查 synchronized (Singleton.class) { if (instance == null) { // 第二次检查 instance = new Singleton(); // volatile禁止重排序 } } } return instance; } } ``` #### 5. **`volatile` vs `synchronized`** | **维度** | `volatile` | `synchronized` | |------------------|-------------------------------------|-------------------------------| | **可见性** | ✅ 保证 | ✅ 保证 | | **原子性** | ❌ 不保证(单操作除外) | ✅ 保证 | | **指令重排序** | ✅ 禁止 | ✅ 禁止(更严格) | | **阻塞** | ❌ 无阻塞 | ✅ 线程阻塞/唤醒 | | **性能** | ⭐ 轻量级(仅内存屏障) | ⚡ 重量级(上下文切换) | > 引用[2]强调:`volatile` 是**轻量级同步触发器**,而 `synchronized` 是**互斥锁**[^2]。 #### 6. **使用注意事项** - **非原子操作风险**: ```java volatile int count = 0; count++; // 实际是3步操作:读取→+1→写入(非原子) ``` 需用 `AtomicInteger`(内部用 `volatile` + CAS)[^4]。 - **过度使用问题**: 频繁访问主存会降低性能(比L1缓存慢100倍以上),仅在必要时使用[^3]。 --- ### 相关问题 1. `volatile` 如何通过内存屏障实现禁止指令重排序?请结合具体CPU架构(如x86/ARM)说明 2. 为什么 `volatile` 不能替代锁?请用复合操作(如 `i++`)的字节码解释原子性问题 3. 在C++中,`volatile` 与 `std::atomic` 有何本质区别?各自适用什么场景? 4. `volatile` 在单例模式的双重检查锁中具体解决了什么问题?若移除它会导致什么后果? 5. JVM的happens-before规则中,`volatile` 变量如何建立跨线程的内存可见性保证?
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值