欢迎光临
我们一直在努力

一篇讲透各种锁(详细,附代码)

核心概念:

  • 原子性:一组操作不可分割,要么全部执行完,要么不执行。
  • 可见性:一个线程修改共享变量,其他线程立刻感知。
  • 有序性:禁止 JVM/CPU 指令重排序。

一、按并发策略:悲观锁 vs 乐观锁【思想层面】

1. 悲观锁

思想:默认一定会发生并发冲突,访问资源先上锁,锁住资源,别人不许碰。 只要操作共享资源,先拿到锁,其他线程阻塞等待。

Java 中代表实现:

  • synchronized
  • ReentrantLock、ReentrantReadWriteLock写锁

数据库代表: select … for update 行级悲观锁。

代码示例

ReentrantLock(悲观锁)

ReentrantLock lock = new ReentrantLock();
lock.lock(); //上来直接加锁,假设会有竞争
try{
count++;
}finally {
lock.unlock();
}

synchronized 内置锁

synchronized锁的是对象,不是代码。

⚠️重点:锁对象必须是同一个,才能互斥。

三种使用方式

  • 修饰实例方法:锁的是this 当前对象
  • 修饰静态方法:锁的是类的 Class 对象
  • 修饰代码块:手动指定锁对象 synchronized(锁对象){}
  • 示例 1:修饰实例方法

    public class SyncDemo {
    private int count = 0;

    //锁:this对象
    public synchronized void add() {
    count++;
    }

    public static void main(String[] args) throws InterruptedException {
    SyncDemo demo = new SyncDemo();
    Thread t1 = new Thread(() -> {
    for (int i = 0; i < 10000; i++) {
    demo.add();
    }
    });
    Thread t2 = new Thread(() -> {
    for (int i = 0; i < 10000; i++) {
    demo.add();
    }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println("count = " + demo.count); //输出20000,线程安全
    }
    }

    原理:t1、t2 操作同一个demo对象,add 方法上加synchronized,锁 this,同一时刻只能一个线程进入 add。

    ❌坑:new 两个不同对象,锁失效!

    SyncDemo d1 = new SyncDemo();
    SyncDemo d2 = new SyncDemo();
    //两个对象,两把不同的锁,互不干扰,并发会出现count小于20000

    示例 2:synchronized 代码块(推荐,粒度可控)

    ⚠️:只锁需要保护的临界区,不要锁整个大方法,减少锁持有时间,提升并发性能。

    //抢票例子
    public class TicketRightDemo {
    private int count = 10;

    public void saleTicket() {
    while (true) {
    //锁对象 this,两个线程使用同一个对象锁
    synchronized (this) {
    // 在锁内部做库存判断
    if (count <= 0) {
    break; //库存为0,退出循环
    }
    System.out.println(Thread.currentThread().getName() + "抢到票:" + count);
    count–;
    }
    }
    }

    public static void main(String[] args) {
    TicketRightDemo task = new TicketRightDemo();
    new Thread(task::saleTicket, "窗口A").start();
    new Thread(task::saleTicket, "窗口B").start();
    }
    }

    输出示例:

    窗口A抢到票:10
    窗口A抢到票:9
    窗口B抢到票:8
    窗口A抢到票:7
    窗口B抢到票:6
    窗口A抢到票:5
    窗口B抢到票:4
    窗口A抢到票:3
    窗口B抢到票:2
    窗口A抢到票:1

    库存不会出现负数,不会超卖。

    正确写法:判断 + 修改共享变量全部放入同步块

    示例 3:synchronized 修饰 static 静态方法

    锁对象是SyncDemo.class类对象,所有实例共用一把锁。

    public static synchronized void staticAdd(){
    count++;
    }

    等价:

    public static void staticAdd(){
    synchronized (SyncDemo.class){
    count++;
    }
    }

    特点:

    • 冲突多的时候性能稳定;竞争激烈线程会阻塞,带来线程切换开销。

    2. 乐观锁

    思想:默认不会发生冲突,不上锁;操作完成时检查是否发生冲突;冲突则重试。 不加真正互斥锁,依靠版本号 / CAS 做冲突检测。

    Java 代表:AtomicXXX 原子类(CAS 实现) 数据库代表:版本号机制乐观锁

    示例 1:AtomicInteger CAS 乐观锁

    AtomicInteger count = new AtomicInteger(0);
    count.incrementAndGet(); //CAS,不加锁,冲突自动重试

    示例 2:数据库乐观锁伪代码

    — version版本号,更新时校验版本
    update goods set stock = stock‑1, version = version+1
    where id=100 and version = #{oldVersion};

    如果返回影响行数 0,代表已经被其他线程修改,发生冲突,业务重试。

    乐观锁优缺点:

    ✅没有阻塞,没有线程切换开销;适合冲突少场景。

    ❌冲突多的时候大量自旋重试,CPU 暴涨;只能保证单个变量原子,不能保护一段业务逻辑。

    重点区分:

    悲观锁:先上锁,再干活;

    乐观锁:先干活,最后校验冲突,失败重试。

    二、按资源访问方式:独占锁 (排他 / 互斥锁) VS 共享锁

    1. 独占锁(排他锁、互斥锁)

    同一时间只允许一个线程持有锁;其他线程全部阻塞。

    实现:

  • synchronized
  • ReentrantLock
  • ReentrantReadWriteLock.WriteLock 写锁
    • 独占:强调资源被一个线程占有
    • 排他:排斥其他线程进来
    • 互斥:线程之间互相排斥

    代码 ReentrantLock,独占锁示例

    ReentrantLock lock = new ReentrantLock();
    //同一时刻只能一个线程获取
    lock.lock();
    try{
    //临界区
    }finally {
    lock.unlock();
    }

    2. 共享锁

    多个线程可以同时获取锁,并发读;

    典型:读写锁的读锁。 ReentrantReadWriteLock.ReadLock 读锁。

    规则:

    • 读锁是共享锁:读‑读可以并发
    • 读锁和写锁互斥;写锁和写锁互斥

    ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();

    //多个线程可以同时拿到读锁,并发读取
    readLock.lock();
    try{
    int val = data;
    }finally {
    readLock.unlock();
    }

    读写锁 ReentrantReadWriteLock(读共享,写独占)

    场景是否允许
    读线程 + 读线程 ✅ 共享,并发执行
    读线程 + 写线程 ❌互斥,写等待读释放 / 读等待写释放
    写线程 + 写线程 ❌互斥

    缺陷:大量读线程持续占读锁,写线程一直拿不到写锁 → 写饥饿。

    三、按获取锁排队策略:公平锁 VS 非公平锁

    只针对 AQS 实现的锁(ReentrantLock);synchronized 只有非公平锁。

    1. 公平锁

    线程按照请求锁的先后顺序排队;先来先得,不允许插队。

    //true 开启公平锁
    ReentrantLock fairLock = new ReentrantLock(true);

    • 优点:不会出现线程饥饿,所有线程都有机会拿到锁。
    • 缺点:线程切换多,性能较低。

    2. 非公平锁(ReentrantLock 默认,synchronized 也是)

    新线程上来直接尝试抢锁,可以插队,不一定排队。

    //false 默认非公平锁
    ReentrantLock lock = new ReentrantLock(false);

    • 优点:减少线程挂起唤醒,性能高。
    • 缺点:有可能老线程一直抢不到锁,线程饥饿。

    ⚠️synchronized 只有非公平锁,没有公平锁选项。

    代码对比

    //公平锁
    ReentrantLock lock = new ReentrantLock(true);
    //非公平锁(默认)
    ReentrantLock lock = new ReentrantLock();

    更加详细例子

    开 5 个线程,每个线程抢锁多次,打印线程名称,观察输出顺序。

    import java.util.concurrent.locks.ReentrantLock;

    public class FairVsUnFairLockDemo {

    // true=公平锁;false=非公平锁(默认)
    private static final ReentrantLock lock = new ReentrantLock(true);
    // private static final ReentrantLock lock = new ReentrantLock(false);

    public static void main(String[] args) {
    // 创建5个线程
    for (int i = 1; i <= 5; i++) {
    final int threadNo = i;
    new Thread(() -> {
    // 每个线程抢锁5次
    for (int j = 0; j < 5; j++) {
    lock.lock();
    try {
    System.out.printf("【%s】拿到锁,执行任务%n", Thread.currentThread().getName());
    //模拟业务耗时,短暂持有锁
    Thread.sleep(10);
    } catch (InterruptedException e) {
    e.printStackTrace();
    } finally {
    lock.unlock();
    }
    }
    }, "线程‑" + threadNo).start();
    }
    }
    }

    ✅打开公平锁 new ReentrantLock(true) 现象

    输出大致顺序:线程会交替按顺序执行,不会一个线程连续多次霸占锁。

    【线程‑1】拿到锁,执行任务
    【线程‑2】拿到锁,执行任务
    【线程‑3】拿到锁,执行任务
    【线程‑4】拿到锁,执行任务
    【线程‑5】拿到锁,执行任务
    【线程‑1】拿到锁,执行任务
    【线程‑2】拿到锁,执行任务
    ……

    公平锁 AQS 内部维护等待队列;锁释放之后,优先唤醒队列头部等待最久的线程;新到来线程不会尝试插队抢锁,直接进队列排队。

    ✅打开非公平锁 new ReentrantLock(false)(默认)现象

    经常出现:同一个线程连续多次拿到锁(插队)。

    【线程‑1】拿到锁,执行任务
    【线程‑1】拿到锁,执行任务
    【线程‑1】拿到锁,执行任务
    【线程‑2】拿到锁,执行任务
    【线程‑3】拿到锁,执行任务
    【线程‑3】拿到锁,执行任务
    ……

    为什么连续拿到?

    线程 1 释放锁之后,还没等队列里沉睡的线程被唤醒,线程 1 自己又立刻去抢锁,CAS 抢占成功,直接再次拿到锁,不用进等待队列。 队列里其他线程还在休眠,还没被唤醒,就被新的抢占抢走了机会,这就是插队。

    完整过程拆解(非公平锁):

  • 线程 1 持有锁,线程 2、3 在 AQS 等待队列休眠阻塞。
  • 线程 1 执行完,调用unlock()释放锁。
  • unlock 内部尝试唤醒队列里的线程 2,唤醒需要操作系统做线程切换,有时间延迟。
  • 在操作系统还没把线程 2 唤醒起来的一瞬间,线程 1 循环再次执行lock(),直接 CAS 抢锁。
  • 此时锁刚好空闲,CAS 成功,线程 1 又拿到锁继续执行。
  • 等到后面线程 2 终于被唤醒,发现锁已经被拿走,只能再次进队列休眠。
  • 多次循环,就看到日志里:线程 1 连续多次拿到锁。

    这就是非公平锁的插队机制;

    好处:避免唤醒其他线程的开销,性能高;

    坏处:队列老线程反复抢不到锁,发生线程饥饿。

    四、其他常见锁概念(附加分类)

    1. 可重入锁

    同一个线程,可以重复获取同一把锁,不会自己卡死自己。 synchronized、ReentrantLock都是可重入锁。

    代码示例:

    ReentrantLock lock = new ReentrantLock();
    public void m1(){
    lock.lock();
    try{
    m2(); //同一个线程再次获取同一把锁,重入
    }finally { lock.unlock(); }
    }
    public void m2(){
    lock.lock();
    try{
    System.out.println("重入成功");
    }finally { lock.unlock(); }
    }

    lock 几次,就要 unlock 几次。

    2. 自旋锁

    获取锁失败,不把线程挂起,循环重试抢锁,不进入阻塞。

    • 优点:避免内核态线程切换,速度快。
    • 缺点:占用 CPU,如果锁持有时间长,CPU 空转浪费。

    应用:

  • synchronized 轻量级锁底层 CAS 自旋;
  • Atomic 原子类底层 CAS 就是自旋;
  • AQS 内部也有自旋逻辑。
  • 自旋锁一般不会单独使用,作为其他锁内部实现机制。

    3. 偏向锁 / 轻量级锁 / 重量级锁

    仅属于 synchronized 锁升级,JVM 内部实现,开发者不能手动使用。

  • 偏向锁:只有一个线程使用锁,消除 CAS 操作;
  • 轻量级锁:多线程交替竞争,CAS 自旋,不阻塞;
  • 重量级锁:竞争激烈,线程阻塞,操作系统互斥锁。
  • 锁只能升级,不能降级。

    4. 分段锁(思想,不是具体锁对象)

    代表:ConcurrentHashMap JDK7。 把大容器拆分成多个小分段,每个分段一把锁,减小锁粒度,提升并发。 JDK8 之后放弃分段锁,改为 CAS+synchronized。

    各类锁归属关系总表(面试非常重要)

    锁名称悲观 / 乐观独占 / 共享公平 / 非公平可重入
    synchronized 悲观锁 独占锁 只能非公平 ✅可重入
    ReentrantLock 悲观锁 独占锁 可配置公平 / 非公平 ✅可重入
    ReentrantReadWriteLock‑写锁 悲观锁 独占锁 可配置公平 / 非公平 ✅可重入
    ReentrantReadWriteLock‑读锁 悲观锁 共享锁 可配置公平 / 非公平 ✅可重入
    AtomicInteger(CAS) 乐观锁 无锁 无 不涉及

    ⚠️:乐观锁没有 “独占 / 共享” 概念,乐观锁不加锁。

    面试高频问答

    Q1:独占锁、排他锁、互斥锁是什么关系?

    三者大部分场景是同一个东西。同一时刻只能一个线程访问资源。

    Q2:悲观锁和乐观锁区别?

    悲观锁假设冲突一定会发生,操作前先上锁;乐观锁假设冲突很少,不加锁,最后校验冲突,冲突重试。 synchronized、ReentrantLock 是悲观锁;Atomic、数据库 version 版本号是乐观锁。

    Q3:读写锁读锁是共享锁,那它属于悲观还是乐观?

    悲观锁,读锁还是会上锁,只是允许多线程同时读;写的时候会阻塞。不是乐观锁。

    Q4:公平锁非公平锁,synchronized 可以设置公平锁吗?

    不行,synchronized 只有非公平锁;只有 ReentrantLock 支持公平锁。

    Q5:乐观锁一定比悲观锁快吗?

    不是。竞争激烈的时候乐观锁大量自旋重试,CPU 飙升,性能不如悲观锁。冲突少乐观锁性能好。

    Q6:什么是写饥饿?

    读写锁大量读线程一直占有读锁,写线程永远拿不到写锁。StampedLock 乐观读可以缓解。

    容易混淆的坑点总结

  • 不要把维度搞混:悲观乐观是思想;独占共享是访问模式;公平非公平是排队策略。一个锁可以同时具备多个属性。
  • 乐观锁不等于共享锁,乐观锁根本不上锁。
  • ReentrantReadWriteLock读锁是共享锁,但依然是悲观锁。
  • synchronized 没有公平锁,不能手动设置。

  • 赞(0)
    未经允许不得转载:171主机测评 » 一篇讲透各种锁(详细,附代码)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址