Java 中已经有 synchronized,为什么还要提供 Lock?

Java 中已经有 synchronized,为什么还要提供 Lock

面向的是“高级并发控制”这类问题:synchronized 能用,但不够精细。Lock 是一把“可编程的锁”。


1. 回顾:synchronized 能做什么?

sychronized 是 Java 最早提供的同步机制,有三种用法:

// 1. 修饰代码块
synchronized (lockObj) {
    // 临界区
}

// 2. 修饰实例方法(锁的是 this)
public synchronized void foo() { ... }

// 3. 修饰静态方法(锁的是 Class 对象)
public static synchronized void bar() { ... }

它的特点:

  1. 互斥:同一把锁同一时刻只允许一个线程进入临界区。
  2. 可重入:同一个线程可以多次获得同一把锁,不会死锁。
  3. 可见性 + 有序性:释放锁前会把工作内存中的数据刷新到主内存,获得锁后会从主内存重新读。
  4. JVM 层实现,语法简单:出错点少。

但是:

  • 一旦阻塞就只能傻等,没有超时、尝试获取的能力。
  • 不支持公平锁(严格按等待顺序获取)。
  • 只能有一个隐式条件队列wait/notify),不能方便地维护多个等待条件。
  • 锁的获取与释放是“语法绑死”的:进入 synchronized 自动加锁,离开块自动释放,无法进行特别灵活的控制。

这些限制在简单并发场景里没问题,但在复杂、高并发框架 / 中间件中就不够用了。


2. Lock 接口是什么?

Lockjava.util.concurrent.locks 包里的一个接口,最常用的实现类是:

  • ReentrantLock:可重入互斥锁
  • ReentrantReadWriteLock:读写锁
  • StampedLock:支持乐观读锁

它把“锁”从 JVM 语法层面抽象成了一个普通对象,可以像操作普通对象一样来控制锁:

Lock lock = new ReentrantLock();

lock.lock();
try {
    // 临界区
} finally {
    lock.unlock();
}

这个写法和 synchronized 相比,更啰嗦,但换来了更大的控制力


3. Lock 相对于 synchronized 的核心优势

