java锁基础与实现
概述
Java中的锁是多线程编程的核心机制,用于控制对共享资源的并发访问,确保线程安全(Thread Safety)和数据一致性。主要有两种实现方式:内置锁(synchronized 关键字) 和 显式锁(java.util.concurrent.locks.Lock 接口及其实现类)。根据锁的不同行为特性,可以进行如下分类:
- 独占锁(Exclusive Lock)vs 共享锁(Shared Lock):
- 独占锁: 在任何时刻只允许一个线程访问资源(如 synchronized, ReentrantLock, ReentrantReadWriteLock 的写锁)。
- 共享锁: 允许多个线程同时访问资源(如 ReentrantReadWriteLock 的读锁, Semaphore)。
- 公平锁(Fair Lock)vs 非公平锁(Non-fair Lock):
- 公平锁: 严格按照线程请求锁的顺序来分配锁。
- 非公平锁: 不保证请求顺序,新请求的线程可能插队获取锁(默认情况下性能通常更高)。ReentrantLock 默认是非公平锁。
- 悲观锁(Pessimistic Lock)vs 乐观锁(Optimistic Lock):
- 悲观锁: 认为并发冲突经常发生,因此在访问前就加锁(大多数Java锁如 synchronized 和 ReentrantLock 属于此类)。
- 乐观锁: 认为冲突很少发生,不加锁,而是在更新时检查是否有冲突(如使用CAS操作实现的原子类 AtomicInteger 或 StampedLock 的乐观读)。
- 可重入锁(Reentrant Lock)vs 不可重入锁(Non-reentrant Lock): 允许持有锁的线程再次获取该锁而不会死锁(Java中 synchronized, ReentrantLock 都是可重入的)。
锁的内存语义
在分析JMM是聊到,Java内存模型始终围绕着如何保证并发的原子性、可见性和有序性。对于java锁来说必须实现互斥性和内存可见性,JJava内存模型围绕这两个核心目标来保证锁的语义:
- 原子性(Mutual Exclusion): 确保同一时刻只有一个线程能够持有同一个锁并执行临界区代码。这是通过监视器(Monitor)机制或AQS(ReentrantLock 等的实现)在JVM层面实现的。
- 可见性(Visibility): 确保一个线程在释放锁时对共享变量的修改,对随后获取同一个锁的另一个线程是可见的。这是通过刷新和加载内存的操作实现的。
具体来说,锁操作具有与 volatile 变量读写相似的内存语义:
- 锁释放(Unlock) 的内存语义:类似于 volatile 写。在释放锁时,JMM 会把该线程工作内存(本地缓存)中的所有共享变量的修改强制刷新到主内存(Main Memory)中。
- 锁获取(Lock Acquire) 的内存语义:类似于 volatile 读。在获取锁时,JMM 会清空当前线程工作内存中该锁保护的共享变量的本地缓存值,强制要求线程从主内存中重新读取这些变量的最新值。
在实现上Java内存模型(JMM)通过定义 happens-before 关系和在底层插入内存屏障(Memory Barrier) 来实现锁的语义(互斥性和内存可见性)。
happens-before 规则
JMM 通过 监视器锁规则(Monitor Lock Rule) 将上述语义形式化,这是实现可见性的关键:对一个监视器锁的解锁操作,happens-before(先行发生)随后对这个锁的加锁操作。 这意味着,如果线程 A 释放了一个锁,而线程 B 随后获取了同一个锁,那么线程 A 在释放锁之前进行的所有操作(包括对共享变量的修改)的效果,对线程 B 都是可见的。底层实现:内存屏障
JVM并不会直接在所有平台上实现一套固定的锁机制,而是利用 内存屏障指令(Memory Barrier/Fence) 来适应不同硬件架构的内存模型。 JMM 保证只要遵守了锁的使用规范(例如,在 finally 块中释放 ReentrantLock),无论底层硬件内存模型是强一致性(如x86)还是弱一致性(如ARM),Java程序都会表现出一致的并发行为。使用内存屏障在JVM层面进行保证:- 在释放锁时,JVM会插入写屏障(Store Barrier),确保屏障前所有的写操作都被刷新到主内存,并且不会被重排序到屏障后。
- 在获取锁时,JVM会插入读屏障(Load Barrier)或全屏障(Full Barrier),清空本地缓存,并防止重排序,确保在临界区内的操作都能看到最新的共享变量值。
总之:JMM 实现锁语义的核心在于结合互斥机制(如监视器)和内存可见性保证(通过 happens-before 规则和内存屏障),确保临界区内的代码能够顺序执行,并且对共享变量的所有修改能在线程间正确同步。
synchronized
synchronized是java内置的锁,Java 中的每个对象都与其关联,用于实现线程的同步。
synchronized使用
synchronized是Java中用于实现线程同步的关键字,用于控制多个线程对共享资源的并发访问,防止数据不一致和线程冲突问题。它主要有三种使用方式:修饰实例方法、修饰静态方法和修饰代码块。
- 修饰实例方法:当synchronized修饰一个普通实例方法时,锁定的是当前实例对象(this) 。这意味着,同一时刻,只有一个线程可以执行该实例的任何一个synchronized实例方法。
1 | |
- 修饰静态方法:当synchronized修饰一个静态方法时,锁定的是当前类的所有实例,即Class对象 。这意味着,同一时刻,无论有多少个该类的实例,都只有一个线程可以执行该类的任何一个synchronized静态方法。
1 | |
修饰代码块:修饰代码块是synchronized提供的更细粒度的控制机制,可以只对方法中的一部分代码进行同步。它需要指定一个锁对象,该对象可以是任意一个非null的对象实例。
- 实例锁:锁定的是指定的对象实例。不同实例的synchronized代码块互不影响。
1
2
3
4
5
6
7
8
9
10
11public class SynchronizedBlockExample {
private final Object lock = new Object();
private int count = 0;
public void performAction() {
synchronized (lock) {
count++;
System.out.println(Thread.currentThread().getName() + " updated, count: " + count);
}
}
}- 类锁:当代码块锁定的对象是Class对象时,它实现了与静态方法一样的效果,即对整个类进行同步。
1
2
3
4
5
6
7
8
9
10public class ClassLockExample {
private static int staticCount = 0;
public void performAction() {
synchronized (ClassLockExample.class) {
staticCount++;
System.out.println(Thread.currentThread().getName() + " static updated, staticCount: " + staticCount);
}
}
}
synchronized 使用小结:
| 类型 | 关键字位置 | 锁定对象 | 影响范围 |
|---|---|---|---|
| 实例方法 | public synchronized void method() |
实例对象(this) |
属于同一实例的所有synchronized实例方法 |
| 静态方法 | public static synchronized void method() |
Class对象(类名.class) |
属于该类的所有synchronized静态方法 |
| 代码块 | synchronized (锁对象) |
指定的锁对象 | 所有在同一锁对象上进行同步的代码块 |
synchronized 实现机制
synchronized 的底层实现主要依赖于Java 对象头中的监视器锁(Monitor),并通过JVM 字节码指令来控制锁的获取和释放。
核心原理:监视器锁(Monitor)
每个Java对象都可以关联一个监视器(Monitor),当一个线程试图获取一个对象的锁时,它就是去竞争这个对象所关联的 Monitor 的所有权。
- 监视器入口(Entry Set):当一个线程试图获取锁时,如果该锁已被其他线程持有,这个线程就会进入 Monitor 的入口集(Entry Set)并阻塞等待。
- 锁的重入性:Monitor 内部有一个计数器。当一个线程成功获取锁后,计数器会加一。如果同一个线程再次进入同步代码块,计数器会继续增加,这就是synchronized可重入的原理。线程每次退出同步代码块时,计数器会减一,直到减为零时,锁才真正被释放。
- 等待队列(Wait Set):当线程获取锁后,如果调用了 wait() 方法,它会释放锁并进入 Monitor 的等待队列。当其他线程调用 notify() 或 notifyAll() 方法时,等待队列中的线程会被唤醒,重新进入入口集去竞争锁。
Monitor依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。每个对象都对应于一个可称为” 互斥锁” 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

