在 Java 并发编程体系中,synchronized、ReentrantLock、Semaphore(信号量)是控制线程竞争、实现资源隔离的三大核心工具。其中 synchronized 是 JVM 原生锁,ReentrantLock 是 JDK 层面的可重入显式锁,二者主要用于独占式资源竞争;而 Semaphore(信号量)是共享式限流工具,支持多线程同时访问资源。

很多开发者仅停留在“synchronized 简单、Lock 灵活”的浅层认知,实际开发中经常出现锁使用不当、并发限流失效、死锁等问题。本文将从底层原理、核心特性、使用场景、代码实战、优缺点对比、适用场景选型六个维度,彻底讲透三者的区别与用法,帮你精准适配各类并发场景。

一、synchronized:JVM 原生隐式锁

1. 核心定义

synchronized 是 Java 内置关键字,底层基于C++实现,属于隐式锁,锁的获取和释放完全由 JVM 自动控制,无需手动干预。它可以修饰方法、代码块,主要用于保证原子性、可见性、有序性三大并发特性,是解决线程安全问题的基础方案。

2. 底层原理

synchronized 底层依赖 JVM 监视器(Monitor)实现,基于 对象头 Mark Word 存储锁状态,存在四种锁升级状态(无锁 → 偏向锁 → 轻量级锁 → 重量级锁),锁升级过程不可逆,有效降低了锁竞争的性能开销:

  • 偏向锁:单线程访问资源时,JVM 记录线程 ID,无锁竞争,性能极高

  • 轻量级锁:少量线程交替竞争,基于 CAS 自旋实现,无阻塞

  • 重量级锁:多线程激烈竞争,线程阻塞挂起,依赖操作系统内核实现,性能最低

3. 核心特性

  • 隐式加锁解锁:进入同步代码块自动加锁,退出代码块/方法、抛出异常时自动释放锁,无需手动操作

  • 天然可重入:同一线程可多次获取同一把锁,不会出现自死锁

  • 默认非公平锁:锁释放后,后续竞争线程随机抢占,不保证线程执行顺序

  • 不可中断:线程阻塞等待锁时,无法被中断,只能一直等待获取锁

  • 不支持超时等待:没有超时机制,竞争不到锁会永久阻塞

4. 代码实战

// 修饰代码块(推荐,锁粒度更细)
public class SyncDemo {
    private int count = 0;
    private final Object lock = new Object();

    public void increment() {
        // 隐式加锁
        synchronized (lock) {
            count++;
        }
        // 隐式解锁
    }
}

// 修饰实例方法(锁当前对象 this)
public synchronized void add() {
    count++;
}

// 修饰静态方法(锁类对象)
public static synchronized void staticAdd() {
    count++;
}

5. 优缺点总结

优点:使用简单、零代码侵入、JVM 底层优化完善、无需手动释放锁、不会出现锁泄漏。

缺点:灵活性极差、不支持中断、不支持超时、不支持公平锁手动配置、锁粒度无法精细化控制,激烈竞争下性能较差。

二、ReentrantLock:JDK 显式可重入锁

1. 核心定义

ReentrantLock 是 java.util.concurrent.locks 包下的显式锁,基于 AQS(AbstractQueuedSynchronizer)队列同步器实现,是 synchronized 的增强替代品。需要开发者手动调用加锁、解锁方法,拥有远超 synchronized 的灵活度。

2. 底层原理

核心基于 AQS 实现:通过 state 状态值 记录锁持有状态(0 无锁、>0 已加锁,可重入次数叠加),通过 CLH 双向队列 存储阻塞等待的线程,线程竞争失败后进入队列排队,锁释放后唤醒队列头部线程竞争锁。

