java锁:从 Mark Word 锁升级到 AQS

在并发编程中,"锁"几乎是绕不开的话题。Java 之所以能在多线程环境下保持数据一致性,很大程度上依赖了其丰富而高效的锁机制。

一、 为什么我们需要"锁升级"?(Synchronized 的进化)

1. Java 对象头(Mark Word)与锁状态

Java 锁的状态其实是记录在对象头中的。这是一个动态变化的 64 位数据结构。

2. 锁升级状态流转

在 JDK 1.5 之前,synchronized 是一个重量级锁。无论竞争是否激烈,只要线程进入同步块,JVM 就会向操作系统申请互斥量(Mutex)。

为什么这很慢?

因为这涉及**用户态(User Mode)内核态(Kernel Mode)**的切换。这种切换需要保存线程上下文、更新寄存器等,开销极大。有时候,线程执行代码的时间只有几纳秒,但挂起和唤醒线程却要几毫秒,"排队的时间比吃饭的时间还长"。

为了解决这个问题,JDK 1.6 引入了锁升级机制:JVM 不再一上来就动用操作系统的重武器,而是先尝试用"轻武器"解决。

1. 锁的四种状态详解

  1. 无锁(No Lock)

    • 描述:没有任何线程持有对象锁。

    • 场景:刚创建的对象。

  2. 偏向锁(Biased Lock)------"熟人免检"

    • 原理 :JVM 认为,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得

    • 机制 :当线程 A 第一次获取锁时,JVM 把线程 A 的 ID 写到对象头的 Mark Word 中。以后线程 A 再来,只要检查 Mark Word 里是不是自己的 ID,如果是,直接通行,连 CAS 操作都不用做

    • 注:JDK 15+ 默认废弃了偏向锁,因为现代应用线程竞争普遍较多,撤销偏向锁反而消耗性能。

  3. 轻量级锁(Lightweight Lock)------"占座贴条"

    • 触发:当有第二个线程 B 尝试获取锁时,偏向锁失效,升级为轻量级锁。

    • 原理 :线程 B 不会立即阻塞。它会在自己的栈帧中创建一个叫 Lock Record 的空间,然后尝试用 CAS (Compare And Swap) 指令,把对象头的 Mark Word 替换为指向自己 Lock Record 的指针。

    • 比喻:就像去图书馆,虽然座位被占了,但我贴张条子说"我在排队",然后我不走,在旁边等着(自旋)。

  4. 重量级锁(Heavyweight Lock)------"找管理员"

    • 触发:如果线程 B 自旋了很多次(JDK 自适应自旋)还没拿到锁,或者竞争线程过多。

    • 原理 :CAS 失败,锁膨胀。对象头指向操作系统底层的 Monitor(监视器) 对象。

    • 后果:未抢到锁的线程被挂起,进入阻塞状态,等待操作系统唤醒。

3. AQS (AbstractQueuedSynchronizer) 等待队列

ReentrantLock 的核心。

synchronized 是 JVM 也就是 C++ 层面实现的,而 java.util.concurrent 包下的锁是 JDK 层面用 Java 代码实现的。它们的基石是 AQS (AbstractQueuedSynchronizer)

AQS 到底是什么?

你可以把 AQS 想象成一个银行的叫号大厅

  1. State(资源状态)

    • 大厅里的柜台窗口。state = 0 表示窗口空闲;state = 1 表示有人在办理;state > 1 表示同一个人在办多项业务(可重入)。

    • 所有线程都通过 CAS 操作去争抢修改这个 state。

  2. CLH 队列(等待区)

    • 抢不到窗口的人,不能挤在窗口前(浪费 CPU 自旋),必须去休息区排队。

    • 这是一个双向链表。新来的线程被包装成 Node 节点,挂到队尾。

    • 前一个节点释放锁后,会唤醒它的后继节点:"哥们,到你了"。

二、 各种锁的场景分析

1. Synchronized

  • 特点:语法最简单,隐式加锁释放,异常时自动释放,不易死锁。

  • 适用场景

    • 代码块短小:业务逻辑很简单,比如只是对一个 count++ 操作或者简单的赋值。

    • 绝大多数常规业务:在没有极致性能要求,且不存在复杂的交互(如尝试拿锁、超时打断)时,它是首选。

    • 单例模式:DCL(双重检查锁)单例。

2. ReentrantLock (可重入锁)

  • 特点:功能丰富。支持 tryLock()(尝试获取)、lockInterruptibly()(可被中断)、new ReentrantLock(true)(公平锁)。

  • 适用场景

    • 场景一:防止死锁(使用 tryLock)

      • 描述:比如转账业务,A 转给 B,B 转给 A,如果不小心可能死锁。使用 tryLock(),如果拿不到锁,等待 2 秒就放弃并回滚,而不是一直死等。
    • 场景二:需要中断等待

      • 描述:用户取消了操作,后台线程正在等待锁,使用 lockInterruptibly() 可以让线程响应中断信号立刻停止,而不是傻等。
    • 场景三:严格排队(公平锁)

      • 描述:比如抢票系统或打印队列,必须先来后到,不能让新来的插队,此时开启公平锁模式(虽然性能会下降)。

3. ReentrantReadWriteLock (读写锁)

  • 特点:一把锁分为"读锁"和"写锁"。读读不互斥,读写互斥,写写互斥。

  • 适用场景

    • 高频读取、低频修改的数据

    • 案例:本地缓存系统

      • 假设你维护一个庞大的配置中心缓存。99% 的时间是业务线程在读取配置,只有 1% 的时间是管理员修改配置。

      • 如果用 synchronized,所有读取线程都要排队,并发度极低。

      • 使用读写锁,1000 个线程可以同时持有读锁读取数据,只有当写线程来临时,才阻塞读线程。