每个 Java 对象都可以关联一个监视器(Monitor)。当一个线程试图获取一个对象的锁时,它就是去竞争这个对象所关联的 Monitor 的所有权。那Java对象是如何关联的呢?
在HotSpot虚拟机中,一个Java对象在堆内存中的存储布局主要包括三个部分:对象头(Object Header)、实例数据(Instance Data) 和对齐填充(Padding)。
- 对象头 (Object Header):对象头是每个Java对象都必须有的,用于存储对象自身的运行时数据和类型信息。它主要包含两部分(数组对象有三部分):
- Mark Word (标记字段):这部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志(无锁、偏向锁、轻量级锁、重量级锁)、偏向线程ID、偏向时间戳等。在32位或64位JVM中,其大小和结构会有所不同。
- Klass Pointer (类型指针):这是对象指向其类元数据(在方法区中)的指针,虚拟机通过这个指针来确定该对象是哪个类的实例。
- 数组长度 (Array Length):如果对象是一个Java数组,对象头中还会有一块额外的数据用于记录数组的长度。普通Java对象的大小可以从其元数据中确定,但数组的大小不能,因此需要单独记录。
- 实例数据 (Instance Data)
这部分是真正存储对象有效信息的地方,即代码中所定义的各种字段(成员变量)的数据,包括从父类继承下来的和子类中定义的字段。JVM会根据字段的类型、在代码中的定义顺序以及内存对齐规则进行排列。 - 对齐填充 (Padding)
对齐填充并不是必然存在的,它仅起到占位符的作用。HotSpot虚拟机要求对象的总内存大小必须是8字节的倍数,如果对象头和实例数据加起来的大小不足8字节的倍数,就会通过对齐填充来补齐,以确保对象在内存中是8字节对齐的。
在这里我们只需要关注对象头的Mark Word,以32位JVM Mark Word为例:

