《Java并发编程研读》第三章:锁机制

本期内容为自己总结归档,共分6章,本人遇到过的面试问题会重点标记。

第一章:线程基础

第二章:线程安全

第三章:锁机制

第四章:线程池与任务框架

第五章:线程协作工具和并发容器工具

第六章:底层原理

(若有任何疑问,可在评论区告诉我,看到就回复)

第三章:锁机制

3.1 锁的分类体系

3.1.1 锁的多维度分类全景

3.2 互斥锁(Mutex)的实现原理与性能分析

3.2.1 synchronized的底层实现与锁升级机制

synchronized是Java原生的隐式锁机制,基于JVM实现,通过对象头中的Mark Word和Monitor对象实现线程同步

其核心实现原理如下:

对象头结构与Monitor锁: Java对象的对象头包含Mark Word(存储对象的锁状态、哈希码、GC年龄等信息)和类元数据指针。Mark Word的结构会随着锁状态的变化而更新,具体包括:

锁状态 Mark Word结构 特点
无锁 哈希码、分代年龄等 默认状态,无同步开销
偏向锁 线程ID、偏向模式标志 单线程访问时无需竞争,通过CAS直接记录线程ID
轻量级锁 指向栈帧中Monitor Record的指针 多线程交替访问时,通过自旋尝试获取锁,避免OS调度
重量级锁 指向操作系统Monitor对象的指针 多线程激烈竞争时,通过OS互斥量实现线程阻塞与唤醒

锁升级流程: synchronized的锁升级是按需逐步升级的机制,目的是"按需升级锁粒度,减少锁竞争开销" 。具体流程如下:

锁升级的源码实现: 通过JDK8的源码片段,我们可以看到偏向锁和轻量级锁的实现逻辑:

java 复制代码
// 偏向锁获取
if (ThreadLocalRandom.current().nextInt(4) != 0) {
    // 通过CAS尝试将Mark Word中的线程ID设置为当前线程
    if (CAS Mark Word) {
        // 成功,进入偏向锁状态
    } else {
        // 失败,升级为轻量级锁
    }
}

// 轻量级锁获取
// 将栈帧中的Monitor Record指针写入对象头的Mark Word
// 通过循环CAS尝试获取锁
while (!CAS Mark Word) {
    // 自旋等待
}

3.2.2 ReentrantLock的AQS实现原理

ReentrantLock是Java显式锁的典型实现,基于**AQS(AbstractQueuedSynchronizer)**框架实现

java 复制代码
// ReentrantLock的AQS实现
public class ReentrantLock implements Lock, java.io.Serializable {
    private final Sync sync;

    static abstract class Sync extends AbstractQueuedSynchronizer {
        final void lock() {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
            } else {
                acquire(1);
            }
        }

        protected final boolean tryAcquire(int acquires) {
            // 非公平锁实现
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (current != getExclusiveOwnerThread()) {
                    if (compareAndSetState(0, acquires)) {
                        setExclusiveOwnerThread(current);
                        return true;
                    }
                }
            } else if (current == getExclusiveOwnerThread()) {
                // 可重入逻辑
                int nextc = c + acquires;
                if (nextc < 0) {
                    throw new Error("Maximum lock count exceeded");
                }
                setState(nextc);
                return true;
            }
            return false;
        }

        void unlock() {
            release(1);
        }
    }

    public ReentrantLock() {
        sync = new NonfairSync();
    }

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
}

state变量与可重入性: 在ReentrantLock中,state变量是一个32位的int类型,其中:

  • 低16位表示锁的持有次数
  • 高16位保留给其他用途

当线程第一次获取锁时,state被设置为1;当同一线程再次获取锁时,state递增;当释放锁时,state递减,直到为0时锁被完全释放。这种设计使得ReentrantLock支持可重入性,即同一线程可以多次获取同一把锁而不导致死锁。

公平锁与非公平锁的实现差异: ReentrantLock提供了公平和非公平两种锁策略,通过构造函数参数控制

