Java的锁机制问题

锁机制

1.锁监视器

在 Java 并发编程中,锁监视器(Monitor) 是对象内部与锁关联的同步机制,用于控制多线程对共享资源的访问。以下是核心要点:


🔒 监视器的核心组成

  1. 独占区(Ownership)

    • 一次仅允许一个线程持有监视器(即获得锁)
    • 通过 synchronized 关键字实现
  2. 入口区(Entry Set)

    • 竞争锁的线程队列(未获得锁的线程在此等待)
  3. 等待区(Wait Set)

    • 调用 wait() 的线程释放锁后进入此区域
    • 需通过 notify()/notifyAll() 唤醒

⚙️ 关键操作

操作 作用 触发条件
synchronized 线程尝试获取监视器锁,成功则进入独占区,失败则阻塞在入口区 进入同步代码块/方法时
wait() 释放锁并进入等待区,线程状态变为 WAITING 必须在持有锁时调用 (synchronized 内)
notify() 随机唤醒一个等待区的线程(唤醒后需重新竞争锁) 必须在持有锁时调用
notifyAll() 唤醒所有等待区的线程 必须在持有锁时调用

🔄 工作流程示例

java 复制代码
public class MonitorDemo {
    private final Object lock = new Object(); // 锁监视器关联到此对象

    public void doWork() throws InterruptedException {
        synchronized (lock) { // 1. 线程进入独占区
            while (条件不满足) {
                lock.wait(); // 2. 释放锁并进入等待区
            }
            // 3. 执行临界区代码
            lock.notifyAll(); // 4. 唤醒其他等待线程
        }
    }
}

⚠️ 重要规则

  1. 锁绑定对象

    每个 Java 对象(包括 Class 对象)都有一个内置监视器

    java 复制代码
    synchronized(obj) { ... }  // 使用对象监视器
    synchronized(MyClass.class) { ... }  // 使用类对象的监视器
  2. wait/notify 必须持有锁

    未在 synchronized 块中调用会抛出 IllegalMonitorStateException

  3. 虚假唤醒(Spurious Wakeup)

    等待线程可能被随机唤醒(即使未调用 notify),必须用循环检查条件:

    java 复制代码
    while (!condition) { 
        obj.wait();
    }

💡 监视器 vs 显式锁(ReentrantLock)

特性 内置监视器 ReentrantLock
实现方式 JVM 内置 JDK 代码实现 (java.util.concurrent)
锁获取 隐式获取/释放 显式调用 lock()/unlock()
等待条件 单一等待队列 支持多个 Condition 队列
公平性 非公平锁 可选公平/非公平模式
中断响应 不支持 支持 lockInterruptibly()

📌 总结

  • 监视器 = 锁 + 等待队列 + 线程调度机制
  • synchronized 实现锁的互斥访问
  • wait()/notify() 实现线程间协作
  • 经典应用场景:生产者-消费者模型、线程间条件等待

⚠️ 注意:优先使用 java.util.concurrent 包中的高级工具(如 ReentrantLockCondition),它们提供更灵活的同步控制,能避免许多内置监视器的限制。

2.锁升级

在 Java 并发编程中,锁升级(Lock Escalation) 是 JVM 对 synchronized 锁的优化机制,它会根据锁竞争情况动态调整锁状态,从低开销状态逐步升级到高开销状态。这是 Java 性能优化的关键特性(自 JDK 1.6 引入)。

🔄 锁升级的四个阶段

无锁 偏向锁 轻量级锁 重量级锁

1. 无锁状态(No Lock)

  • 特征:对象刚创建时,没有任何线程访问
  • 开销:无任何锁操作成本
  • 对象头标志001

2. 偏向锁(Biased Lock)

  • 适用场景单线程重复访问同步块
  • 优化原理
    • 在对象头记录首个获得锁的线程ID
    • 同一线程后续进入同步块时无需 CAS 操作
  • 对象头标志101
  • 升级触发:当其他线程尝试获取锁时

3. 轻量级锁(Lightweight Lock)

  • 适用场景多线程交替执行(无实际竞争)
  • 实现机制
    1. 在栈帧创建锁记录(Lock Record)
    2. 通过 CAS 将对象头替换为指向锁记录的指针
    3. 成功:获得锁;失败:自旋尝试
  • 对象头标志00
  • 升级触发:自旋超过阈值(默认10次)或自旋时出现第三个线程竞争