字段说明:
- 锁标志位(lock):区分锁状态,11时表示对象待GC回收状态, 只有最后2位锁标识(11)有效。
- biased_lock:是否偏向锁,由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位。
- 分代年龄(age):表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代。
- 对象的hashcode(hash):运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里。当对象加锁后,计算的结果31位不够表示,在偏向锁,轻量锁,重量锁,hashcode会被转移到Monitor中。
- 偏向锁的线程ID(JavaThread):偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作。
- epoch:偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。
- 指向栈中锁记录的指针(ptr_to_lock_record):轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争的时,JVM使用原子操作而不是OS互斥。这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的标题字中设置指向锁记录的指针。
- 指向Montor指针(ptr_to_heavyweight_monitor):重量级锁状态下,指向对象监视器Monitor的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向Monitor的指针。
从Mark Word结构中,当轻量级锁竞争失败后,JVM会为对象创建一个Monitor对象(通常涉及操作系统级别的互斥量 Mutex),这就是synchronized锁。所有竞争的线程都会进入 Monitor 的等待队列并阻塞(涉及用户态到内核态的切换)。
另外,Mark Word通过其内部比特位的重用和状态标志位,实现了JVM对synchronized锁的优化升级路径。这种机制使得JVM可以根据实际的并发竞争情况,动态地调整锁的实现方式,从未竞争时的无开销(偏向锁/无锁)平滑过渡到高竞争时的操作系统级同步(重量级锁),从而在保证线程安全的同时最大化性能。
字节码层面实现
1 | |
- 同步代码块:在编译时,编译器会在同步代码块的入口和出口分别插入 monitorenter 和 monitorexit 这两个字节码指令。
- monitorenter:线程试图获取 Monitor 的所有权。
- monitorexit:线程释放 Monitor 的所有权。为了确保锁在任何情况下(包括发生异常时)都能被释放,编译器会生成两个monitorexit指令,一个用于正常退出,一个用于异常退出。
- 同步方法:synchronized修饰的方法,其字节码中会添加 ACC_SYNCHRONIZED 标志。JVM 在调用这个方法时,会根据这个标志自动处理 Monitor 的获取和释放,无需显式插入 monitorenter 和 monitorexit 指令。

