Java中的锁技术概述
锁是多线程编程中保证线程安全的核心机制,它通过限制对共享资源的并发访问来维护数据一致性。在Java并发编程中,正确使用锁可以避免竞态条件、内存可见性等问题。Java提供了多种锁机制,包括:
- 内置锁 :基于
synchronized关键字的隐式锁 - 显式锁 :
java.util.concurrent.locks包中的ReentrantLock等 - 读写锁 :
ReentrantReadWriteLock实现读-写分离 - 乐观锁 :
StampedLock提供的乐观读模式
不同锁机制适用于不同场景,选择时需考虑并发度、吞吐量要求和业务特点。
synchronized关键字
基本用法
synchronized是Java最基础的同步机制,有两种使用方式:
- 同步方法:
java
public synchronized void method() {
// 同步代码
}
- 同步代码块:
java
public void method() {
synchronized(this) { // 锁对象
// 同步代码
}
}
底层实现
synchronized基于Monitor机制实现,每个Java对象都与一个Monitor相关联:
- 当线程进入同步块时,会尝试获取Monitor的所有权
- 获取成功则持有锁,其他线程必须等待
- 退出同步块时释放Monitor
JVM通过对象头中的Mark Word记录锁状态,包含无锁、偏向锁、轻量级锁和重量级锁等状态。
优缺点
优点:
- 语法简单,自动释放锁
- JVM内置支持,优化空间大
- 不需要显式创建锁对象
缺点:
- 无法中断等待锁的线程
- 不支持尝试获取锁(tryLock)
- 只有一种锁模式(非公平)
- 粒度较粗,可能影响性能
ReentrantLock
ReentrantLock是Lock接口的主要实现类,提供比synchronized更灵活的锁操作。
主要特性
- 可重入性 :与
synchronized相同,允许线程重复获取已持有的锁 - 公平性选择 :
- 公平锁:按请求顺序分配(构造传入true)
- 非公平锁:允许插队(默认,吞吐量更高)
- 锁绑定:可与多个Condition关联,实现精细等待/通知
高级功能
java
Lock lock = new ReentrantLock();
// 1. 可中断获取
lock.lockInterruptibly();
// 2. 尝试获取锁
if(lock.tryLock()) {
try {
// 操作共享资源
} finally {
lock.unlock();
}
}
// 3. 超时获取
if(lock.tryLock(5, TimeUnit.SECONDS)) {
// ...
}
与synchronized对比
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现方式 | JVM内置 | Java代码实现 |
| 锁释放 | 自动 | 必须手动unlock |
| 可中断 | 不支持 | 支持 |
| 尝试获取 | 不支持 | 支持 |
| 公平锁 | 非公平 | 可配置 |
| Condition | 单一wait/notify | 支持多个 |
| 性能 | Java6后优化相当 | 高竞争时可能更优 |
ReadWriteLock
ReentrantReadWriteLock实现了读写分离,允许多个读操作并发执行,但写操作独占。
锁协作机制
- 读锁:共享锁,多个线程可同时持有
- 写锁:独占锁,排斥所有其他锁
- 锁降级:持有写锁时获取读锁,然后释放写锁(反向不允许)
适用场景
缓存系统示例:
java
class Cache<K,V> {
private final Map<K,V> map = new HashMap<>();
private final ReadWriteLock rwl = new ReentrantReadWriteLock();
public V get(K key) {
rwl.readLock().lock();
try {
return map.get(key);
} finally {
rwl.readLock().unlock();
}
}
public void put(K key, V value) {
rwl.writeLock().lock();
try {
map.put(key, value);
} finally {
rwl.writeLock().unlock();
}
}
}
性能优势:
- 读多写少时(如90%读+10%写),吞吐量可提升5-10倍
- 减少线程竞争,提高并发度
StampedLock
Java 8引入的性能更优的锁,支持三种访问模式:
锁模式
- 写锁:独占锁,类似ReentrantLock
- 悲观读锁:共享锁,类似ReadWriteLock的读锁
- 乐观读:不阻塞写操作,读取后需验证
典型使用
java
class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
void move(double deltaX, double deltaY) {
long stamp = sl.writeLock(); // 获取写锁
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp); // 释放写锁
}
}
double distanceFromOrigin() {
// 1. 尝试乐观读
long stamp = sl.tryOptimisticRead();
double currentX = x, currentY = y;
if (!sl.validate(stamp)) { // 检查是否被修改
// 2. 升级为悲观读锁
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX*currentX + currentY*currentY);
}
}
适用场景
- 读操作非常频繁(如1000:1的读写比)
- 可以容忍短暂的数据不一致
- 不适合重入场景(StampedLock不可重入)
锁优化技术
JVM层优化
-
锁消除:JIT编译器通过逃逸分析,去除不可能存在竞争的锁
javapublic String concat(String s1, String s2) { StringBuffer sb = new StringBuffer(); // 局部变量,线程安全 sb.append(s1); sb.append(s2); return sb.toString(); // 会自动消除synchronized } -
锁粗化:将连续的多个锁操作合并为一个
java// 优化前 for(int i=0; i<100; i++) { synchronized(this) { // ... } } // 优化后 synchronized(this) { for(int i=0; i<100; i++) { // ... } } -
偏向锁:无竞争时消除同步开销
- 第一个获取锁的线程ID记录在对象头
- 该线程再次获取时无需同步操作
-
轻量级锁:竞争不激烈时用CAS代替OS互斥
- 失败后膨胀为重量级锁
应用层优化
- 减小锁粒度:如ConcurrentHashMap分段锁
- 锁分离:读写锁思想
- 无锁编程:使用原子类(AtomicInteger等)
分布式锁扩展
实现方式
-
基于Redis:
- SETNX命令实现互斥
- RedLock算法解决单点问题
java// Redisson实现示例 RLock lock = redisson.getLock("myLock"); lock.lock(); try { // 业务代码 } finally { lock.unlock(); } -
基于ZooKeeper:
- 创建临时顺序节点
- 最小节点获取锁,其他监听前驱节点
CAP权衡
- Redis:AP系统,高性能但可能不一致
- ZooKeeper:CP系统,强一致但性能较低
锁的常见问题与解决方案
死锁
条件(全部满足才会发生):
- 互斥条件
- 请求与保持
- 不剥夺条件
- 循环等待
检测工具:
bash
jstack <pid> # 查看线程堆栈
jconsole # 图形化监控
预防措施:
- 按固定顺序获取锁
- 设置锁超时(tryLock)
- 使用jhat分析线程dump
活锁
表现:线程不断重试却无法取得进展
解决:引入随机退避机制
锁饥饿
原因:低优先级线程始终无法获取锁
方案:使用公平锁(但会降低吞吐量)
最佳实践与性能考量
锁选择原则
- 简单场景 :优先使用
synchronized - 需要高级功能 :选择
ReentrantLock - 读多写少 :考虑
ReadWriteLock或StampedLock - 超高并发:探索无锁算法(如CAS)
注意事项
-
粒度控制:
- 过粗:降低并发度
- 过细:增加锁开销
-
避免嵌套:减少死锁风险
-
资源清理:确保finally中释放锁
性能监控
-
工具:
- JVisualVM:查看线程状态
- JProfiler:分析锁竞争
- Arthas:监控锁热点
-
关键指标:
- 锁等待时间
- 持有时间
- 竞争频率
通过合理选择和优化锁的使用,可以显著提升Java并发程序的性能和可靠性。
Condition接口详解
Condition接口概述
Condition接口是Java并发包中提供的高级线程协调机制,作为Object监视器方法(wait/notify/notifyAll)的替代方案出现。与基本的Object监视器方法相比,Condition接口提供了更精细的线程控制能力,主要体现在:
- 一个Lock可以关联多个Condition实例,允许对不同等待条件进行分组管理
- 支持公平/非公平的线程唤醒策略
- 提供可中断/不可中断的等待选项
- 支持超时等待功能
Condition接口核心方法详解
await()方法
使当前线程进入等待状态,直到以下情况之一发生:
- 被其他线程调用signal()或signalAll()唤醒
- 线程被中断(抛出InterruptedException)
- 发生虚假唤醒(spurious wakeup)
典型使用模式:
java
while (!conditionSatisfied) {
condition.await();
}
signal()方法
精确唤醒一个等待在该Condition上的线程(如果是公平锁则唤醒等待时间最长的线程)。与notify()不同的是,signal()可以确保唤醒的是特定条件队列中的线程。
signalAll()方法
唤醒所有等待在该Condition上的线程,类似于Object.notifyAll(),但只会影响当前Condition关联的等待线程。
Condition与Lock的关系
Condition必须与Lock配合使用的原因:
- 线程安全性:Condition操作需要建立在已获取锁的基础上
- 状态一致性:await()会自动释放锁,signal()后需要重新获取锁
- 精细控制:通过不同Condition实例管理不同的等待条件
创建Condition实例的标准方式:
java
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
使用场景深度分析
生产者-消费者模型增强实现
java
class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
线程精确交替执行
java
class AlternateExecution {
private Lock lock = new ReentrantLock();
private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition();
private boolean runA = true;
public void executeA() throws InterruptedException {
lock.lock();
try {
while (!runA) {
conditionA.await();
}
System.out.println("A executing");
runA = false;
conditionB.signal();
} finally {
lock.unlock();
}
}
public void executeB() throws InterruptedException {
lock.lock();
try {
while (runA) {
conditionB.await();
}
System.out.println("B executing");
runA = true;
conditionA.signal();
} finally {
lock.unlock();
}
}
}
注意事项深度解析
虚假唤醒问题
Java语言规范允许await()在没有调用signal()的情况下返回(称为虚假唤醒),因此必须使用循环结构来重新检查等待条件。这是与Object.wait()相同的要求,但Condition接口的设计使这种模式更加自然。
中断处理策略
Condition提供多种等待方法处理中断:
- await():可中断等待,响应中断抛出InterruptedException
- awaitUninterruptibly():完全忽略中断
- awaitNanos()/awaitUntil():支持超时的可中断等待
中断时的状态变化:
- 被中断的线程会从等待队列转移到同步队列
- 中断状态会被保留,await()在返回前会清除中断状态
性能优化建议
- 在简单同步场景(单个等待条件)下,Object监视器方法性能略优
- 在复杂同步场景(多个等待条件)下,Condition接口性能明显更好
- 高竞争环境下,Condition的非公平模式(默认)吞吐量更高
- 对等待时间有严格要求的场景,应使用公平模式
选择建议:
- 单个条件:考虑Object.wait/notify
- 多个条件:必须使用Condition
- 需要超时/中断控制:优先选择Condition
底层实现原理
Condition在AQS中的实现涉及两个队列:
- 条件队列(Condition Queue):单向链表存储等待线程
- 同步队列(Sync Queue):CLH队列等待获取锁
关键工作流程:
-
await()时:
- 创建节点加入条件队列
- 完全释放锁
- 阻塞当前线程
-
signal()时:
- 将节点从条件队列转移到同步队列
- 转移后的节点在同步队列中参与锁竞争
-
signalAll()时:
- 将条件队列所有节点转移到同步队列
- 转移顺序保持FIFO特性
节点状态转换图:
[Condition Queue] --signal()--> [Sync Queue] --acquire()--> [Running]
完整示例代码
java
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionDemo {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private boolean ready = false;
public void awaitDemo() throws InterruptedException {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " entering await");
while (!ready) {
condition.await(); // 释放锁并等待
}
System.out.println(Thread.currentThread().getName() + " resuming execution");
// 执行条件满足后的操作
} finally {
lock.unlock();
}
}
public void signalDemo() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " preparing condition");
ready = true;
condition.signal(); // 唤醒一个等待线程
System.out.println(Thread.currentThread().getName() + " signaled condition");
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ConditionDemo demo = new ConditionDemo();
Thread waiter = new Thread(() -> {
try {
demo.awaitDemo();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "WaiterThread");
Thread signaller = new Thread(() -> {
try {
Thread.sleep(2000); // 模拟准备工作
demo.signalDemo();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "SignallerThread");
waiter.start();
signaller.start();
waiter.join();
signaller.join();
}
}
该示例展示了:
- 基本await/signal用法
- 正确的锁释放/获取模式
- 典型的多线程协作流程
- 线程安全的条件检查方式