java 复制代码
// 非公平锁实现
static final class NonfairSync extends Sync {
    final void lock() {
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(Thread.currentThread());
        } else {
            acquire(1);
        }
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

// 公平锁实现
static final class FairSync extends Sync {
    final void lock() {
        acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            // 公平锁会检查队列中是否有更早等待的线程
            if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        } else if (current == getExclusiveOwnerThread()) {
            // 可重入逻辑
            int nextc = c + acquires;
            if (nextc < 0) {
                throw new Error("Maximum lock count exceeded");
            }
            setState(nextc);
            return true;
        }
        return false;
    }
}

hasQueuedPredecessors()方法: 这是公平锁的关键方法,用于检查当前线程是否在等待队列的队首:

java 复制代码
// AQS中的hasQueuedPredecessors()方法
public final boolean hasQueuedPredecessors() {
    // 获取等待队列中的第一个节点
    Node first = getFirstWaiter();
    // 如果队列为空,或者当前线程是队列的第一个等待者,则没有前驱节点
    return (first != null && first.nextWaiter != null) || isHeldExclusively();
}

这一方法确保了公平锁的线程获取顺序遵循FIFO原则,但也会增加一定的性能开销 。这可以防止线程"饥饿"(即某个线程长期得不到锁),但会牺牲一定的吞吐量。

3.3 读写锁(ReadWriteLock)的工作原理与适用场景

3.3.1 场景

读写锁 最适合读多写少的场景,如配置中心、缓存系统、日志记录等。例如,在一个缓存系统中,大部分时间是业务线程在读取缓存数据,只有少数时间是管理员修改配置,这时使用读写锁可以显著提升并发性能。

不适用场景 : 读写锁不适合写多读少读写均衡 的场景,因为此时读写锁的状态管理开销会超过其带来的性能收益。此外,读写锁也不适合强一致性要求的场景,因为并发读可能导致数据状态不一致。

3.2.2 ReentrantReadWriteLock的AQS实现

ReentrantReadWriteLock是Java中读写锁的典型实现,基于AQS框架实现,但与ReentrantLock不同的是,它将state变量分为两部分:高16位表示写锁状态,低16位表示读锁状态

读锁与写锁的获取机制: 读锁和写锁的获取通过不同的AQS子类实现:

java 复制代码
// ReentrantReadWriteLock的读锁实现
public class ReadLock implements Lock {
    private final Sync sync;

    public void lock() {
        sync.acquireShared(1);
    }

    public void unlock() {
        sync.releaseShared(1);
    }
}

// ReentrantReadWriteLock的写锁实现
public class WriteLock implements Lock {
    private final Sync sync;

    public void lock() {
        sync.acquire(1);
    }

    public void unlock() {
        sync.release(1);
    }
}

state变量的位操作: 读写锁通过位操作管理读锁和写锁的状态:

java 复制代码
// 读锁获取
int c = getState();
if (c != 0) {
    // 检查是否有写锁被持有(高16位不为0)
    if (exclusiveCount(c) != 0) {
        // 如果有写锁被持有,则无法获取读锁
        return false;
    }
}

// 增加读锁计数(低16位)
int nextc = c + SHARED;
setState(nextc);
return true;

写锁获取: 写锁获取更为严格,需要确保state为0(即没有读锁或写锁被持有):

java 复制代码
int c = 0;
// 检查是否有读锁或写锁被持有
if (c == 0) {
    // 公平锁需要检查队列中的等待线程
    if (!fair || !hasQueuedPredecessors()) {
        // 通过CAS尝试获取写锁
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
    }
}

// 如果无法直接获取,进入等待队列
return false;

3.4 公平锁与非公平锁

公平锁和非公平锁的核心区别在于线程获取锁的顺序策略

非公平锁:新线程尝试获取锁时,不管等待队列中是否有其他线程在排队,它都会先尝试直接获取。如果此时锁恰好可用,它就能立即获取到,而不用排队。这在大多数情况下可以提高吞吐量,减少线程切换的开销。

公平锁:内部维护了一个等待队列。当锁被释放时,锁会优先分配给在队列中等待时间最长的线程(即队首的线程),严格按照FIFO(先进先出)顺序进行。这可以防止线程"饥饿"(即某个线程长期得不到锁),但会牺牲一定的吞吐量。

3.5 乐观锁与悲观锁的实现原理与适用场景

3.5.1 乐观锁

乐观锁是一种"先执行,后检查"的并发控制策略,它假设在大多数情况下线程不会发生冲突。乐观锁的典型实现基于CAS(Compare-And-Swap)操作,如Java中的Atomic类。

