线程安全--彻底搞懂volatile关键字和synchronized(从偏向锁到重量级锁)

本文详细探讨了Java中的volatile关键字和synchronized机制,从计算机内存模型出发解释了线程不安全的原因,接着介绍了volatile的可见性和有序性保证,以及其在多线程环境下的局限性。此外,文章还分析了synchronized的优化,包括从偏向锁到重量级锁的演进,解释了轻量级锁和重量级锁的区别和应用场景。

1. volatile关键字

对于volatile这个关键字,相信很多朋友都听说过,甚至使用过,这个关键字虽然字面上理解起来比较简单,但是要用好起来却不是一件容易的事。
这篇文章将从多个方面来讲解volatile,让你对它更加理解。

计算机中为什么会出现线程不安全的问题

volatile既然是与线程安全有关的问题,那我们先来了解一下计算机在处理数据的过程中为什么会出现线程不安全的问题。
大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中会涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。
为了处理这个问题,在CPU里面就有了高速缓存(Cache)的概念。当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。
我举个简单的例子,比如cpu在执行下面这段代码的时候,

t = t + 1;

会先从高速缓存中查看是否有t的值,如果有,则直接拿来使用,如果没有,则会从主存中读取,读取之后会复制一份存放在高速缓存中方便下次使用。之后cup进行对t加1操作,然后把数据写入高速缓存,最后会把高速缓存中的数据刷新到主存中。

这一过程在单线程运行是没有问题的,但是在多线程中运行就会有问题了。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的,本次讲解以多核cup为主)。这时就会出现同一个变量在两个高速缓存中的不一致问题了。
例如:
两个线程分别读取了t的值,假设此时t的值为0,并且把t的值存到了各自的高速缓存中,然后线程1对t进行了加1操作,此时t的值为1,并且把t的值写回到主存中。但是线程2中高速缓存的值还是0,进行加1操作之后,t的值还是为1,然后再把t的值写回主存。
此时,就出现了线程不安全问题了。

Java中的线程安全问题

上面那种线程安全问题,可能对于不同的操作系统会有不同的处理机制,例如Windows操作系统和Linux的操作系统的处理方法可能会不同。
我们都知道,Java是一种夸平台的语言,因此Java这种语言在处理线程安全问题的时候,会有自己的处理机制,例如volatile关键字,synchronized关键字,并且这种机制适用于各种平台。
Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。
由于java中的每个线程有自己的工作空间,这种工作空间相当于上面所说的高速缓存,因此多个线程在处理一个共享变量的时候,就会出现线程安全问题。

这里简单解释下共享变量,上面我们所说的t就是一个共享变量,也就是说,能够被多个线程访问到的变量,我们称之为共享变量。在java中共享变量包括实例变量,静态变量,数组元素。他们都被存放在堆内存中。

volatile关键字

上面扯了一大堆,都没提到volatile关键字的作用,下面开始讲解volatile关键字是如何保证线程安全问题的。

可见性

什么是可见性?

意思就是说,在多线程环境下,某个共享变量如果被其中一个线程给修改了,其他线程能够立即知道这个共享变量已经被修改了,当其他线程要读取这个变量的时候,最终会去内存中读取,而不是从自己的工作空间中读取。
例如我们上面说的,当线程1对t进行了加1操作并把数据写回到主存之后,线程2就会知道它自己工作空间内的t已经被修改了,当它要执行加1操作之后,就会去主存中读取。这样,两边的数据就能一致了。
假如一个变量被声明为volatile,那么这个变量就具有了可见性的性质了。这就是volatile关键的作用之一了。

volatile保证变量可见性的原理

当一个变量被声明为volatile时,在编译成会变指令的时候,会多出下面一行:

0x00bbacde: lock add1 $0x0,(%esp);

这句指令的意思就是在寄存器执行一个加0的空操作。不过这条指令的前面有一个lock(锁)前缀。
当处理器在处理拥有lock前缀的指令时:
在之前的处理中,lock会导致传输数据的总线被锁定,其他处理器都不能访问总线,从而保证处理lock指令的处理器能够独享操作数据所在的内存区域,而不会被其他处理所干扰。
但由于总线被锁住,其他处理器都会被堵住,从而影响了多处理器的执行效率。为了解决这个问题,在后来的处理器中,处理器遇到lock指令时不会再锁住总线,而是会检查数据所在的内存区域,如果该数据是在处理器的内部缓存中,则会锁定此缓存区域,处理完后把缓存写回到主存中,并且会利用缓存一致性协议来保证其他处理器中的缓存数据的一致性。

缓存一致性协议

刚才我在说可见性的时候,说“如果一个共享变量被一个线程修改了之后,当其他线程要读取这个变量的时候,最终会去内存中读取,而不是从自己的工作空间中读取”,实际上是这样的:
线程中的处理器会一直在总线上嗅探其内部缓存中的内存地址在其

