1. Java并发锁机制全景解析
在Java并发编程中,锁机制是保证线程安全的核心工具。但锁不是简单的"加锁-解锁"操作,而是一套完整的策略体系。理解这些策略的适用场景和实现原理,对于编写高性能、高可靠的并发程序至关重要。
2. 锁策略分类与实现原理
2.1 悲观锁与乐观锁
2.1.1 悲观锁实现方式
悲观锁的核心思想是"先加锁再操作",典型的实现就是synchronized关键字:
java复制public class PessimisticCounter {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized(lock) {
count++; // 操作前先获取锁
}
}
}
这种方式的优点是实现简单,线程安全有保障。缺点是当竞争激烈时,线程频繁阻塞和唤醒会带来较大的性能开销。在实际应用中,适合写操作频繁或临界区较长的场景。
2.1.2 乐观锁实现方式
乐观锁的代表是CAS(Compare-And-Swap)操作,Java中的Atomic类就是基于此:
java复制import java.util.concurrent.atomic.AtomicInteger;
public class OptimisticCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
int oldValue;
int newValue;
do {
oldValue = count.get();
newValue = oldValue + 1;
} while (!count.compareAndSet(oldValue, newValue));
}
}
乐观锁的优势在于没有真正的锁竞争,线程不会阻塞。但在高竞争环境下,CAS失败重试可能反而降低性能。适合读多写少、冲突概率低的场景。
经验之谈:在JDK8+环境中,对于简单的计数器场景,推荐使用
LongAdder替代AtomicLong,它在高并发下性能更好,内部采用了分段CAS的策略。
2.2 重量级锁与轻量级锁
2.2.1 重量级锁的实现
重量级锁会直接导致线程进入阻塞状态,需要操作系统介入进行线程调度:
java复制public class HeavyLockExample {
private final Object lock = new Object();
public void longRunningTask() {
synchronized(lock) {
// 模拟长时间操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
重量级锁适合临界区执行时间较长(超过1ms)的场景,因为这时让线程阻塞比自旋等待更节省CPU资源。
2.2.2 轻量级锁的实现
轻量级锁通常采用自旋的方式实现:
java复制public class SpinLock {
private AtomicReference<Thread> owner = new AtomicReference<>();
public void lock() {
Thread current = Thread.currentThread();
while (!owner.compareAndSet(null, current)) {
// 自旋等待
}
}
public void unlock() {
Thread current = Thread.currentThread();
owner.compareAndSet(current, null);
}
}
现代JVM中的synchronized在竞争不激烈时,会先尝试轻量级锁策略。自旋锁适合临界区非常短(纳秒级)的场景,可以避免线程切换的开销。
注意事项:纯自旋锁在单核CPU上无效,因为持有锁的线程无法执行。在实际应用中,通常会采用自适应自旋策略,即根据历史等待时间动态调整自旋次数。
2.3 公平锁与非公平锁
2.3.1 公平锁实现
公平锁保证线程按照申请锁的顺序获取锁:
java复制import java.util.concurrent.locks.ReentrantLock;
public class FairLockExample {
private final ReentrantLock lock = new ReentrantLock(true);
public void fairMethod() {
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
}
}
公平锁的优点是可以防止线程饥饿,缺点是吞吐量相对较低,因为要维护一个等待队列。
2.3.2 非公平锁实现
非公平锁允许插队,是大多数情况下的默认选择:
java复制public class NonFairLockExample {
private final ReentrantLock lock = new ReentrantLock(); // 默认非公平
public void nonFairMethod() {
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
}
}
非公平锁的优点是吞吐量高,缺点是可能导致某些线程长时间获取不到锁。在实际应用中,除非有特殊需求,否则建议使用非公平锁。
2.4 可重入锁与不可重入锁
2.4.1 可重入锁示例
Java中的synchronized和ReentrantLock都是可重入锁:
java复制public class ReentrantExample {
public synchronized void method1() {
method2(); // 可以重入
}
public synchronized void method2() {
// ...
}
}
可重入锁通过维护持有线程和计数器实现,每次重入计数器加1,退出时减1,直到0才真正释放锁。
2.4.2 不可重入锁问题
使用不可重入锁会导致死锁:
java复制public class NonReentrantExample {
private Lock lock = new NonReentrantLock();
public void outer() {
lock.lock();
try {
inner();
} finally {
lock.unlock();
}
}
public void inner() {
lock.lock(); // 这里会死锁
try {
// ...
} finally {
lock.unlock();
}
}
}
在实际开发中,几乎总是需要使用可重入锁,因为方法调用链中可能会多次需要同一把锁。
2.5 读写锁
读写锁适用于读多写少的场景:
java复制import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteCache {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Map<String, Object> cache = new HashMap<>();
public Object get(String key) {
rwLock.readLock().lock();
try {
return cache.get(key);
} finally {
rwLock.readLock().unlock();
}
}
public void put(String key, Object value) {
rwLock.writeLock().lock();
try {
cache.put(key, value);
} finally {
rwLock.writeLock().unlock();
}
}
}
读写锁允许多个读操作并发执行,但写操作是独占的。在实际应用中,需要注意读写锁的升级和降级问题。
性能提示:在极端读多写少的场景下,可以考虑使用
StampedLock的乐观读模式,它比ReadWriteLock性能更好,但API更复杂。
3. synchronized深度解析
3.1 实现原理
synchronized的底层实现依赖于对象头中的Mark Word和Monitor机制。在字节码层面,它通过monitorenter和monitorexit指令实现:
java复制public class SyncBytecode {
public void syncMethod() {
synchronized(this) {
// ...
}
}
}
编译后的字节码会包含monitorenter和monitorexit指令对,JVM会确保即使抛出异常也能正确释放锁。
3.2 锁升级过程
现代JVM中synchronized的锁状态会随着竞争情况升级:
- 无锁状态:初始状态
- 偏向锁:第一个线程获取锁时,记录线程ID
- 轻量级锁:有轻微竞争时,通过CAS获取锁
- 重量级锁:竞争激烈时,升级为操作系统层面的互斥量
这个升级过程是JVM自动完成的,称为"锁膨胀"。
3.3 内存语义
synchronized不仅提供互斥,还保证可见性:
java复制public class VisibilityDemo {
private boolean flag = false;
public void writer() {
synchronized(this) {
flag = true; // 写操作
}
}
public void reader() {
synchronized(this) {
if(flag) { // 读操作
// ...
}
}
}
}
这是因为synchronized建立了一个happens-before关系,确保解锁前的写操作对后续加锁的线程可见。
4. ReentrantLock详解
4.1 基本用法
java复制import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
private final ReentrantLock lock = new ReentrantLock();
public void doSomething() {
lock.lock(); // 获取锁
try {
// 临界区代码
} finally {
lock.unlock(); // 必须在finally中释放锁
}
}
}
与synchronized相比,ReentrantLock需要显式地加锁和解锁,这增加了灵活性但也更容易出错。
4.2 高级特性
4.2.1 可中断锁
java复制public void interruptibleLock() throws InterruptedException {
lock.lockInterruptibly(); // 可被中断的获取锁
try {
// ...
} finally {
lock.unlock();
}
}
这个特性可以防止死锁,当线程在等待锁时可以被其他线程中断。
4.2.2 尝试获取锁
java复制public boolean tryLockExample() {
if (lock.tryLock()) { // 立即尝试获取锁
try {
// 成功获取锁
return true;
} finally {
lock.unlock();
}
} else {
// 获取锁失败
return false;
}
}
还可以设置超时时间:
java复制public boolean tryLockWithTimeout() throws InterruptedException {
if (lock.tryLock(1, TimeUnit.SECONDS)) { // 等待1秒
try {
// ...
return true;
} finally {
lock.unlock();
}
}
return false;
}
4.2.3 条件变量
java复制public class ConditionDemo {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
private final Queue<Object> queue = new LinkedList<>();
private final int capacity = 10;
public void put(Object item) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await(); // 等待队列不满
}
queue.add(item);
notEmpty.signal(); // 通知队列不空
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // 等待队列不空
}
Object item = queue.remove();
notFull.signal(); // 通知队列不满
return item;
} finally {
lock.unlock();
}
}
}
条件变量比Object.wait()/notify()更灵活,可以创建多个等待队列。
5. 性能对比与选型建议
5.1 synchronized vs ReentrantLock
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现方式 | JVM内置 | Java代码实现 |
| 锁释放 | 自动 | 必须手动 |
| 可中断 | 不支持 | 支持 |
| 尝试获取锁 | 不支持 | 支持 |
| 公平锁 | 不支持 | 支持 |
| 条件变量 | 单一 | 多个 |
| 性能 | JDK6+优化好 | 灵活但稍慢 |
5.2 选型建议
-
优先考虑synchronized的情况:
- 简单的同步需求
- 不需要高级特性
- 希望代码更简洁
- 对性能要求不是极端苛刻
-
考虑使用ReentrantLock的情况:
- 需要可中断的锁获取
- 需要尝试获取锁的能力
- 需要公平锁
- 需要多个条件变量
- 需要知道锁是否被持有
-
其他场景:
- 读多写少:考虑
ReadWriteLock或StampedLock - 简单的原子操作:考虑
Atomic类 - 无阻塞算法:考虑CAS操作
- 读多写少:考虑
6. 最佳实践与常见陷阱
6.1 锁的最佳实践
- 减小临界区范围:只锁必要的代码
- 避免嵌套锁:容易导致死锁
- 使用相同的加锁顺序:预防死锁
- 考虑锁的粒度:太粗影响并发,太细增加开销
- 优先使用并发容器:如
ConcurrentHashMap - 考虑无锁算法:如CAS操作
6.2 常见问题排查
-
死锁诊断:
- 使用jstack获取线程转储
- 查找"deadlock"关键词
- 分析锁的持有和等待关系
-
性能问题排查:
- 使用JProfiler等工具分析锁竞争
- 关注
BLOCKED状态的线程 - 检查锁的持有时间
-
内存一致性问题:
- 确保对共享变量的访问都有正确的同步
- 考虑使用
volatile修饰标志变量 - 避免在锁外发布共享对象
6.3 实际案例分享
在一个高并发的交易系统中,我们遇到了性能瓶颈。通过分析发现:
- 原始实现使用了
synchronized方法,锁粒度太粗 - 改为使用
ConcurrentHashMap和细粒度锁 - 对热点数据采用读写分离策略
- 最终QPS从1000提升到8000+
关键优化代码片段:
java复制public class OptimizedTradeService {
private final ConcurrentMap<String, ReentrantLock> symbolLocks = new ConcurrentHashMap<>();
private final Map<String, BigDecimal> priceMap = new HashMap<>();
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public void updatePrice(String symbol, BigDecimal price) {
ReentrantLock lock = symbolLocks.computeIfAbsent(symbol, k -> new ReentrantLock());
lock.lock();
try {
// 更新价格
priceMap.put(symbol, price);
} finally {
lock.unlock();
}
}
public BigDecimal getPrice(String symbol) {
rwLock.readLock().lock();
try {
return priceMap.get(symbol);
} finally {
rwLock.readLock().unlock();
}
}
}
这个案例展示了如何根据实际场景选择合适的锁策略和粒度。