锁机制入门: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();
        }
    }
}

使用场景很明确:

  • 读多写少:比如配置中心、缓存系统
  • 读操作耗时较长:比如从数据库加载数据
  • 对数据一致性要求不是极端严格

我曾经踩过的坑:读写锁的「锁升级」问题。读锁持有期间,不能直接获取写锁。否则会导致死锁。我早期写代码时,在读取数据发现过期后,直接尝试获取写锁更新,结果线程全卡死了。正确的做法是:先释放读锁,再获取写锁。

四、知识体系总览

下面这张图帮你理清本章的核心脉络:

锁机制知识体系 Java锁机制 synchronized 隐式获取/释放 不可中断/无超时 非公平锁 Lock接口 显式lock/unlock 可中断/可超时 公平/非公平可选 读写锁 读读不互斥 读写/写写互斥 读多写少场景 可重入特性:所有锁都支持同一线程重复获取

五、选型建议

说了这么多,到底怎么选?我总结几条实战经验:

  1. 99%的场景用synchronized就够了。别过度设计。
  2. 需要超时控制时,用Lock的tryLock。比如调用外部接口,不能无限等锁。
  3. 读远多于写,考虑读写锁。但要注意锁升级的坑。
  4. 需要多个条件变量,用Lock+Condition。比wait/notify灵活得多。

我的个人习惯:新项目里,我默认用synchronized。等压测发现锁竞争严重,再针对性优化成Lock或读写锁。过早优化是万恶之源。

嗯,锁机制入门就聊到这儿。记住一句话:锁是手段,不是目的。能用无锁设计就用无锁,实在不行再上锁。

可可精品资料库