<think>好的,用户之前问过Java的异常体系,现在又问volatilesynchronized的区别。这明他们可能在学习Java并发相关的知识,或者在实际开发中遇到了多线程的问题。需要先确定用户的基础,但根据之前的回答,他们可能已经有一定的Java基础,所以这次要深入浅出地解释这两个关键字的区别。 首先,用户可能知道synchronized是用来同步的,但volatile可能不太清楚。需要明确两者的不同点,比如可见性、原子性、使用场景等。同时,用户可能想知道在什么情况下应该用哪一个,或者两者的组合使用。需要举一些例子,比如单例模式中的双重检查定,这样用户更容易理解。 另外,用户可能对内存屏障、指令重排这些底层概念不太熟悉,所以需要用更易懂的语言解释,避免过于技术化。还要注意结构清晰,分点明区别,可能用表格对比会更直观。最后,要提醒用户常见的误区最佳实践,帮助他们避免错误使用。</think>以下是Java中`volatile`与`synchronized`的核心区别解析: --- ### 一、本质区别对比表 | **对比维度** | `volatile` | `synchronized` | |-------------------|---------------------------------------------|-----------------------------------------| | **作用范围** | 变量级别 | 代码块/方法级别 | | **内存可见性** | 保证可见性(直接读写主内存) | 保证可见性(解前同步到主内存) | | **原子性** | 不保证原子性 | 保证操作的原子性 | | **指令重排序** | 禁止指令重排序(内存屏障) | 临界区内代码有序执行 | | **阻塞机制** | 非阻塞 | 阻塞(线程竞争) | | **性能消耗** | 低(仅内存操作) | 高(上下文切换开销) | | **使用场景** | 状态标志位、双重检查 | 需要原子性操作的代码区域 | --- ### 二、内存可见性原理 #### 1. `volatile`的内存语义 ```java // volatile变量写入时会立即刷新到主内存 public class Counter { private volatile int count = 0; public void increment() { count++; // 非原子操作,存在线程安全问题! } } ``` - **写操作**:在变量修改后立即将值同步到主内存 - **读操作**:每次读取都直接从主内存获取最新值 #### 2. `synchronized`的可见性保证 ```java public class SafeCounter { private int count = 0; public synchronized void increment() { count++; // 原子操作 } } ``` - 进入同步块时清空工作内存 - 退出同步块时将修改刷回主内存 --- ### 三、原子性差异演示 #### 典型反例:volatile不保证原子性 ```java volatile int num = 0; // 多线程并发执行时会出现计数错误 void unsafeAdd() { num++; // 实际是read-modify-write三步操作 } ``` #### synchronized保证原子性 ```java int num = 0; // 线程安全的计数 synchronized void safeAdd() { num++; } ``` --- ### 四、禁止指令重排序 #### volatile的内存屏障(Memory Barrier) ```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禁止new操作的重排序 } } } return instance; } } ``` - **写屏障**:确保在volatile写之前的操作不会被重排序到写之后 - **读屏障**:确保在volatile读之后的操作不会被重排序到读之前 --- ### 五、性能对比测试 | **操作类型** | 执行时间(纳秒/op) | |-------------------|--------------------| | volatile读 | ~5-10 | | synchronized块 | ~20-100 | | CAS操作 | ~10-20 | *(数据基于x86架构测试环境)* --- ### 六、使用场景指南 #### 优先使用`volatile`的情况 1. 状态标志位(单写多读场景) ```java volatile boolean shutdownRequested; void shutdown() { shutdownRequested = true; } void doWork() { while (!shutdownRequested) { // 执行任务 } } ``` 2. 双重检查定模式(需配合volatile) #### 必须使用`synchronized`的情况 1. 复合操作(check-then-act) ```java // 不安全的实现 if (!vector.contains(element)) { vector.add(element); } // 正确的同步实现 synchronized(vector) { if (!vector.contains(element)) { vector.add(element); } } ``` 2. 需要保证原子性的代码块 --- ### 七、常见误区警示 1. **错误认知**:认为`volatile`可以替代`synchronized` - 事实:`volatile`仅解决可见性问题,不解决原子性问题 2. **危险代码示例**: ```java volatile int a = 0, b = 0; void unsafeMethod() { a++; // 非原子操作 b = a + 1; // 可能使用过期的a值 } ``` 3. **正确组合使用**: ```java // 使用volatile作为轻量级状态标志 volatile boolean initialized = false; // 使用synchronized保证初始化原子性 synchronized void init() { if (!initialized) { // 初始化代码... initialized = true; } } ``` --- ### 八、底层实现原理 | **机制** | `volatile` | `synchronized` | |-------------------|-------------------------------------|-------------------------------------| | **字节码层面** | ACC_VOLATILE标志 | monitorenter/monitorexit指令 | | **CPU层面** | 内存屏障指令(如mfence) | 总线或缓存(MESI协议) | | **JVM优化** | 无优化 | 升级(偏向->轻量->重量) | --- 掌握二者的核心区别,可以更精准地选择线程安全方案:需要简单可见性控制时用`volatile`,需要原子性保证时用`synchronized`,在高并发场景下可结合`java.util.concurrent`包中的更高效工具类。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值