锁升级
Java虚拟机(JVM)中的锁升级过程是其对synchronized关键字进行性能优化的核心机制。为了在线程安全和性能之间找到平衡,JVM(特别是HotSpot虚拟机)引入了四种锁状态,并根据竞争情况进行动态升级。锁的状态从低到高(性能从高到低,竞争程度从低到高)依次是:无锁状态 (Unlocked)、偏向锁 (Biased Locking)、轻量级锁 (Lightweight Locking)、重量级锁 (Heavyweight Locking)。整个升级过程是不可逆的:锁只能从低级状态升级到高级状态,不能降级(重量级锁在释放后可以回到无锁状态,但过程是升级)。
无锁状态 (Unlocked):当对象刚创建时,其对象头中的 Mark Word 处于无锁状态。此时,任何线程都可以访问该对象而无需同步开销。
偏向锁 (Biased Locking):
- 当第一个线程 A 试图获取一个处于无锁状态的对象的锁时,JVM会尝试使用一个CAS(Compare-AndSwap)操作,将线程 A 的ID记录到对象的 Mark Word 中,并把标志位设为偏向锁状态。
- 如果CAS成功,线程 A 获得偏向锁。此后,只要是线程 A 再次访问这个同步块,无需进行任何同步操作,只需简单检查 Mark Word 中的线程 ID 是否是自己。
- 如果另一个线程 B 尝试获取这个锁,偏向锁就会被撤销,并升级到轻量级锁。
轻量级锁 (Lightweight Locking):
- 当线程 B 发现对象处于偏向锁状态(且线程ID不是自己)或无锁状态时,会进入轻量级锁的获取流程。
- 线程 B 会在自己的栈帧中创建一个Lock Record(锁记录),并将对象 Mark Word 的当前值复制进去。
- 线程 B 使用CAS操作尝试将对象的 Mark Word 替换为指向自己栈中 Lock Record 的指针。
- 如果CAS成功,线程 B 获得轻量级锁。
- 如果CAS失败(说明有其他线程同时在竞争,此时可能另一个线程也试图加轻量级锁),锁会膨胀(升级)为重量级锁。
- 线程在释放轻量级锁时,也会尝试使用CAS将Displaced Mark Word(之前复制的原始Mark Word值)写回对象头。如果CAS失败,同样意味着发生了竞争,需要升级到重量级锁。
重量级锁 (Heavyweight Locking):
- 当轻量级锁的 CAS 操作失败时,JVM 会将锁膨胀为重量级锁。
- 重量级锁依赖于操作系统底层的 互斥量(Mutex) 来实现。
- 对象的 Mark Word 会存储一个指向 Monitor(监视器)对象的指针。
- 未能获取锁的线程将被阻塞,并进入 Monitor 的等待队列中,涉及用户态到内核态的切换,这是所有锁状态中开销最大的。
ReentrantLock
ReentrantLock 的实现原理核心在于 AbstractQueuedSynchronizer(AQS) 这个同步器框架。AQS 提供了一套通用的机制,用于构建锁和同步器,而 ReentrantLock 则是基于 AQS 实现的一种独占锁:
- 同步状态(state):volatile int 类型的变量,用于表示同步状态。
- state == 0:表示当前没有线程持有锁。
- state > 0:表示锁被某个线程持有。对于 ReentrantLock,这个值就是重入的次数。
- 独占模式:在 ReentrantLock 中,一次只能有一个线程持有锁,属于独占模式。
- CLH 同步队列(等待队列):双向链表,用于存放因未能获取锁而被阻塞的线程。
- 节点(Node):队列中的每个元素是一个 Node,封装了等待线程、等待状态等信息。
- CAS 操作:所有对 state 变量的修改都是通过 CAS (Compare-And-Swap) 指令来保证原子性的。
1 | |
AQS(AbstractQueuedSynchronizer:抽象队列同步器)
AQS(AbstractQueuedSynchronizer,抽象队列同步器)的实现原理核心是: 一个volatile修饰的整型状态变量(state)、一个基于CAS操作的原子性机制,以及一个先进先出(FIFO)的双向等待队列。 它定义了一套多线程访问共享资源的同步器框架,许多Java并发包中的同步器都基于AQS实现,例如ReentrantLock、Semaphore、CountDownLatch和ReentrantReadWriteLock。
同步状态(state)
AQS内部使用一个volatile int类型的变量state来表示共享资源的状态。volatile保证了state变量在多线程之间的可见性。
子类需要通过getState()、setState()和compareAndSetState()三个方法来操作这个状态变量。不同的同步器对state的定义不同:- ReentrantLock:state表示锁被重入的次数。
- Semaphore:state表示当前可用的许可证数量。
- CountDownLatch:state表示需要倒数的次数。
同步队列(CLH队列)
- 当线程尝试获取资源失败时,AQS会将该线程及其相关信息封装成一个Node节点,并将其加入一个FIFO的双向链表队列中进行阻塞等待。
- 这个队列是CLH(Craig, Landin, Hagersten)队列的变体,它将等待中的线程以节点的形式排队。
- 队列中的Node节点除了包含线程本身,还包含了等待模式(独占/共享)和等待状态(waitStatus) 等信息。
- 使用双向队列的优势在于,当前一个节点释放锁时,可以更高效地唤醒其后继节点。
CAS(比较并交换)机制
- AQS依赖CAS(Compare And Swap)机制来原子性地修改state变量。
- 获取资源过程:线程在获取锁时,会尝试使用CAS操作来修改state。如果修改成功,则表示获取锁成功;如果失败,则表明有其他线程正在竞争,该线程需要进入等待队列。
- 释放资源过程:当持有锁的线程释放资源时,会通过CAS操作来更新state的值。更新成功后,会唤醒同步队列中的下一个等待线程。
ReentrantLock加锁(lock())的实现流程
- 尝试获取锁:
- 线程尝试使用 CAS 操作修改 state 状态。如果 state 为0,则CAS成功,将state设置为1,表示该线程成功获取锁。
- 如果 state 不为0,表明锁已被其他线程持有,或本线程重入。
- 处理重入:
- 如果发现持有锁的线程是当前线程,则将state加1,实现可重入。
- 入队等待:
- 如果尝试获取锁失败(state 不为0,且持有锁的线程不是当前线程),线程会创建一个 Node,将其封装起来,然后通过 CAS 操作将这个节点添加到 CLH 队列的尾部,进入等待状态。
- 线程会进入自旋,并检查自己是否是队列的第一个节点(头节点的下一个)。
- 如果不是,线程会通过 park() 方法挂起自身,进入阻塞状态,等待被唤醒。
- 公平锁与非公平锁:
- 非公平锁(默认):新来的线程在入队前会先尝试 CAS 获取锁。如果锁刚好被释放,这个新线程可能比等待队列中的老线程先抢到锁。这提升了吞吐量,但可能导致队列中的线程饥饿。
- 公平锁:新来的线程会先检查队列是否为空,只有当队列为空时才会尝试获取锁。如果队列不为空,则直接入队等待,严格按照排队顺序获取锁,但性能稍低。
ReentrantLock解锁(unlock())的实现流程
- 修改同步状态:
- 调用 unlock() 方法时,线程会递减 state 的值。
- 释放锁:
- 当 state 的值减为0时,表明锁被完全释放。
- 唤醒等待线程:
- 锁被释放后,会唤醒 CLH 队列中等待时间最长的线程(即队列头节点的后继节点),让它重新尝试获取锁。
- 被唤醒的线程会从 park() 处返回,再次尝试获取锁。
公平锁的获取流程
ReentrantLock 实现公平锁的核心在于 AbstractQueuedSynchronizer(AQS) 中的 CLH 同步队列,以及在尝试获取锁时,对队列是否存在等待者进行额外的检查。公平锁的获取流程与非公平锁的主要区别在于:新来的线程在尝试获取锁时,会先检查 CLH 队列中是否有比自己等待时间更长的线程。
- 调用 lock():线程调用 lock() 方法尝试获取锁。
- 检查等待队列:
- ReentrantLock 的公平模式在内部会调用 AQS 的 hasQueuedPredecessors() 方法。
- 这个方法会检查同步队列中,当前线程之前是否还有等待的线程。
- 判断是否可以获取锁:
- 有等待者:如果 hasQueuedPredecessors() 返回 true(即队列中有比自己先到的等待者),即使当前锁是空闲的,新线程也不会尝试通过 CAS 直接抢占锁。它会老老实实地排队,避免“插队”行为。
- 无等待者:如果 hasQueuedPredecessors() 返回 false,或者当前线程就是队列中的第一个节点,它才会尝试通过 CAS 竞争锁。
- 排队等待:如果无法获取锁(无论是排队还是竞争失败),线程都会被封装成一个 Node,加入到 CLH 队列的尾部,进入等待状态,然后被 park() 挂起。
- 按序唤醒:当持有锁的线程调用 unlock() 释放锁时,会从 CLH 队列中唤醒等待时间最长的那个线程(即队列头部的下一个节点)。被唤醒的线程才能继续尝试获取锁。
ReentrantReadWriteLock
ReentrantReadWriteLock 的实现原理同样基于 AQS(AbstractQueuedSynchronizer),但它利用一个32位的整型变量 state 分别表示读锁和写锁的状态,从而实现了“读-读共享,读-写互斥,写-写互斥”的并发访问控制。ReentrantReadWriteLock 的核心在于如何巧妙地用一个 int 类型的 state 变量来管理两种不同的锁:
- 高16位:用来表示读锁的持有次数,即有多少个线程正在持有读锁。
- 低16位:用来表示写锁的持有次数,因为写锁是独占锁,这个值只可能为0或1,而它同时还表示了写锁的重入次数。
写锁的获取与释放(独占模式)
- 获取(acquire):
- CAS 尝试:线程尝试通过 CAS 操作将 state 变量的低16位从0设置为1。
- 如果 state 的高16位不为0(即有读锁存在),或者低16位不为0(即有其他线程持有写锁),则获取写锁失败。
- 如果 state 为0,表示当前没有其他读锁或写锁,CAS 成功,当前线程成功获取写锁,并进入临界区。
- 排队等待:如果获取失败,该线程会被封装成一个独占模式的节点,进入 AQS 的同步等待队列。
- CAS 尝试:线程尝试通过 CAS 操作将 state 变量的低16位从0设置为1。
- 重入(Reentrancy):
- 如果当前线程已经持有了写锁,再次尝试获取写锁时,只需要将 state 的低16位加1,实现可重入。
- 释放(release):
- 持有写锁的线程调用 unlock() 时,会将 state 的低16位减1。
- 如果低16位减为0,则完全释放写锁,并唤醒 AQS 队列中等待的线程(包括读线程和写线程)。
读锁的获取与释放(共享模式)
- 获取(acquireShared):
- 如果 state 的低16位不为0(即有写锁存在),则获取读锁失败。
- 如果没有写锁,线程尝试通过 CAS 操作将 state 的高16位加1,表示读锁计数加1。
- 入队等待:如果获取失败,线程被封装成一个共享模式的节点,进入 AQS 同步队列。
- 释放(releaseShared):
- 线程调用 unlock() 时,会将 state 的高16位减1。
- 如果高16位减为0,则释放读锁,并唤醒 AQS 队列中等待的下一个线程。
核心锁状态逻辑
- 读-写互斥:写锁在获取时会检查 state 的高16位(读锁状态)和低16位(写锁状态),只要任何一方不为0,就无法获取。这保证了写操作的独占性。
- 读-读共享:读锁在获取时,只检查 state 的低16位(写锁状态)。只要没有写锁,多个线程就可以并发地通过 CAS 操作增加 state 的高16位,共享读锁。
- 写锁降级:ReentrantReadWriteLock 支持写锁降级。一个持有写锁的线程可以先获取读锁,然后再释放写锁。持有写锁的线程在获取读锁时,因为是同一个线程,所以不会被阻塞。然后释放写锁,其他线程就可以并发地获取读锁了。
- 写锁升级(不支持):ReentrantReadWriteLock 不支持写锁升级,即持有读锁的线程无法直接获取写锁。:如果允许写锁升级,多个持有读锁的线程都试图升级为写锁,将导致死锁。
CAS
在Java中,CAS(Compare-And-Swap) 的实现原理依赖于底层的 CPU 硬件指令和 Unsafe 类,这是一种实现无锁(lock-free)编程的乐观并发机制。CAS 的基本思想:CAS 操作包含三个核心参数:
- 内存位置(V):要更新的变量在内存中的地址。
- 预期旧值(A):线程认为该变量当前应有的值。
- 新值(B):希望将该变量更新成的值。
CAS 操作的逻辑是:如果内存位置 V 的值与预期旧值 A 相匹配,那么就将 V 的值原子地更新为新值 B;否则,不做任何操作。
Java CAS 的实现细节
Java 中的 CAS 操作主要由 java.util.concurrent.atomic 包下的原子类(如 AtomicInteger)通过调用 sun.misc.Unsafe 类中的 native 方法实现。Unsafe提供了直接操作内存的能力,是Java中CAS操作的基石。compareAndSet 方法它实际上会调用 Unsafe 类的 compareAndSwapxxx() 本地方法。Unsafe 类的本地方法最终会通过 JNI(Java Native Interface) 调用操作系统的 C/C++ 代码,并利用 CPU 提供的特殊硬件指令来实现原子操作,在 x86 处理器上,CAS 操作通常会使用 lock cmpxchg 汇编指令, lock cmpxchg指令保证原子性:
- 锁定总线:lock 前缀会锁定处理器总线,确保在指令执行期间,其他处理器无法访问共享内存,从而保证了比较和交换操作的原子性。
- 缓存一致性:对于多核处理器,lock 指令还会触发 缓存一致性协议(如 MESI),将当前处理器的操作通知给其他处理器,使其他处理器中缓存了该变量的副本无效,确保数据是最新的。
CAS 的工作流程(以 AtomicInteger 为例)
- 获取当前值:线程首先获取共享变量的当前值,例如 int current = atomicInt.get()。
- 计算新值:线程基于 current 值进行计算,得到新值。
- 循环尝试:
- 线程调用 compareAndSet(current, newValue),尝试将 atomicInt 的值从 current 替换为 newValue。
- 如果在这期间,没有其他线程修改该变量,CAS 操作成功,循环结束。
- 如果CAS操作失败(说明在线程计算新值期间,有其他线程修改了变量),线程会重新读取最新的值,再次计算新值,并重新尝试 CAS 操作。这个过程被称为自旋,会一直持续直到成功。
CAS 的局限性
- ABA 问题:一个值从 A 变为 B,然后又变回 A。一个线程的 CAS 操作会认为变量没有被修改过,但实际上已经有其他线程对其进行了操作。可以引入版本号或时间戳,使用 AtomicStampedReference 来解决,在更新变量时,也更新版本号,确保操作的唯一性。
- 开销大:如果长时间自旋,会占用 CPU 资源。在竞争激烈的情况下,CAS 性能不如传统的锁。
- 只能保证一个共享变量的原子操作:对于多个共享变量的操作,CAS 无法保证原子性。要实现多个变量的原子操作,可以考虑使用 AtomicReference 将多个变量封装成一个对象,或者使用传统的锁机制。