Java并发锁机制知识总结(最新整理)
作者:秋风不识晚间路
并发锁机制知识总结
Java 的锁机制是并发编程的核心,用于解决多线程共享资源竞争问题,保证数据一致性和线程安全。Java 提供了内置锁(synchronized)、显式锁(Lock 接口) 等多种锁实现,同时衍生出偏向锁、轻量级锁、重量级锁等优化机制,以及可重入锁、公平锁、读写锁等功能分类。
一、锁的核心作用
- 互斥性:同一时间只有一个线程能持有锁,访问共享资源(解决 “竞态条件”)。
- 可见性:持有锁的线程修改共享资源后,释放锁时会将修改同步到主内存;其他线程获取锁时会从主内存读取最新值(避免 “缓存一致性问题”)。
- 有序性:禁止指令重排序(避免多线程下的 “指令重排导致的逻辑错乱”)。
二、Java 锁的分类
(一)按实现方式:内置锁 vs 显式锁
1. 内置锁:synchronized(JVM 层面实现)
synchronized 是 Java 原生的隐式锁,基于 对象监视器(Monitor) 实现,无需手动释放,简单易用、线程安全。
(1)使用场景
(2)示例代码
public class SynchronizedDemo {
// 1. 实例方法锁(锁:this)
public synchronized void method1() {
// 共享资源操作
}
// 2. 静态方法锁(锁:SynchronizedDemo.class)
public static synchronized void method2() {
// 共享资源操作
}
// 3. 代码块锁(锁:自定义对象)
private final Object lock = new Object();
public void method3() {
synchronized (lock) {
// 共享资源操作
}
}
}(3)核心特性
(4)JVM 优化:锁升级(无锁 → 偏向锁 → 轻量级锁 → 重量级锁)
为了平衡性能和并发安全,JVM 对 synchronized 进行了锁升级优化(从低开销到高开销逐步过渡):
锁升级是不可逆的(一旦升级为重量级锁,不会降级)。
2. 显式锁:Lock 接口(Java 代码层面实现)
java.util.concurrent.locks.Lock 是 JDK 1.5 引入的显式锁,提供比 synchronized 更灵活的功能(如可中断、超时获取、公平锁等),但需手动释放锁(否则会导致死锁)。
(1)核心方法
| 方法 | 作用 |
|---|---|
lock() | 获取锁(阻塞,不可中断) |
lockInterruptibly() | 获取锁(可被中断,抛出 InterruptedException) |
tryLock() | 尝试获取锁(非阻塞,成功返回 true,失败返回 false) |
tryLock(long time, TimeUnit unit) | 超时获取锁(超时未获取则返回 false) |
unlock() | 释放锁(必须在 finally 中调用) |
newCondition() | 创建条件变量(实现线程等待 / 唤醒) |
(2)常用实现类
(3)示例代码(ReentrantLock)
import java.util.concurrent.locks.ReentrantLock;
public class LockDemo {
// 创建非公平锁(默认),若要公平锁:new ReentrantLock(true)
private final ReentrantLock lock = new ReentrantLock();
public void method() {
// 1. 获取锁(必须手动获取)
lock.lock();
try {
// 共享资源操作
} finally {
// 2. 释放锁(必须在 finally 中释放,避免锁泄漏)
lock.unlock();
}
}
// 可中断获取锁示例
public void interruptibleMethod() throws InterruptedException {
lock.lockInterruptibly(); // 线程可被中断
try {
// 共享资源操作
} finally {
lock.unlock();
}
}
}(4)核心特性
(二)按功能特性分类
1. 可重入锁 vs 不可重入锁
2. 公平锁 vs 非公平锁
3. 读写锁(ReentrantReadWriteLock)
针对 “读多写少” 场景优化,分离读锁(共享锁) 和写锁(排他锁):
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockDemo {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
private int data;
// 读操作(共享锁)
public int readData() {
readLock.lock();
try {
return data;
} finally {
readLock.unlock();
}
}
// 写操作(排他锁)
public void writeData(int value) {
writeLock.lock();
try {
data = value;
} finally {
writeLock.unlock();
}
}
}4. 乐观锁 vs 悲观锁
(三)其他特殊锁
1. 自旋锁(SpinLock)
线程获取锁失败时,不立即阻塞,而是循环(自旋)尝试获取锁(避免线程上下文切换开销)。
2. 偏向锁 / 轻量级锁 / 重量级锁(JVM 层面的锁优化)
见前文 synchronized 的锁升级机制,核心是 “按需升级”,平衡性能和并发安全。
3. StampedLock(乐观读锁)
JDK 1.8 引入,性能优于 ReentrantReadWriteLock,支持三种模式:
三、synchronized vs ReentrantLock 对比
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM 层面(底层是 Monitor) | Java 代码层面(AQS 框架) |
| 可重入性 | 支持 | 支持 |
| 公平性 | 仅非公平锁 | 公平 / 非公平可选 |
| 可中断性 | 不支持 | 支持(lockInterruptibly ()) |
| 超时获取 | 不支持 | 支持(tryLock (time)) |
| 条件变量 | 仅一个(wait ()/notify ()) | 多个(newCondition ()) |
| 锁释放 | 自动释放(退出同步块 / 方法) | 手动释放(必须在 finally 中 unlock ()) |
| 性能 | JDK 1.6 后优化(锁升级),性能接近 ReentrantLock | 高(灵活控制) |
| 适用场景 | 简单场景(无需复杂功能) | 复杂场景(需中断、超时、公平锁等) |
四、锁的使用注意事项
总结
Java 锁机制的核心是解决共享资源竞争,提供了从简单到复杂的多种实现:
使用锁时需平衡线程安全和性能,避免死锁、锁粒度过大等问题,根据实际场景选择合适的锁类型。
最后祝各位开发者工作顺利,万事如意。
- 修饰实例方法:锁是当前对象(
this)。 - 修饰静态方法:锁是当前类的
Class对象(全局锁)。 - 修饰代码块:锁是括号中的对象(可自定义锁对象,灵活度更高)。
- 可重入:同一线程可多次获取同一把锁(避免死锁,如递归调用 synchronized 方法)。
- 非公平锁:线程获取锁的顺序不按请求顺序(默认,性能优先)。
- 自动释放:线程退出同步块 / 方法时,无论正常退出还是异常退出,锁都会自动释放(无需手动处理)。
- 不可中断:线程获取锁时若被阻塞,只能等待锁释放或一直阻塞(无法主动中断)。
- 无锁:无线程竞争时,无需加锁(直接执行)。
- 偏向锁:单线程重复获取锁时,仅记录线程 ID(避免 CAS 操作,开销极低)。
- 触发条件:只有一个线程访问同步资源。
- 撤销:当有第二个线程竞争时,偏向锁会升级为轻量级锁。
- 轻量级锁: 多个线程交替访问锁(无激烈竞争),通过CAS(Compare and Swap)尝试获取锁(自旋等待,避免阻塞线程)。
- 自旋次数:默认 10 次(可通过
-XX:PreBlockSpin调整),自旋失败则升级为重量级锁。
- 自旋次数:默认 10 次(可通过
- 重量级锁:多个线程激烈竞争锁(自旋无效),通过操作系统的 互斥量(Mutex) 实现(线程会阻塞,开销最高)。
- ReentrantLock:可重入锁(最常用),支持公平锁和非公平锁(默认非公平)。
- ReentrantReadWriteLock:读写锁(分离读锁和写锁,提高读多写少场景的并发性能)。
- StampedLock:JDK 1.8 引入的乐观读锁(性能优于读写锁,支持乐观读、写锁、悲观读锁)。
- 可重入:与
synchronized一致,同一线程可多次获取锁(锁计数器递增,释放时递减至 0 才释放)。 - 公平 / 非公平可选:构造函数传入
true则为公平锁(按线程请求顺序分配锁,性能略低),默认非公平锁(性能优先)。 - 可中断:支持
lockInterruptibly()中断阻塞中的线程(避免无限等待)。 - 超时获取:通过
tryLock(time)避免线程永久阻塞。 - 条件变量:通过
newCondition()创建多个条件变量(比synchronized的wait()/notify()更灵活,可精准唤醒特定线程)。 可重入锁:同一线程可多次获取同一把锁(如synchronized、ReentrantLock),避免递归调用或嵌套同步时的死锁。
- 原理:锁内部维护一个 “线程持有计数”,线程首次获取时计数为 1,再次获取时计数递增,释放时计数递减,计数为 0 时锁释放。
不可重入锁:同一线程无法多次获取同一把锁(如简单的
SpinLock实现),容易导致死锁,极少使用。- 公平锁:线程获取锁的顺序严格按照 “请求顺序”(FIFO),不会出现饥饿(每个线程都能最终获取锁)。
- 实现:维护一个等待队列,新线程需加入队列尾部,仅当队列头部线程才能获取锁。
- 缺点:性能较低(频繁切换线程上下文)。
- 非公平锁:线程获取锁时不按请求顺序,允许 “插队”(刚释放锁的线程可能再次获取锁,减少上下文切换)。
- 优点:性能更高(默认选择,如
synchronized、ReentrantLock默认)。 - 缺点:可能导致部分线程长期无法获取锁(饥饿)。
- 优点:性能更高(默认选择,如
- 读锁(共享锁):多个线程可同时获取读锁(读操作不互斥)。
- 写锁(排他锁):仅一个线程可获取写锁(写操作与读 / 写操作互斥)。
- 核心规则:读锁持有期间,写锁无法获取;写锁持有期间,读锁和写锁均无法获取。
- 示例代码:
- 悲观锁:认为并发竞争必然发生,每次访问共享资源都先加锁(如synchronized、ReentrantLock)。
- 优点:简单可靠,适合写操作多的场景。
- 缺点:阻塞线程,性能开销高。
- 乐观锁:认为并发竞争很少发生,不加锁直接访问资源,仅在修改时通过 CAS 验证是否有冲突(冲突则重试)。
- 实现:
AtomicInteger(基于 CAS)、StampedLock(乐观读模式)、数据库的版本号机制。 - 优点:非阻塞,性能高,适合读操作多的场景。
- 缺点:存在 “ABA 问题”(可通过版本号 / 时间戳解决),重试次数过多会消耗 CPU。
- 实现:
- 适用场景:锁持有时间短、并发度低的场景(如 JVM 轻量级锁的自旋阶段)。
- 缺点:自旋期间占用 CPU,若锁持有时间长,会浪费 CPU 资源。
- 乐观读模式:无锁访问,读取后验证版本号(无冲突则成功,有冲突则升级为悲观读锁)。
- 悲观读模式:共享锁(类似读写锁的读锁)。
- 写模式:排他锁(类似读写锁的写锁)。
- 优点:乐观读模式下无锁竞争,性能极高;缺点:不支持可重入,使用复杂。
- 避免死锁:
- 手动释放锁(ReentrantLock 必须在 finally 中 unlock ())。
- 避免嵌套锁(或保证嵌套锁的获取顺序一致)。
- 避免长时间持有锁(减少锁竞争)。
- 选择合适的锁类型:
- 读多写少:用
ReentrantReadWriteLock或StampedLock。 - 简单场景:用
synchronized(无需手动管理锁)。 - 复杂场景(中断、超时、公平锁):用
ReentrantLock。
- 读多写少:用
- 减少锁粒度:
- 避免锁覆盖整个方法(仅对共享资源的操作加锁,如 synchronized 代码块而非方法)。
- 示例:用
synchronized (lock)替代synchronized method()(仅锁定关键代码)。
- 避免锁竞争:
- 无状态对象无需加锁(无共享资源)。
- 共享资源尽量设计为不可变(如
String、Integer)。 - 用局部变量替代共享变量(线程私有,无需锁)。
- 基础场景:优先使用
synchronized(简单、安全、JVM 优化充分)。 - 复杂场景:使用
ReentrantLock(可中断、超时、公平锁)或ReentrantReadWriteLock(读多写少)。 - 性能优化:利用 JVM 锁升级(synchronized)、乐观锁(StampedLock、Atomic 类)减少锁开销。
到此这篇关于Java-并发锁机制知识总结的文章就介绍到这了,更多相关Java并发锁机制内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