3.1 支持可中断锁获取(lockInterruptibly

问题场景:
线程 A 在拿锁时堵住了,这时你想取消这个线程(比如用户取消了操作),synchronized 做不到,只能一直等锁。

Lock 提供:

lock.lockInterruptibly();

如果线程在等待锁的过程中被 interrupt(),会立刻抛出 InterruptedException,线程可以优雅退出。

适用场景:

  • 做 RPC、远程调用、数据库操作等时,如果锁等待过长且业务允许取消,就可以用可中断锁避免线程“僵死”。

3.2 支持尝试获取 / 超时获取(tryLock

问题场景:
一段代码想拿锁,但如果一时拿不到,不想死等,可以:

  • 马上放弃做别的事情;
  • 或者只等待一段时间,超时就算了。

Lock 提供:

if (lock.tryLock()) {
    try {
        // 获取到锁,执行逻辑
    } finally {
        lock.unlock();
    }
} else {
    // 没拿到锁,做降级、返回友好提示等
}

支持超时版本:

if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
    try {
        // 半秒内拿到锁
    } finally {
        lock.unlock();
    }
} else {
    // 半秒没拿到锁,放弃
}

适用场景:

  • 高并发下避免“锁饥饿”:拿不到就快速返回,不阻塞线程池。
  • 做限流、降级时用“试探性”获取锁。

sychronized 完全不支持这些细粒度策略。


3.3 支持公平锁(new ReentrantLock(true)

sychronized 的锁是非公平锁:后来的线程可能插队先拿到锁,吞吐量更高,但可能造成部分线程长期拿不到锁(饥饿)。

Lock 允许你选择:

Lock fairLock = new ReentrantLock(true);  // 公平锁
Lock unfairLock = new ReentrantLock(false); // 默认非公平

公平锁会按等待时间顺序唤醒线程,类似排队叫号,牺牲吞吐,换取更可预期的调度。

适用场景:

  • 业务对“公平性”“请求顺序”要求很高的地方(如队列、排队系统)。

3.4 支持多个条件队列(Condition

synchronized + wait/notify 有几个问题:

  • 每个锁只有一个等待队列,所有等待都混在一起。
  • notify 叫醒的是“某一个”线程,不可控。
  • notifyAll 又容易叫醒一堆不相关的线程,造成“惊群效应”。

Lock 提供 Condition

Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();

你可以:

// 线程 A 等待 notEmpty 条件
lock.lock();
try {
    while (queue.isEmpty()) {
        notEmpty.await();
    }
    // 取元素
} finally {
    lock.unlock();
}

// 线程 B 生产元素后只唤醒等待 notEmpty 的线程
lock.lock();
try {
    queue.add(xxx);
    notEmpty.signal(); // 或 signalAll()
} finally {
    lock.unlock();
}

这样就可以把“等待队列”按业务拆分:

  • 等待“队列不空”的一组线程;
  • 等待“队列不满”的另一组线程;

不同条件互不影响,逻辑更清晰。

适用场景:

  • 自己实现阻塞队列、连接池、资源池等复杂并发容器时。

3.5 支持读写锁、乐观读等高级玩法

Lock 体系中还有:

  • ReentrantReadWriteLock:读写分离
    • 多线程可以并发进行,读写、写写互斥。
    • 读多写少场景下大幅提高吞吐量。
  • StampedLock:提供乐观读锁
    • 读的时候可以“大胆假设数据不变”,先乐观读,读完检查一下是否被写入修改,没被改则直接用。

synchronized 无法表达这些高级策略。


3.6 锁释放更灵活(但也更危险)

synchronized 的一个优点:离开代码块就自动释放锁,不会忘记。
Lock 则是手动释放:

lock.lock();
try {
    // ...
} finally {
    lock.unlock();  // 必须写
}

这在某些场景很有用:

  • 可以把 unlock() 放在不同的方法里,实现更灵活的生命周期控制;
  • 可以在同一方法中多次 lock/unlock 控制临界区范围;

但也有风险:

  • 忘记写 unlock(),就会导致死锁或长期占用锁。

4. synchronizedLock 的对比总结

维度synchronizedLock(如 ReentrantLock
实现层JVM 关键字Java 层接口 + 实现类
可重入支持支持(ReentrantLock
公平锁不支持支持(构造函数可选公平)
可中断获取锁不支持支持 lockInterruptibly()
尝试/超时获取锁不支持支持 tryLock() / tryLock(timeout)
条件队列只有一个(wait/notify支持多个 Condition
读写锁 / 乐观读不支持支持(ReentrantReadWriteLock, StampedLock
性能JDK8 之后整体很优秀,大量优化实现灵活,部分场景性能更好
锁释放自动(离开作用域)手动 unlock(),易出错
使用难度简单,适合一般业务逻辑较复杂,适合框架 / 中间件 / 高级并发控制

简单记忆:

  • 能用 synchronized 搞定的,就别上 Lock
  • 但一旦遇到:
    • 需要“可中断/可超时”的获取锁;
    • 需要读写分离;
    • 需要多个条件队列;
    • 需要公平锁;

这时候就该上 Lock 系列。


5. 示例对比:阻塞队列实现

5.1 用 synchronized + wait/notify

public class MyBlockingQueueWithSync<E> {
    private final Queue<E> queue = new LinkedList<>();
    private final int capacity;

    public MyBlockingQueueWithSync(int capacity) {
        this.capacity = capacity;
    }

    public synchronized void put(E e) throws InterruptedException {
        while (queue.size() == capacity) {
            wait(); // 队满,等待
        }
        queue.add(e);
        notifyAll(); // 通知消费者可能有元素了
    }

    public synchronized E take() throws InterruptedException {
        while (queue.isEmpty()) {
            wait(); // 队空,等待
        }
        E e = queue.remove();
        notifyAll(); // 通知生产者可能有空间了
        return e;
    }
}

问题:一个 wait() 队列,notifyAll() 会惊扰所有等待的线程,效率较低。

5.2 用 ReentrantLock + Condition

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyBlockingQueueWithLock<E> {
    private final Queue<E> queue = new LinkedList<>();
    private final int capacity;

    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    public MyBlockingQueueWithLock(int capacity) {
        this.capacity = capacity;
    }

    public void put(E e) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == capacity) {
                notFull.await(); // 只在“队满队列”里等待
            }
            queue.add(e);
            notEmpty.signal(); // 唤醒一个“队空队列”的等待者
        } finally {
            lock.unlock();
        }
    }

    public E take() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                notEmpty.await(); // 只在“队空队列”里等待
            }
            E e = queue.remove();
            notFull.signal(); // 唤醒一个“队满队列”的等待者
            return e;
        } finally {
            lock.unlock();
        }
    }
}

这里:

  • 生产者只在 notFull 队列里等待/被唤醒;
  • 消费者只在 notEmpty 队列里等待/被唤醒;
  • 没有无关线程被唤醒,效率更高。

实际 JDK 的 ArrayBlockingQueueLinkedBlockingQueue 就是基于 ReentrantLock + Condition 实现的。


6. 实战选择建议

  1. 默认优先用 synchronized

    • 业务代码里大多数“给某个方法/代码块加锁”的需求,synchronized 足够稳、简单、难出 bug。
  2. 在这些场景考虑 Lock

    • 需要:
      • 尝试获取锁 / 超时获取锁;
      • 可中断的锁获取;
      • 公平锁;
      • 多个条件队列;
      • 读写锁、乐观读;
    • 或者你在实现:
      • 线程池、队列、连接池、缓存容器等高并发基础组件;
      • 对性能、调度公平性要求比较高的底层模块。
  3. 使用 Lock 时的注意点:

    • lock() 后,一定要在 finallyunlock()
      lock.lock();
      try {
          // ...
      } finally {
          lock.unlock();
      }
      
    • 搭配 tryLock 使用时,只有在 true 分支里才 unlock()
    • 注意死锁风险:多个锁要按固定顺序获取。

7. 总结一句话

Java 提供 Lock,不是为了替代 synchronized,而是为了补齐它在“可中断、可超时、公平性、多条件队列、读写分离”等高级并发场景里的短板。简单用 synchronized,复杂用 Lock

<think>我们被要求回答Java并发编程中synchronized和ReentrantLock的区别,并在最后提出3-5个相关问题。首先,我们需要整理两者的主要区别点,然后按照要求输出相关问题。注意:回答末尾必须包含“§§相关问题§§”且在第一行,然后列出3-5个问题,禁止换行分散。</think>### synchronized与ReentrantLock区别分析 #### 1. **机制本质** | 特性 | synchronized | ReentrantLock | |--------------|------------------------------------------------------------------------------|-------------------------------------------------------------------------------| | **实现原理** | JVM内置关键字(监视器) | JDK提供的API(java.util.concurrent包) | | **释放** | 自动释放(代码块结束/异常抛出) | 必须手动调用unlock()(需配合try-finally) | | **类型** | 非公平 | 可配置公平/非公平(构造函数参数控制) | #### 2. **功能差异** - **等待可中断** - `synchronized`:线程阻塞不可中断 - `ReentrantLock`:支持`lockInterruptibly()`方法,可响应中断请求 ```java // ReentrantLock中断示例 ReentrantLock lock = new ReentrantLock(); try { lock.lockInterruptibly(); // 可被Thread.interrupt()中断 // 临界区代码 } catch (InterruptedException e) { // 处理中断 } finally { lock.unlock(); } ``` - **条件队列** - `synchronized`:单一等待队列(wait/notify) - `ReentrantLock`:支持多个Condition条件队列 ```java Condition notFull = lock.newCondition(); Condition notEmpty = lock.newCondition(); ``` - **尝试机制** - `synchronized`:无尝试获取能力 - `ReentrantLock`:提供`tryLock()`(立即返回)和`tryLock(timeout, unit)`(超时等待) #### 3. **性能对比** - JDK6+优化后两者性能接近 - 高竞争场景下: - `ReentrantLock`:吞吐量更高(尤其公平模式) - `synchronized`:内存消耗更低(JVM层优化) #### 4. **使用场景** - **优先选择synchronized**:简单同步场景(如单方法同步) - **选择ReentrantLock**当需要: - 细粒度控制(如超时、可中断) - 公平需求 - 多条件等待(如生产者-消费者模型) > **最佳实践**:默认使用synchronized,仅在需要高级功能时使用ReentrantLock,避免手动管理风险。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值