锁是并发编程的核心机制,其本质是通过控制共享资源的访问权限,解决多线程竞争导致的数据不一致问题。深入理解锁,需要从 “为什么需要锁”“锁的核心特性”“锁的实现原理”“Java 中的锁机制” 四个维度展开,结合底层逻辑和实际场景拆解:

一、为什么需要锁?—— 并发问题的根源与锁的本质

多线程并发访问共享资源(如全局变量、数据库连接、文件)时,会因 “指令交错执行” 导致原子性、可见性、有序性被破坏,引发数据错误(如超卖、余额异常)。

  • 原子性:一个操作或多个操作要么全执行,要么全不执行(如i++实际是 “读 - 改 - 写” 三步,多线程下可能被打断)。
  • 可见性:一个线程修改的共享变量,其他线程能立即看到(CPU 缓存会导致线程间数据不一致)。
  • 有序性:指令执行顺序与代码顺序一致(JVM 指令重排序可能打乱执行顺序)。

锁的核心作用是通过 “互斥” 保证原子性(同一时间只有一个线程执行临界区代码),并通过内存屏障保证可见性和有序性,从而解决并发问题。

二、锁的核心特性与分类:从 “行为” 定义锁的差异

锁的分类维度很多,核心是通过 “行为特性” 区分,决定了其适用场景:

分类维度 具体类型 核心特点 典型例子
是否互斥 互斥锁(Exclusive Lock) 同一时间只允许一个线程持有锁(写操作需互斥) synchronizedReentrantLock
共享锁(Shared Lock) 允许多个线程同时持有锁(读操作可共享) ReadWriteLock的读锁
是否公平 公平锁(Fair Lock) 线程获取锁的顺序与请求顺序一致(先到先得) ReentrantLock(true)
非公平锁(Unfair Lock) 线程获取锁时可 “插队”(可能导致某些线程饥饿,但性能更高) synchronizedReentrantLock(false)
是否可重入 可重入锁(Reentrant) 同一线程可多次获取同一把锁(避免自己锁死自己) synchronizedReentrantLock
非可重入锁(Non-reentrant) 同一线程不可重复获取锁(可能导致死锁) 早期Mutex
是否可中断 可中断锁 线程等待锁时可被中断(避免无限等待) ReentrantLock.lockInterruptibly()
不可中断锁 线程一旦开始等待锁,必须等到获取锁或执行完毕,无法被中断 synchronized
是否有条件 条件锁(Condition Lock) 允许线程在获取锁后,等待某个条件满足再继续执行(如 “队列不为空才消费”) ReentrantLock.newCondition()
关键特性解析:
  1. 可重入性:例:线程 T1 获取锁后,执行方法 A,方法 A 中调用了同样需要该锁的方法 B,可重入锁允许 T1 直接进入方法 B(无需重新竞争锁)。

    • 实现原理:锁内部记录 “持有线程” 和 “重入次数”,同一线程再次获取时,重入次数 + 1;释放时,重入次数 - 1,直到 0 才真正释放锁。
    • 作用:避免 “自己锁死自己”(如递归调用带锁的方法)。
  2. 公平性

    • 公平锁:通过队列记录线程请求顺序,严格按顺序分配锁(无插队),但频繁的队列操作会降低性能。
    • 非公平锁:线程请求锁时,先尝试直接获取(插队),失败再入队,减少上下文切换,性能更高,但可能导致部分线程长期得不到锁(饥饿)。
    • 现实类比:公平锁 = 排队买票,非公平锁 = 排队时有人尝试插队(成功就买,失败再排队)。

三、Java 中锁的底层实现:从synchronizedReentrantLock

Java 中的锁机制分为内置锁(synchronized) 和显式锁(JUC 中的 Lock 接口),两者底层实现不同,但核心目标一致。

1. synchronized:JVM 内置锁(“隐式锁”)

synchronized是 Java 最基础的锁,通过对象头监视器(Monitor) 实现,JDK1.6 后引入 “锁升级” 机制大幅提升性能。

(1)锁的存储位置:对象头的Mark Word

Java 对象在内存中的布局包括:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。其中,对象头的Mark Word 是synchronized实现的关键,用于存储锁状态、持有线程等信息(32 位 JVM 示例):