4. 重量级锁(Heavyweight Lock)

  • 适用场景高并发竞争
  • 实现机制
    • 通过操作系统 mutex 互斥量实现
    • 未获锁线程进入阻塞队列(涉及内核态切换)
  • 对象头标志10
  • 特点:开销最大,但保证公平性

🧪 锁升级过程示例

java 复制代码
public class LockEscalationDemo {
    private static final Object lock = new Object();
    private static int counter = 0;

    public static void main(String[] args) {
        // 阶段1: 偏向锁 (单线程)
        synchronized (lock) {
            counter++;
        }

        // 阶段2: 轻量级锁 (多线程交替)
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                synchronized (lock) { counter++; }
            }
        }).start();

        // 阶段3: 重量级锁 (高并发竞争)
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                synchronized (lock) { counter++; }
            }).start();
        }
    }
}

📊 锁状态对比表

特性 偏向锁 轻量级锁 重量级锁
适用场景 单线程访问 多线程交替执行 高并发竞争
实现方式 记录线程ID CAS自旋 操作系统mutex
开销 极低 中等
竞争处理 升级为轻量级锁 自旋失败则升级 线程阻塞
对象头 存储线程ID+epoch 指向栈中锁记录指针 指向监视器对象指针
是否阻塞 自旋(非阻塞) 是(内核阻塞)
公平性 可配置

⚙️ 锁升级关键技术细节

  1. Mark Word 结构变化

    java 复制代码
    // 32位JVM对象头示例
    | 锁状态   | 25bit          | 4bit     | 1bit(偏向) | 2bit(锁标志) |
    |----------|----------------|----------|------------|--------------|
    | 无锁     | 哈希码         | 分代年龄 | 0          | 01           |
    | 偏向锁   | 线程ID+epoch   | 分代年龄 | 1          | 01           |
    | 轻量级锁 | 指向锁记录指针 |          |            | 00           |
    | 重量级锁 | 指向监视器指针 |          |            | 10           |
  2. 批量重偏向(Bulk Rebias)

    • 当一类对象的偏向锁被撤销超过阈值(默认20次),JVM 会认为该类不适合偏向锁
    • 后续该类的对象会直接进入轻量级锁状态
  3. 锁消除(Lock Elision)

    • JIT 编译器对不可能存在共享竞争的锁进行消除

      java 复制代码
      // 示例:局部StringBuffer的同步会被消除
      public String localMethod() {
        StringBuffer sb = new StringBuffer(); // 局部变量
        sb.append("Hello");
        return sb.toString();
      }

⚠️ 重要注意事项

  1. 锁降级不存在

    • 锁升级是单向过程(偏向→轻量→重量)
    • 一旦升级为重量级锁,不会降级(即使竞争消失)
  2. 偏向锁延迟启动

    • JVM 启动后前 4 秒默认禁用偏向锁(避免初始化时的无效偏向)

      bash 复制代码
      # 关闭偏向锁(JDK 15+默认)
      -XX:-UseBiasedLocking
  3. 自旋优化

    • 轻量级锁的自旋次数由 JVM 自适应调整(Adaptive Spinning)
    • 基于前一次锁获取的成功率动态变化

💡 最佳实践建议

  1. 低竞争场景

    • 保持默认设置(允许锁升级)
    • 避免不必要的同步块
  2. 高竞争场景

    • 考虑使用 ReentrantLock 替代 synchronized
    • 利用 java.util.concurrent 高级并发工具
  3. 性能调优

    bash 复制代码
    # 查看锁竞争情况
    -XX:+PrintSynchronizationStatistics
    
    # 禁用偏向锁(若确认高竞争)
    -XX:-UseBiasedLocking

锁升级的本质 :JVM 在线程安全执行效率之间寻找最佳平衡点,开发者应理解其原理但避免过度干预自动优化。

3.ABA问题

ABA 问题详解

在并发编程中,ABA 问题 是使用 CAS(Compare-And-Swap)操作时可能遇到的一种经典问题。它发生在共享变量的值经历了 A→B→A 的变化序列后,CAS 操作无法检测到中间状态变化的情况。

🔍 ABA 问题发生机制

线程1 共享变量 线程2 读取值 A 修改值 A→B 修改值 B→A 执行CAS(A→C):成功! 线程1 共享变量 线程2

