锁机制入门:synchronized与Lock的区别,可重入锁、读写锁的使用场景
多线程开发中,锁是最基础也最容易踩坑的地方。我刚开始做并发编程时,总觉得锁就是加个synchronized完事。直到线上出了几次死锁,才老老实实把锁机制重新学了一遍。
今天咱们聊聊Java里两种核心的锁:synchronized和Lock。还有两个高频使用的锁类型——可重入锁和读写锁。
一、synchronized vs Lock:到底选哪个?
先看个对比表格,心里有个底:
| 对比维度 | synchronized | Lock |
|---|---|---|
| 获取锁方式 | 隐式,自动获取释放 | 显式,需手动lock/unlock |
| 可中断性 | 不支持 | 支持(lockInterruptibly) |
| 超时机制 | 不支持 | 支持(tryLock(timeout)) |
| 公平性 | 非公平 | 可设置公平/非公平 |
| 条件变量 | wait/notify | Condition(更灵活) |
| 性能(低竞争) | 较好(偏向锁优化) | 一般 |
| 性能(高竞争) | 较差(重量级锁) | 较好 |
说白了,synchronized是Java内置的语法糖,简单粗暴。Lock是JDK提供的工具类,功能更强大。
我个人习惯:能用synchronized解决的场景,优先用synchronized。代码更干净,不容易忘释放锁。但遇到需要超时、可中断、或者多个条件变量的场景,我会换成Lock。
核心原则:简单场景用synchronized,复杂场景用Lock。别为了炫技而用Lock。
二、可重入锁:同一个线程能反复进
可重入锁,字面意思就是「可以重新进入」。同一个线程在持有锁的情况下,可以再次获取同一把锁,不会死锁。
举个例子:
public class ReentrantDemo {
public synchronized void methodA() {
System.out.println("进入methodA");
methodB(); // 同一个线程,再次获取锁
}
public synchronized void methodB() {
System.out.println("进入methodB");
}
}
如果synchronized不可重入,methodA调用methodB时就会死锁。但Java的synchronized和ReentrantLock都是可重入的。
我在项目中遇到过:一个递归调用的缓存更新方法,每次递归都要加锁。如果锁不可重入,第一层递归就把自己卡死了。可重入锁完美解决了这个问题。
小提示:可重入锁内部维护了一个计数器。每次获取锁+1,释放锁-1。只有计数器归零,锁才真正释放。
三、读写锁:读读不互斥,读写互斥
读写锁的核心思想:读操作可以并发,写操作必须独占。
你想想看,如果100个线程都在读数据,用普通的互斥锁,大家排队读,多浪费。读写锁允许读读并行,只有读写或写写时才互斥。
Java里用ReentrantReadWriteLock实现:
public class ReadWriteCache {
private final Map<String, Object> cache = new HashMap<>();
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
public Object get(String key) {
readLock.lock();
try {
return cache.get(key);
} finally {
readLock.unlock();
}
}
public void put(String key, Object value) {
writeLock.lock();
try {
cache.put(key, value);
} finally {
writeLock.unlock();
}
}
}
使用场景很明确:
- 读多写少:比如配置中心、缓存系统
- 读操作耗时较长:比如从数据库加载数据
- 对数据一致性要求不是极端严格
我曾经踩过的坑:读写锁的「锁升级」问题。读锁持有期间,不能直接获取写锁。否则会导致死锁。我早期写代码时,在读取数据发现过期后,直接尝试获取写锁更新,结果线程全卡死了。正确的做法是:先释放读锁,再获取写锁。
四、知识体系总览
下面这张图帮你理清本章的核心脉络:
五、选型建议
说了这么多,到底怎么选?我总结几条实战经验:
- 99%的场景用synchronized就够了。别过度设计。
- 需要超时控制时,用Lock的tryLock。比如调用外部接口,不能无限等锁。
- 读远多于写,考虑读写锁。但要注意锁升级的坑。
- 需要多个条件变量,用Lock+Condition。比wait/notify灵活得多。
我的个人习惯:新项目里,我默认用synchronized。等压测发现锁竞争严重,再针对性优化成Lock或读写锁。过早优化是万恶之源。
嗯,锁机制入门就聊到这儿。记住一句话:锁是手段,不是目的。能用无锁设计就用无锁,实在不行再上锁。