3. 核心特性(核心优势)

  • 显式加锁解锁:手动 lock() 加锁、unlock() 解锁,粒度完全可控

  • 可重入:和 synchronized 一致,同一线程可多次获取锁

  • 支持公平/非公平锁:构造方法传 true 为公平锁(线程排队有序执行),默认非公平

  • 可中断锁等待:lockInterruptibly() 方法可响应线程中断,避免永久阻塞

  • 超时锁机制:tryLock(timeout) 可设置等待超时时间,超时直接放弃竞争

  • 绑定多个条件变量:可通过 newCondition() 创建多个 Condition,精准唤醒指定线程,精准控制线程等待/唤醒

4. 代码实战(含核心用法)

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class LockDemo {
    private int count = 0;
    // 可指定公平/非公平锁:true=公平,false=非公平(默认)
    private final ReentrantLock lock = new ReentrantLock(true);
    // 自定义条件变量
    private final Condition condition = lock.newCondition();

    // 基础加锁
    public void increment() {
        lock.lock(); // 手动加锁
        try {
            count++;
        } finally {
            lock.unlock(); // 必须在finally手动解锁,防止锁泄漏
        }
    }

    // 超时等待锁
    public boolean tryIncrement() throws InterruptedException {
        // 等待1秒,获取不到锁直接返回false
        if (lock.tryLock(1000, java.util.concurrent.TimeUnit.MILLISECONDS)) {
            try {
                count++;
                return true;
            } finally {
                lock.unlock();
            }
        }
        return false;
    }

    // 精准线程等待唤醒
    public void waitThread() throws InterruptedException {
        lock.lock();
        try {
            condition.await(); // 线程等待
        } finally {
            lock.unlock();
        }
    }

    public void signalThread() {
        lock.lock();
        try {
            condition.signal(); // 精准唤醒等待线程
        } finally {
            lock.unlock();
        }
    }
}

5. 优缺点总结

优点:灵活性极强、支持公平锁、中断等待、超时机制、多条件精准唤醒,高并发场景性能优于 synchronized。

缺点:需要手动加锁解锁,编码繁琐;必须在 finally 解锁,否则极易出现锁泄漏;需要开发者手动控制锁逻辑,容错率低。

三、Semaphore:信号量

1. 核心定义

Semaphore(信号量)同样基于 AQS 实现,和前两者独占锁不同,它是共享式同步工具。核心作用是控制并发访问资源的线程数量,实现限流、资源池控制、接口限流等场景。

简单理解:synchronized/ReentrantLock 是 一把锁,同一时间只允许1个线程执行;Semaphore 是 多个通行证,同一时间允许N个线程并行执行。

2. 底层原理

基于 AQS 的共享模式实现:初始化时设置许可数量(permits),对应 AQS 的 state 值。线程调用 acquire() 申请许可,state 减 1;线程执行完毕调用 release() 释放许可,state 加 1。当 state=0 时,后续线程阻塞等待,直到有许可释放。

3. 核心特性

  • 共享式并发控制:支持多线程同时访问资源,核心用于限流而非互斥

  • 支持公平/非公平模式:可指定线程排队规则

  • 可批量申请/释放许可:支持一次性获取/释放多个许可

  • 支持中断、超时机制:acquire() 可响应中断,tryAcquire() 支持超时

  • 无锁重入概念:核心是许可计数,不区分线程,可重复申请

4. 常用核心方法

方法

作用

acquire()

获取1个许可,无许可则阻塞,可响应中断(P操作)

acquire(int n)

批量获取n个许可

tryAcquire()

尝试获取许可,获取失败直接返回false,不阻塞

tryAcquire(timeout)

超时获取许可,超时未获取则放弃

release()

释放1个许可(V操作)

release(int n)

批量释放n个许可

5. 代码实战:接口限流

import java.util.concurrent.Semaphore;

public class SemaphoreDemo {
    // 初始化许可数:同一时间最多允许5个线程并发执行(限流5)
    private static final Semaphore SEMAPHORE = new Semaphore(5);

    public static void main(String[] args) {
        // 模拟20个并发请求
        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                try {
                    // 申请许可
                    SEMAPHORE.acquire();
                    System.out.println(Thread.currentThread().getName() + " 执行任务,当前并发数:" + (5 - SEMAPHORE.availablePermits()));
                    // 模拟业务耗时
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 释放许可
                    SEMAPHORE.release();
                }
            }, "线程" + i).start();
        }
    }
}