锁状态 Mark Word 结构(32 位) 说明
无锁状态 哈希码(25 位) + 分代年龄(4 位) + 01(无锁标志) 未被锁定,仅存储对象哈希码等信息
偏向锁 线程 ID(23 位) + Epoch(2 位) + 分代年龄(4 位) + 01(偏向标志) 长期被同一线程持有,减少竞争开销
轻量级锁 指向栈中锁记录的指针(30 位) + 00(轻量标志) 多线程交替执行,无激烈竞争
重量级锁 指向 Monitor 的指针(30 位) + 10(重量标志) 多线程激烈竞争,依赖操作系统互斥量
(2)锁升级:从 “无锁” 到 “重量级锁” 的渐进式优化

JVM 为了减少锁的性能开销,设计了 “锁升级” 机制(不能降级),根据竞争激烈程度自动切换锁状态:

  • 无锁 → 偏向锁:当一个线程第一次获取锁时,JVM 在Mark Word中记录该线程 ID(通过 CAS 操作),后续该线程再次获取锁时,只需判断Mark Word中的线程 ID 是否为自己,无需任何同步操作(几乎零开销)。

    • 适用场景:单线程重复获取锁(如单线程操作集合)。
  • 偏向锁 → 轻量级锁:当有第二个线程尝试获取锁时,偏向锁失效,升级为轻量级锁。此时,线程会在自己的栈帧中创建 “锁记录(Lock Record)”,并通过 CAS 将Mark Word指向自己的锁记录:

    • 若 CAS 成功,线程获取轻量级锁;
    • 若 CAS 失败(说明有竞争),则自旋重试(通过循环尝试获取锁,避免进入内核态)。
    • 适用场景:多线程交替执行(竞争不激烈),自旋次数少。
  • 轻量级锁 → 重量级锁:若自旋次数超过阈值(JDK1.6 后自适应,根据前一次自旋成功率调整),或有更多线程参与竞争,轻量级锁升级为重量级锁。此时,线程会进入Monitor 的等待队列,放弃 CPU 资源(进入阻塞状态),由操作系统调度(开销大)。

    • 适用场景:多线程激烈竞争(如高并发写操作)。
(3)synchronized的内存语义

synchronized除了互斥,还通过内存屏障保证可见性和有序性:

  • 进入同步块时:会清空工作内存,从主内存加载最新共享变量(保证可见性);
  • 退出同步块时:会将工作内存中的修改刷新到主内存(保证可见性);
  • 禁止指令重排序:同步块内的指令不会被重排序到块外(保证有序性)。
2. ReentrantLock:JUC 中的显式锁(基于 AQS)

ReentrantLockjava.util.concurrent.locks包下的可重入锁,功能比synchronized更灵活(支持公平 / 非公平、可中断、条件锁),底层基于AQS(AbstractQueuedSynchronizer,抽象队列同步器) 实现。

(1)AQS:锁的 “骨架”

AQS 是 JUC 中同步工具的基础(如ReentrantLockSemaphoreCountDownLatch),核心由两部分组成:

  • 状态变量(state):用volatile int存储锁的状态(0 = 未锁定,>0 = 已锁定,值 = 重入次数)。
  • 同步队列(CLH 队列):双向链表,存储等待锁的线程(Node 节点包含线程引用、等待状态等)。
(2)ReentrantLock的加锁 / 解锁流程(非公平锁为例)
  • 加锁(lock ())

    1. 尝试 CAS 将state从 0 改为 1(直接插队获取锁);
    2. 若成功,设置当前线程为持有线程;
    3. 若失败,判断当前线程是否为持有线程(可重入):是则state+1
    4. 若不是,将线程封装为 Node 加入同步队列,进入阻塞状态(通过LockSupport.park())。
  • 解锁(unlock ())

    1. 调用tryRelease()state-1
    2. state变为 0,释放持有线程,从同步队列唤醒一个线程(通过LockSupport.unpark());
    3. 被唤醒的线程尝试获取锁(重复步骤 1-3)。