问题本质

  • CAS 只检查值是否匹配,不关心值是否被修改过
  • 虽然最终值回到了 A,但中间状态变化被忽略
  • 可能导致数据一致性问题

⚠️ 经典案例:无锁栈实现中的 ABA

java 复制代码
public class Stack {
    private AtomicReference<Node> top = new AtomicReference<>();

    public void push(Node node) {
        Node oldTop;
        do {
            oldTop = top.get();
            node.next = oldTop;
        } while (!top.compareAndSet(oldTop, node));
    }

    public Node pop() {
        Node oldTop;
        Node newTop;
        do {
            oldTop = top.get();
            if (oldTop == null) return null;
            newTop = oldTop.next;
        } while (!top.compareAndSet(oldTop, newTop));
        return oldTop;
    }
}

ABA 问题发生场景:

  1. 线程1读取栈顶节点 A
  2. 线程1被挂起
  3. 线程2弹出 A,栈顶变为 B
  4. 线程2弹出 B
  5. 线程2压入 A(新节点,地址相同)
  6. 线程1恢复执行,CAS 成功将 A 替换为 C
  7. 结果:C.next 指向 B,但 B 已被弹出,造成内存错误

🛡️ ABA 问题解决方案

1. 版本号机制(推荐)

为每个状态变化添加版本号戳记:

java 复制代码
// Java 内置解决方案
AtomicStampedReference<V> // 带整数戳记的引用
AtomicMarkableReference<V> // 带布尔标记的引用

实现原理

匹配 不匹配 值 版本戳 CAS操作 同时检查值和版本号 更新值和版本号 操作失败

使用示例

java 复制代码
public class ABASolution {
    private AtomicStampedReference<Integer> value = 
        new AtomicStampedReference<>(0, 0); // 初始值=0, 版本=0

    public void update(int expectedValue, int newValue) {
        int[] stampHolder = new int[1];
        int oldStamp;
        int newStamp;

        do {
            // 读取当前值和版本
            int currentValue = value.get(stampHolder);
            oldStamp = stampHolder[0];

            // 验证值是否被修改过
            if (currentValue != expectedValue) {
                break; // 值已被其他线程修改
            }

            newStamp = oldStamp + 1; // 更新版本号
        } while (!value.compareAndSet(expectedValue, newValue, oldStamp, newStamp));
    }
}

2. 不重复使用内存地址

  • 确保被替换的对象不会被重用
  • 适用于对象池或资源管理场景
  • 实现复杂,不推荐作为通用方案

3. 延迟回收(GC 语言中)

  • 依赖垃圾回收机制防止对象复用
  • 在非 GC 环境(如 C/C++)中不可靠

📊 ABA 问题与其他并发问题对比

问题类型 发生场景 检测难度 典型解决方案
ABA 问题 CAS 操作 版本号机制
竞态条件 多线程无序访问 同步锁
死锁 多锁相互等待 锁排序、超时机制
活锁 线程持续重试失败 随机退避策略

ABA 问题本质 :CAS 操作只能检查值的相等性 ,无法检测值的历史变化。版本号机制通过添加状态元数据,将值检查扩展为状态机检查,从而解决这一问题。

相关推荐
comeilmforever13 分钟前
IDEA2025 Version Control 窗口 local changes显示
java·ide·intellij-idea
火车叨位去194914 分钟前
映射阿里云OSS(对象存储服务)
java·spring
2301_14725836920 分钟前
7月1日作业
java·前端·算法
背影疾风20 分钟前
C++之路:类基础、构造析构、拷贝构造函数
linux·开发语言·c++
Ting-yu24 分钟前
Java中Stream流的使用
java·开发语言·windows
一只猿Hou41 分钟前
java分页插件| MyBatis-Plus分页 vs PageHelper分页:全面对比与最佳实践
java·mybatis
程序员弘羽1 小时前
C++ 第四阶段 内存管理 - 第二节:避免内存泄漏的技巧
java·jvm·c++
旷世奇才李先生1 小时前
Tomcat 安装使用教程
java·tomcat
【ql君】qlexcel1 小时前
Notepad++ 复制宏、编辑宏的方法
开发语言·javascript·notepad++··宏编辑·宏复制
Zevalin爱灰灰1 小时前
MATLAB GUI界面设计 第六章——常用库中的其它组件
开发语言·ui·matlab