Java 锁的机制 【入门讲解】
分类维度具体类型核心特点典型例子是否互斥互斥锁(Exclusive Lock)同一时间只允许一个线程持有锁(写操作需互斥)共享锁(Shared Lock)允许多个线程同时持有锁(读操作可共享)的读锁是否公平公平锁(Fair Lock)线程获取锁的顺序与请求顺序一致(先到先得)非公平锁(Unfair Lock)线程获取锁时可 “插队”(可能导致某些线程饥饿,但性能更高)是否可重入可重入锁(Reent
锁是并发编程的核心机制,其本质是通过控制共享资源的访问权限,解决多线程竞争导致的数据不一致问题。深入理解锁,需要从 “为什么需要锁”“锁的核心特性”“锁的实现原理”“Java 中的锁机制” 四个维度展开,结合底层逻辑和实际场景拆解:
一、为什么需要锁?—— 并发问题的根源与锁的本质
多线程并发访问共享资源(如全局变量、数据库连接、文件)时,会因 “指令交错执行” 导致原子性、可见性、有序性被破坏,引发数据错误(如超卖、余额异常)。
- 原子性:一个操作或多个操作要么全执行,要么全不执行(如
i++实际是 “读 - 改 - 写” 三步,多线程下可能被打断)。 - 可见性:一个线程修改的共享变量,其他线程能立即看到(CPU 缓存会导致线程间数据不一致)。
- 有序性:指令执行顺序与代码顺序一致(JVM 指令重排序可能打乱执行顺序)。
锁的核心作用是通过 “互斥” 保证原子性(同一时间只有一个线程执行临界区代码),并通过内存屏障保证可见性和有序性,从而解决并发问题。
二、锁的核心特性与分类:从 “行为” 定义锁的差异
锁的分类维度很多,核心是通过 “行为特性” 区分,决定了其适用场景:
| 分类维度 | 具体类型 | 核心特点 | 典型例子 |
|---|---|---|---|
| 是否互斥 | 互斥锁(Exclusive Lock) | 同一时间只允许一个线程持有锁(写操作需互斥) | synchronized、ReentrantLock |
| 共享锁(Shared Lock) | 允许多个线程同时持有锁(读操作可共享) | ReadWriteLock的读锁 |
|
| 是否公平 | 公平锁(Fair Lock) | 线程获取锁的顺序与请求顺序一致(先到先得) | ReentrantLock(true) |
| 非公平锁(Unfair Lock) | 线程获取锁时可 “插队”(可能导致某些线程饥饿,但性能更高) | synchronized、ReentrantLock(false) |
|
| 是否可重入 | 可重入锁(Reentrant) | 同一线程可多次获取同一把锁(避免自己锁死自己) | synchronized、ReentrantLock |
| 非可重入锁(Non-reentrant) | 同一线程不可重复获取锁(可能导致死锁) | 早期Mutex锁 |
|
| 是否可中断 | 可中断锁 | 线程等待锁时可被中断(避免无限等待) | ReentrantLock.lockInterruptibly() |
| 不可中断锁 | 线程一旦开始等待锁,必须等到获取锁或执行完毕,无法被中断 | synchronized |
|
| 是否有条件 | 条件锁(Condition Lock) | 允许线程在获取锁后,等待某个条件满足再继续执行(如 “队列不为空才消费”) | ReentrantLock.newCondition() |
关键特性解析:
-
可重入性:例:线程 T1 获取锁后,执行方法 A,方法 A 中调用了同样需要该锁的方法 B,可重入锁允许 T1 直接进入方法 B(无需重新竞争锁)。
- 实现原理:锁内部记录 “持有线程” 和 “重入次数”,同一线程再次获取时,重入次数 + 1;释放时,重入次数 - 1,直到 0 才真正释放锁。
- 作用:避免 “自己锁死自己”(如递归调用带锁的方法)。
-
公平性:
- 公平锁:通过队列记录线程请求顺序,严格按顺序分配锁(无插队),但频繁的队列操作会降低性能。
- 非公平锁:线程请求锁时,先尝试直接获取(插队),失败再入队,减少上下文切换,性能更高,但可能导致部分线程长期得不到锁(饥饿)。
- 现实类比:公平锁 = 排队买票,非公平锁 = 排队时有人尝试插队(成功就买,失败再排队)。
三、Java 中锁的底层实现:从synchronized到ReentrantLock
Java 中的锁机制分为内置锁(synchronized) 和显式锁(JUC 中的 Lock 接口),两者底层实现不同,但核心目标一致。
1. synchronized:JVM 内置锁(“隐式锁”)
synchronized是 Java 最基础的锁,通过对象头和监视器(Monitor) 实现,JDK1.6 后引入 “锁升级” 机制大幅提升性能。
(1)锁的存储位置:对象头的Mark Word
Java 对象在内存中的布局包括:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。其中,对象头的Mark Word 是synchronized实现的关键,用于存储锁状态、持有线程等信息(32 位 JVM 示例):
| 锁状态 | Mark Word 结构(32 位) | 说明 |
|---|---|---|
| 无锁状态 | 哈希码(25 位) + 分代年龄(4 位) + 01(无锁标志) | 未被锁定,仅存储对象哈希码等信息 |
| 偏向锁 | 线程 ID(23 位) + Epoch(2 位) + 分代年龄(4 位) + 01(偏向标志) | 长期被同一线程持有,减少竞争开销 |
| 轻量级锁 | 指向栈中锁记录的指针(30 位) + 00(轻量标志) | 多线程交替执行,无激烈竞争 |
| 重量级锁 | 指向 Monitor 的指针(30 位) + 10(重量标志) | 多线程激烈竞争,依赖操作系统互斥量 |
(2)锁升级:从 “无锁” 到 “重量级锁” 的渐进式优化
JVM 为了减少锁的性能开销,设计了 “锁升级” 机制(不能降级),根据竞争激烈程度自动切换锁状态:
-
无锁 → 偏向锁:当一个线程第一次获取锁时,JVM 在
Mark Word中记录该线程 ID(通过 CAS 操作),后续该线程再次获取锁时,只需判断Mark Word中的线程 ID 是否为自己,无需任何同步操作(几乎零开销)。- 适用场景:单线程重复获取锁(如单线程操作集合)。
-
偏向锁 → 轻量级锁:当有第二个线程尝试获取锁时,偏向锁失效,升级为轻量级锁。此时,线程会在自己的栈帧中创建 “锁记录(Lock Record)”,并通过 CAS 将
Mark Word指向自己的锁记录:- 若 CAS 成功,线程获取轻量级锁;
- 若 CAS 失败(说明有竞争),则自旋重试(通过循环尝试获取锁,避免进入内核态)。
- 适用场景:多线程交替执行(竞争不激烈),自旋次数少。
-
轻量级锁 → 重量级锁:若自旋次数超过阈值(JDK1.6 后自适应,根据前一次自旋成功率调整),或有更多线程参与竞争,轻量级锁升级为重量级锁。此时,线程会进入Monitor 的等待队列,放弃 CPU 资源(进入阻塞状态),由操作系统调度(开销大)。
- 适用场景:多线程激烈竞争(如高并发写操作)。
(3)synchronized的内存语义
synchronized除了互斥,还通过内存屏障保证可见性和有序性:
- 进入同步块时:会清空工作内存,从主内存加载最新共享变量(保证可见性);
- 退出同步块时:会将工作内存中的修改刷新到主内存(保证可见性);
- 禁止指令重排序:同步块内的指令不会被重排序到块外(保证有序性)。
2. ReentrantLock:JUC 中的显式锁(基于 AQS)
ReentrantLock是java.util.concurrent.locks包下的可重入锁,功能比synchronized更灵活(支持公平 / 非公平、可中断、条件锁),底层基于AQS(AbstractQueuedSynchronizer,抽象队列同步器) 实现。
(1)AQS:锁的 “骨架”
AQS 是 JUC 中同步工具的基础(如ReentrantLock、Semaphore、CountDownLatch),核心由两部分组成:
- 状态变量(state):用
volatile int存储锁的状态(0 = 未锁定,>0 = 已锁定,值 = 重入次数)。 - 同步队列(CLH 队列):双向链表,存储等待锁的线程(Node 节点包含线程引用、等待状态等)。
(2)ReentrantLock的加锁 / 解锁流程(非公平锁为例)
-
加锁(lock ()):
- 尝试 CAS 将
state从 0 改为 1(直接插队获取锁); - 若成功,设置当前线程为持有线程;
- 若失败,判断当前线程是否为持有线程(可重入):是则
state+1; - 若不是,将线程封装为 Node 加入同步队列,进入阻塞状态(通过
LockSupport.park())。
- 尝试 CAS 将
-
解锁(unlock ()):
- 调用
tryRelease(),state-1; - 若
state变为 0,释放持有线程,从同步队列唤醒一个线程(通过LockSupport.unpark()); - 被唤醒的线程尝试获取锁(重复步骤 1-3)。
- 调用
(3)与synchronized的核心区别
| 特性 | synchronized |
ReentrantLock |
|---|---|---|
| 锁释放 | 自动释放(出同步块 / 方法时) | 手动释放(必须在finally中调用unlock()) |
| 公平性 | 仅支持非公平锁 | 支持公平 / 非公平(构造器参数指定) |
| 可中断性 | 不可中断 | 支持可中断获取(lockInterruptibly()) |
| 条件锁 | 不支持 | 支持(newCondition()) |
| 性能(JDK1.6+) | 与ReentrantLock接近(锁升级优化后) |
高并发下略优(灵活控制) |
四、锁的高级形态:从 “独占” 到 “高效共享”
为了在并发场景中提升性能,Java 还提供了针对特定场景的锁:
1. 读写锁(ReadWriteLock):读共享、写互斥
- 核心思想:区分 “读操作” 和 “写操作”—— 多个线程可同时读(共享锁),但写操作必须互斥(且读写互斥),适合 “读多写少” 场景(如缓存)。
- 实现:
ReentrantReadWriteLock,包含readLock()(共享锁)和writeLock()(互斥锁)。 - 示例:
java
运行
ReadWriteLock rwLock = new ReentrantReadWriteLock(); Lock readLock = rwLock.readLock(); // 读锁(共享) Lock writeLock = rwLock.writeLock(); // 写锁(互斥) // 读操作:多个线程可同时执行 readLock.lock(); try { /* 读取共享资源 */ } finally { readLock.unlock(); } // 写操作:同一时间只有一个线程执行 writeLock.lock(); try { /* 修改共享资源 */ } finally { writeLock.unlock(); }
2. stampedLock:乐观读优化的读写锁
StampedLock是 JDK1.8 新增的锁,比ReadWriteLock更灵活,支持三种模式:
- 写锁(writeLock ()):互斥,获取后返回一个 “戳记(stamp)”;
- 悲观读锁(readLock ()):共享,类似
ReadWriteLock的读锁; - 乐观读(tryOptimisticRead ()):无锁,尝试获取戳记,读取后验证戳记是否有效(期间无写操作),适合读操作极多、写操作极少的场景。
3. 无锁编程:CAS(非阻塞同步)
锁的本质是 “阻塞同步”(线程竞争时会阻塞),而 CAS(Compare And Swap,比较并交换)是 “非阻塞同步” 的核心,通过硬件指令保证原子性,无需线程阻塞。
- 原理:
CAS(V, A, B)—— 若内存值V等于预期值A,则更新为B,否则不操作,返回V。 - 示例:
AtomicInteger的incrementAndGet()(原子自增):java
运行
public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; } // 底层调用CAS:循环尝试直到成功 - 优点:无线程阻塞 / 唤醒的开销,性能高;
- 缺点:可能出现 “ABA 问题”(值从 A→B→A,CAS 认为未变),可通过版本号解决(如
AtomicStampedReference)。
五、锁的性能优化:从 “减少竞争” 到 “高效利用”
实际开发中,锁的性能优化核心是减少锁的竞争程度,常用策略:
-
减少锁持有时间:只在必要的代码段加锁(临界区最小化),避免锁范围过大。
java
运行
// 优化前:锁范围大 synchronized (this) { readFile(); // 耗时操作,无需加锁 updateData(); // 仅这步需要加锁 } // 优化后:锁范围最小化 readFile(); synchronized (this) { updateData(); } -
降低锁粒度:将大对象拆分为小对象,减少竞争(如
ConcurrentHashMap的 “分段锁”,JDK1.7 前将数组分为 16 段,每段独立加锁)。 -
锁分离:根据操作类型拆分锁(如读写锁分离读和写)。
-
锁粗化:连续多次加锁 / 解锁合并为一次(避免频繁切换锁状态的开销)。
java
运行
// 优化前:频繁加锁解锁 for (int i = 0; i < 100; i++) { synchronized (this) { count++; } } // 优化后:一次加锁覆盖整个循环 synchronized (this) { for (int i = 0; i < 100; i++) { count++; } } -
使用无锁编程:如
Atomic系列、LongAdder(高并发下比AtomicLong性能更高)。
六、总结:锁的本质与选择
锁的本质是 **“并发环境下共享资源的访问控制器”**,其设计围绕 “安全性” 和 “性能” 的平衡:
- 简单场景用
synchronized(无需手动释放,不易出错); - 复杂场景用
ReentrantLock(需要公平性、可中断、条件锁时); - 读多写少用
ReadWriteLock或StampedLock; - 高并发计数用
Atomic系列或LongAdder(无锁)。
理解锁的底层原理(如synchronized的锁升级、AQS 的队列机制),能帮你在实际开发中避开 “死锁”“性能瓶颈” 等坑,写出高效且安全的并发代码。
更多推荐

所有评论(0)