Java 锁的分类
Java 中的锁可以从多个维度进行分类:
悲观锁 vs. 乐观锁公平锁 vs. 非公平锁独占锁 (互斥锁) vs. 共享锁 (读写锁)可重入锁 vs. 不可重入锁自旋锁偏向锁 vs. 轻量级锁 vs. 重量级锁 (JVM 锁优化)
1. synchronized 关键字:
类型: 悲观锁,非公平锁,独占锁,可重入锁
特点:
内置锁: 由 JVM 提供,使用简单,隐式加锁和释放锁。互斥性: 保证同一时刻只有一个线程可以执行被 synchronized 修饰的代码块。可见性: 确保在释放锁之前,对共享变量的修改对其他线程可见。可重入性: 允许同一个线程多次获取同一个锁。非公平性: 线程获取锁的顺序是不确定的,可能导致某些线程长时间无法获取锁(饥饿)。自动释放: 无论正常执行完成还是抛出异常,锁都会自动释放。
适用场景: 简单的同步场景,代码量少,竞争不激烈的场景。
示例代码:
public class SynchronizedExample {
private int count = 0;
// 修饰方法
public synchronized void increment() {
count++;
}
// 修饰代码块
public void incrementBlock() {
synchronized (this) {
count++;
}
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
SynchronizedExample example = new SynchronizedExample();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
example.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
example.increment();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Count: " + example.getCount()); // 结果一定是20000
}
}
2. Lock 接口及其实现类:
类型: 悲观锁,可配置公平性,独占锁/共享锁,可重入锁
特点:
手动加锁/解锁: 需要显式调用 lock() 和 unlock() 方法,灵活性更高。
可中断: 可以使用 lockInterruptibly() 方法响应中断,避免线程长时间等待。
可轮询: 可以使用 tryLock() 方法尝试获取锁,如果获取不到立即返回,避免阻塞。
公平性选择: 可以创建公平锁,按照请求顺序获取锁,避免饥饿。
多种实现: 提供了 ReentrantLock(可重入互斥锁)和 ReentrantReadWriteLock(可重入读写锁)等实现。
适用场景: 需要更灵活的锁控制、可中断、可轮询、公平锁等场景。
示例代码(ReentrantLock):
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private int count = 0;
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 加锁
try {
count++;
} finally {
lock.unlock(); // 必须在 finally 中释放锁,确保锁一定会被释放
}
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockExample example = new ReentrantLockExample();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
example.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
example.increment();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Count: " + example.getCount()); // 结果一定是20000
}
}
3. ReadWriteLock 接口及其实现类(ReentrantReadWriteLock):
类型: 悲观锁,可配置公平性,共享锁(读锁)/独占锁(写锁),可重入锁
特点:
读写分离: 允许多个线程同时读取共享资源(读锁),但只允许一个线程写入共享资源(写锁)。
读读共享: 多个线程可以同时持有读锁。
读写互斥/写写互斥: 读锁和写锁、写锁和写锁之间互斥。
提升并发性能: 适用于读多写少的场景,提高并发性能。
适用场景: 读多写少的场景,例如缓存、配置文件、大型数据结构的读取等。
示例代码:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockExample {
private int count = 0;
private ReadWriteLock rwLock = new ReentrantReadWriteLock();
public int getCount() {
rwLock.readLock().lock(); // 获取读锁
try {
return count;
} finally {
rwLock.readLock().unlock(); // 释放读锁
}
}
public void increment() {
rwLock.writeLock().lock(); // 获取写锁
try {
count++;
} finally {
rwLock.writeLock().unlock(); // 释放写锁
}
}
public static void main(String[] args) throws InterruptedException {
ReadWriteLockExample example = new ReadWriteLockExample();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
example.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
example.increment();
}
});
Thread thread3 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
System.out.println("Count: " + example.getCount()); // 多个线程同时读
}
});
thread1.start();
thread2.start();
thread3.start();
thread1.join();
thread2.join();
thread3.join();
System.out.println("Final Count: " + example.getCount()); // 结果一定是20000
}
}
4. StampedLock (Java 8):
类型: 乐观锁/悲观锁,非公平锁,共享锁/独占锁,不可重入锁
特点:
更灵活的读写锁: 提供了三种模式:写锁、悲观读锁和乐观读。
乐观读: 允许在没有写锁的情况下进行读取,但需要在读取完成后验证数据是否被修改过,如果被修改过则需要重试。
避免锁饥饿: 支持读锁升级为写锁。
不可重入: 不支持重入。
适用场景: 读多写少的场景,且对性能要求极高,可以容忍一定的重试开销。
示例代码:
import java.util.concurrent.locks.StampedLock;
public class StampedLockExample {
private int count = 0;
private final StampedLock stampedLock = new StampedLock();
public int getCount() {
long stamp = stampedLock.tryOptimisticRead(); // 尝试乐观读
int currentCount = count;
if (!stampedLock.validate(stamp)) { // 检查读取过程中是否有写操作
stamp = stampedLock.readLock(); // 如果有写操作,则升级为悲观读
try {
currentCount = count;
} finally {
stampedLock.unlockRead(stamp);
}
}
return currentCount;
}
public void increment() {
long stamp = stampedLock.writeLock(); // 获取写锁
try {
count++;
} finally {
stampedLock.unlockWrite(stamp); // 释放写锁
}
}
public static void main(String[] args) throws InterruptedException {
StampedLockExample example = new StampedLockExample();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
example.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
example.increment();
}
});
Thread thread3 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
System.out.println("Count: " + example.getCount()); // 多个线程同时乐观读
}
});
thread1.start();
thread2.start();
thread3.start();
thread1.join();
thread2.join();
thread3.join();
System.out.println("Final Count: " + example.getCount()); // 结果一定是20000
}
}
5. 锁优化(JVM 层面):
偏向锁: 适用于只有一个线程访问同步代码块的场景,消除不必要的锁竞争。轻量级锁: 适用于多个线程交替访问同步代码块的场景,避免使用重量级锁。自旋锁: 当线程尝试获取锁时,如果锁已经被占用,则线程不会立即阻塞,而是循环尝试获取锁,避免线程切换的开销。
锁的选择建议:
简单场景: 优先考虑 synchronized 关键字。需要更多控制: 使用 ReentrantLock。读多写少: 使用 ReentrantReadWriteLock 或 StampedLock。性能极致要求: 评估 StampedLock 的适用性,并进行充分测试。避免长时间持有锁: 尽量缩小锁的范围,减少锁的持有时间,提高并发性能。注意死锁: 避免循环依赖,保证锁的释放。
锁的区别总结:
特性synchronizedReentrantLockReentrantReadWriteLockStampedLock类型内置锁Lock 接口ReadWriteLock 接口类加锁方式隐式显式显式显式释放方式自动手动手动手动公平性非公平可配置可配置非公平可重入性支持支持支持不支持读写分离不支持不支持支持支持 (乐观读)中断等待不支持支持支持不支持性能一般较好读多写少时性能好极致性能 (但需重试)使用复杂度简单复杂复杂复杂注意事项:
死锁: 避免循环依赖的锁获取顺序,确保锁的释放。
性能测试: 在选择锁时,进行性能测试,验证锁的性能是否满足需求。
代码规范: 遵循代码规范,正确使用锁机制,避免出现并发问题。