(3)与synchronized的核心区别
特性 synchronized ReentrantLock
锁释放 自动释放(出同步块 / 方法时) 手动释放(必须在finally中调用unlock()
公平性 仅支持非公平锁 支持公平 / 非公平(构造器参数指定)
可中断性 不可中断 支持可中断获取(lockInterruptibly()
条件锁 不支持 支持(newCondition()
性能(JDK1.6+) ReentrantLock接近(锁升级优化后) 高并发下略优(灵活控制)

四、锁的高级形态:从 “独占” 到 “高效共享”

为了在并发场景中提升性能,Java 还提供了针对特定场景的锁:

1. 读写锁(ReadWriteLock):读共享、写互斥
  • 核心思想:区分 “读操作” 和 “写操作”—— 多个线程可同时读(共享锁),但写操作必须互斥(且读写互斥),适合 “读多写少” 场景(如缓存)。
  • 实现ReentrantReadWriteLock,包含readLock()(共享锁)和writeLock()(互斥锁)。
  • 示例

    java

    运行

    ReadWriteLock rwLock = new ReentrantReadWriteLock();
    Lock readLock = rwLock.readLock();   // 读锁(共享)
    Lock writeLock = rwLock.writeLock(); // 写锁(互斥)
    
    // 读操作:多个线程可同时执行
    readLock.lock();
    try { /* 读取共享资源 */ } finally { readLock.unlock(); }
    
    // 写操作:同一时间只有一个线程执行
    writeLock.lock();
    try { /* 修改共享资源 */ } finally { writeLock.unlock(); }
    
2. stampedLock:乐观读优化的读写锁

StampedLock是 JDK1.8 新增的锁,比ReadWriteLock更灵活,支持三种模式:

  • 写锁(writeLock ()):互斥,获取后返回一个 “戳记(stamp)”;
  • 悲观读锁(readLock ()):共享,类似ReadWriteLock的读锁;
  • 乐观读(tryOptimisticRead ()):无锁,尝试获取戳记,读取后验证戳记是否有效(期间无写操作),适合读操作极多、写操作极少的场景。
3. 无锁编程:CAS(非阻塞同步)

锁的本质是 “阻塞同步”(线程竞争时会阻塞),而 CAS(Compare And Swap,比较并交换)是 “非阻塞同步” 的核心,通过硬件指令保证原子性,无需线程阻塞。

  • 原理CAS(V, A, B)—— 若内存值V等于预期值A,则更新为B,否则不操作,返回V
  • 示例AtomicIntegerincrementAndGet()(原子自增):

    java

    运行

    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    // 底层调用CAS:循环尝试直到成功
    
  • 优点:无线程阻塞 / 唤醒的开销,性能高;
  • 缺点:可能出现 “ABA 问题”(值从 A→B→A,CAS 认为未变),可通过版本号解决(如AtomicStampedReference)。

五、锁的性能优化:从 “减少竞争” 到 “高效利用”

实际开发中,锁的性能优化核心是减少锁的竞争程度,常用策略:

  1. 减少锁持有时间:只在必要的代码段加锁(临界区最小化),避免锁范围过大。

    java

    运行

    // 优化前:锁范围大
    synchronized (this) {
        readFile(); // 耗时操作,无需加锁
        updateData(); // 仅这步需要加锁
    }
    
    // 优化后:锁范围最小化
    readFile();
    synchronized (this) {
        updateData();
    }
    
  2. 降低锁粒度:将大对象拆分为小对象,减少竞争(如ConcurrentHashMap的 “分段锁”,JDK1.7 前将数组分为 16 段,每段独立加锁)。

  3. 锁分离:根据操作类型拆分锁(如读写锁分离读和写)。

  4. 锁粗化:连续多次加锁 / 解锁合并为一次(避免频繁切换锁状态的开销)。

    java

    运行

    // 优化前:频繁加锁解锁
    for (int i = 0; i < 100; i++) {
        synchronized (this) { count++; }
    }
    
    // 优化后:一次加锁覆盖整个循环
    synchronized (this) {
        for (int i = 0; i < 100; i++) { count++; }
    }
    
  5. 使用无锁编程:如Atomic系列、LongAdder(高并发下比AtomicLong性能更高)。

六、总结:锁的本质与选择

锁的本质是 **“并发环境下共享资源的访问控制器”**,其设计围绕 “安全性” 和 “性能” 的平衡:

  • 简单场景用synchronized(无需手动释放,不易出错);
  • 复杂场景用ReentrantLock(需要公平性、可中断、条件锁时);
  • 读多写少用ReadWriteLockStampedLock
  • 高并发计数用Atomic系列或LongAdder(无锁)。

理解锁的底层原理(如synchronized的锁升级、AQS 的队列机制),能帮你在实际开发中避开 “死锁”“性能瓶颈” 等坑,写出高效且安全的并发代码。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