运行效果:始终最多5个线程同时执行,完美实现接口、资源、数据库连接池的并发限流。

6. 典型使用场景

  • 接口限流、秒杀限流(限制最大并发请求数)

  • 资源池控制(数据库连接池、线程池、硬件资源占用)

  • 多线程分段任务、并行任务控速

  • 实现简单的生产者消费者模型

四、synchronized vs ReentrantLock 核心深度对比

二者核心定位一致:独占互斥锁,保证同一时间只有一个线程执行临界区代码,解决线程安全问题,核心差异在灵活性、性能、功能特性。

对比维度

synchronized

ReentrantLock

实现方式

JVM 原生隐式锁,关键字级别

JDK 显式锁,基于 AQS 实现

加锁解锁

自动加锁、自动解锁,无需手动操作

手动 lock()、unlock(),需 finally 释放

锁类型

默认非公平锁,不支持公平锁

支持公平/非公平锁,可自由配置

中断等待

不支持,阻塞线程无法被中断

支持 lockInterruptibly() 中断等待

超时机制

不支持,永久阻塞等待

支持 tryLock() 超时放弃竞争

条件变量

只有1个隐式条件,只能整体唤醒/随机唤醒

支持多 Condition,精准唤醒指定线程

可重入性

支持可重入

支持可重入

性能

JDK1.6 后优化极高,低竞争场景性能优秀

高竞争场景性能优于 synchronized

容错性

极高,无锁泄漏风险

低,手动解锁失误会导致锁泄漏、死锁

五、三者核心定位与选型区别(重点)

很多开发者混淆三者的核心用途,本质区别一句话总结:

  • synchronized:简单独占互斥,低并发、简单线程安全场景首选

  • ReentrantLock:复杂独占互斥,高并发、需要灵活锁控制场景首选

  • Semaphore:共享限流,控制并发线程数,不做互斥,只做流量控制

最终选型规则

  1. 简单线程安全、代码简洁优先:无脑用 synchronized(普通变量计数、简单同步方法)

  2. 需要公平锁、超时、中断、精准线程唤醒:必须用 ReentrantLock

  3. 需要控制并发数量、接口限流、资源池管控:必须用 Semaphore

  4. 绝对不要混用:独占锁用于“互斥排他”,信号量用于“并发限流”,场景完全不同

六、常见面试高频问题总结

  1. synchronized 和 ReentrantLock 区别? 核心是隐式/显式、功能灵活性、公平锁、中断、超时机制的差异,上文对比表全覆盖。

  2. 为什么 ReentrantLock 要在 finally 解锁? 防止业务代码异常导致无法解锁,造成锁泄漏,后续线程永久阻塞。

  3. Semaphore 和锁的本质区别? 锁是独占模式(1线程执行),Semaphore 是共享模式(N线程并行),用于限流而非互斥。

  4. Semaphore 许可数可以设置为1吗? 可以,此时等价于非公平独占锁,但性能和功能不如原生锁,不推荐使用。

  5. 可重入锁的意义? 避免同一线程多次获取同一锁时出现自死锁,适配嵌套同步场景。

七、全文总结

1. synchronized:JVM 原生隐式独占锁,简单稳定、零出错成本,适配低并发、简单互斥场景,是日常开发首选基础锁。

2. ReentrantLock:AQS 实现的显式独占锁,功能全面、灵活性拉满,适配高并发、复杂锁控制场景,是 synchronized 的增强替代方案。

3. Semaphore:AQS 共享式同步工具,核心能力是并发限流,用于控制并行线程数,和互斥锁无竞争关系,是接口限流、资源管控的核心工具。

掌握三者的底层原理和场景差异,才能在并发开发中精准选型,既避免过度设计,也能解决高并发下的线程安全、限流、阻塞等核心问题。

Logo

一站式 AI 云服务平台

更多推荐