CAS操作原理: CAS是一种无锁算法,它并不会为对象加锁,而是在执行的时候,看看当前数据的值是不是我们预期的那样,如果是,那就正常进行替换,如果不是,那么就替换失败 。CAS操作接收三个输入参数:一个内存地址、期望值和新值。它然后原子地比较内存地址内容与期望值,如果相等,则替换为新值;否则,不做任何修改。

AtomicInteger的CAS实现

java 复制代码
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

乐观锁的局限性

  1. ABA问题:线程A读取变量值为A,线程B将其修改为B再改回A,线程A的CAS操作可能成功,但实际上变量状态已经变化 。
  2. 性能问题:在高竞争场景下,CAS可能导致大量自旋,降低系统性能 。

3.5.2 悲观锁

悲观锁是一种"先检查,后执行"的并发控制策略,它假设在大多数情况下线程会发生冲突。悲观锁的典型实现是synchronized和ReentrantLock 。

悲观锁的性能开销: 悲观锁的加锁、释放锁以及线程的挂起、唤醒会带来显著的性能开销 。但在高冲突场景下,悲观锁的性能反而优于乐观锁,因为它避免了频繁的更新失败和重试。

适用场景

  1. 乐观锁适用场景:读多写少的场景,如多数互联网应用的信息缓存更新、商品详情查询 。
  2. 悲观锁适用场景:数据一致性要求极高、写操作非常频繁且冲突概率大的场景,如银行核心系统的账户余额扣减、库存管理中的出库入库操作

3.6 面试高频考点

1. synchronized和ReentrantLock的区别与选择

核心区别

bash 复制代码
// 主要差异对比表
/*
| 特性 | synchronized | ReentrantLock |
|------|-------------|---------------|
| 实现层面 | JVM内置 | JDK实现 |
| 锁释放 | 自动释放 | 必须手动unlock() |
| 可中断 | 不支持 | lockInterruptibly()支持 |
| 公平性 | 非公平 | 可选公平/非公平 |
| 条件变量 | wait/notify | 支持多个Condition |
| 性能 | 优化后接近 | 高竞争时更好 |
| 锁绑定 | 与对象绑定 | 灵活绑定 |
| 锁信息 | 有限信息 | 丰富的监控信息 |
*/

选择策略

  • 优先synchronized:简单同步场景,JVM会优化

  • 选择ReentrantLock:需要可中断、超时、公平锁、多个条件变量

  • 性能考虑:低竞争选synchronized,高竞争测试两者性能

2. 解释锁升级过程(偏向锁→轻量级锁→重量级锁)

锁升级流程: 同 3.2.1

每个阶段的原理

  1. 偏向锁:记录线程ID,同一个线程重入无需CAS

  2. 轻量级锁:栈帧中创建Lock Record,CAS更新Mark Word

  3. 重量级锁:创建Monitor对象,线程进入阻塞队列

升级触发条件

  • 偏向锁撤销:其他线程尝试获取锁

  • 升级为重量级锁:自旋超过阈值(-XX:PreBlockSpin)或第三个线程竞争

3. 谈谈你对AQS的理解

这块单独特别篇

《Java并发编程研读》特别篇:AQS

相关推荐
一 乐2 小时前
健康管理|基于springboot + vue健康管理系统(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·后端·学习
是三好2 小时前
分布式事务seata
java·分布式·seata
为什么要做囚徒3 小时前
多线程基础系列-线程死锁
java·多线程
bluetata3 小时前
在 Spring Boot 中使用 Amazon Textract 从图像中提取文本
java·spring boot·后端
黎雁·泠崖3 小时前
Java底层探秘入门:从源码到字节码!方法调用的中间形态全解析
java·开发语言
we1less3 小时前
[audio] AudioTrack (六) 共享内存使用分析
java·开发语言
CYTElena3 小时前
关于JAVA异常的笔记
java·开发语言·笔记·语言基础
YIN_尹3 小时前
【C++11】lambda表达式(匿名函数)
java·c++·windows
猴子年华、3 小时前
【每日一技】:SQL 常用函数实战速查表(函数 + 场景版)
java·数据库·sql·mysql