4. StampedLock (印章锁 - JDK 8)

  • 特点 :读写锁的性能加强版。引入了乐观读

  • 注意不可重入,使用不当容易死锁;编码复杂度高。

  • 适用场景

    • 极致性能要求的读多写少场景

    • 案例:金融系统的实时行情处理

      • 行情数据变化很快,读取请求量巨大。

      • StampedLock 的乐观读允许线程在不加任何物理锁的情况下读取数据,最后校验一下"版本号"有没有变。如果没变,这次读取就相当于无锁,性能极高。

三:JDK 层面的锁(JUC)

当需要更高级的功能(如超时、公平性、读写分离)时,我们需要 java.util.concurrent.locks。

1. ReentrantLock 与 AQS

ReentrantLock 基于 AQS 实现。

  • State 变量:volatile int state。0 表示无锁,1 表示有锁,>1 表示重入次数。

  • CLH 队列:如上图所示,抢不到锁的线程会被封装成 Node 节点,加入双向链表排队。

java 复制代码
import java.util.concurrent.locks.ReentrantLock;

public class FairLockDemo {
    // 参数 true 表示公平锁:严格按照排队顺序获取锁
    private final ReentrantLock lock = new ReentrantLock(true);

    public void accessResource() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 正在运行");
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); // 极其重要:务必在 finally 释放
        }
    }
}

2. 读写锁 ReentrantReadWriteLock

适用场景:读多写少(如缓存)。

  • 读锁:共享锁,大家都能读。

  • 写锁:独占锁,我写的时候,你们不能读也不能写。

  • 缺陷:如果一直在读,写线程可能"饿死"(一直拿不到锁)。

3. 性能怪兽:StampedLock (JDK 8+)

这是 ReadWriteLock 的升级版,它引入了乐观读策略,性能极高。

  • 乐观读:我不加锁,我先读,读完看看版本号(Stamp)有没有变。如果变了(说明中间有人写过),我再升级成悲观读锁重读一遍。

代码:StampedLock 乐观读

java 复制代码
import java.util.concurrent.locks.StampedLock;

public class Point {
    private double x, y;
    private final StampedLock sl = new StampedLock();

    // 写方法:独占
    public void move(double deltaX, double deltaY) {
        long stamp = sl.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            sl.unlockWrite(stamp);
        }
    }

    // 读方法:乐观读(高性能关键)
    public double distanceFromOrigin() {
        // 1. 尝试乐观读,返回一个版本号戳
        long stamp = sl.tryOptimisticRead();
        double currentX = x, currentY = y;
        
        // 2. 检查在读的过程中,有没有被写过
        if (!sl.validate(stamp)) {
            // 3. 如果验证失败(说明数据不一致),升级为悲观读锁
            stamp = sl.readLock();
            try {
                currentX = x;
                currentY = y;
            } finally {
                sl.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
}

场景:高性能阻塞队列(生产者-消费者)

我们需要两个等待队列:一个叫"不再满"(notFull),一个叫"不再空"(notEmpty)。

java 复制代码
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class BoundedQueue<T> {
    private Object[] items;
    private int count, putIndex, takeIndex;
    private final ReentrantLock lock = new ReentrantLock();
    
    // 两个条件队列:精准控制
    private final Condition notFull = lock.newCondition();  // 队列不满,才能写
    private final Condition notEmpty = lock.newCondition(); // 队列不空,才能读

    public BoundedQueue(int size) {
        items = new Object[size];
    }

    // 生产者方法
    public void put(T t) throws InterruptedException {
        lock.lock();
        try {
            // 如果满了,就在 notFull 这个房间里等
            while (count == items.length)
                notFull.await();
            
            items[putIndex] = t;
            if (++putIndex == items.length) putIndex = 0;
            count++;
            
            // 生产完了,去敲 notEmpty 房间的门,唤醒消费者
            notEmpty.signal(); 
        } finally {
            lock.unlock();
        }
    }

    // 消费者方法
    public T take() throws InterruptedException {
        lock.lock();
        try {
            // 如果空了,就在 notEmpty 这个房间里等
            while (count == 0)
                notEmpty.await();
            
            T x = (T) items[takeIndex];
            if (++takeIndex == items.length) takeIndex = 0;
            count--;
            
            // 消费完了,去敲 notFull 房间的门,唤醒生产者
            notFull.signal();
            return x;
        } finally {
            lock.unlock();
        }
    }
}
相关推荐
扶尔魔ocy2 小时前
【QT opencv】手动去噪--网格化获取区域坐标
开发语言·qt·opencv
程序员与背包客_CoderZ2 小时前
C/C++版LLM推理框架Llama.cpp——入门与编码实战
c语言·开发语言·网络·c++·人工智能·语言模型·llama
chxii2 小时前
mybatis-spring 浅析
java·spring·mybatis
喵了几个咪2 小时前
C++ IDE:最适合 C++ 初学者的 IDE 是什么?
开发语言·c++·ide
梅梅绵绵冰2 小时前
springmvc文件上传
java·开发语言
龙华2 小时前
Maven多仓库/依赖配置
java·maven
天道佩恩2 小时前
MapStruct转换实体
java·后端
Hat_man_3 小时前
虚拟机Ubuntu22.04交叉编译Qt5.15.2(ARM64)
开发语言·qt
Boop_wu3 小时前
[Java 面试] 多线程1
java·开发语言