欢迎光临
我们一直在努力

并发编程底层锁机制

并发编程常见的锁

🧱 并发编程三要素

并发编程需要保证三个核心特性,才能避免线程安全问题:

特性 定义 核心问题
原子性 一个或多个操作要么全部执行成功,要么全部执行失败,期间不能被中断 线程切换会打断操作,导致数据不一致
有序性 程序执行顺序与代码顺序一致,避免处理器指令重排序导致逻辑混乱 指令重排可能破坏代码逻辑
可见性 一个线程对共享变量的修改,其他线程能立刻看到 多线程缓存导致修改不及时同步

🔒 Java 常见锁种类分类

一、悲观锁 vs 乐观锁

表格

类型 核心思想 实现示例 适用场景
悲观锁 认为数据一定会被其他线程修改,每次操作都先上锁,其他线程阻塞等待 synchronized、ReentrantLock 写操作多的场景
乐观锁 认为数据不会被修改,更新时才判断是否被改动,通过版本 / CAS 保证同步 CAS(如 AtomicInteger)、数据库乐观锁 读操作多的场景,吞吐量更高

二、公平锁 vs 非公平锁

表格

类型 核心思想 实现示例 特点
公平锁 线程按申请锁的顺序获取锁(FIFO),保证每个线程都能拿到锁 ReentrantLock(true) 无线程饥饿,但吞吐量略低
非公平锁 线程随机抢占锁,可能导致某些线程长期拿不到锁(饥饿) synchronized、ReentrantLock(false) 吞吐量更高,但存在饥饿风险

三、独享锁(互斥锁) vs 共享锁
类型 别名 核心特性 读写权限
独享锁 排它锁 / 写锁 一次只能被一个线程持有,其他线程加锁会阻塞 可读可写
共享锁 S 锁 / 读锁 可被多个线程持有,只允许读不允许写 只读,不可修改

四、可重入锁 vs 不可重入锁
类型 核心特性 实现示例 优势
可重入锁 外层加锁后,内层可再次获取同一锁,不会死锁 synchronized、ReentrantLock 避免嵌套调用时的死锁
不可重入锁 已持有锁的线程再次获取时会被阻塞 早期锁实现 容易产生死锁,现已较少使用

五、自旋锁 & 分段锁 & 死锁
  • 自旋锁:线程获取锁失败时,循环重试(不进入阻塞态),减少上下文切换开销,但会消耗 CPU。

  • 分段锁:将数据分段加锁,细化锁粒度,提升并发量(如 ConcurrentHashMap 底层实现)。

  • 死锁:多个线程互相等待对方释放资源,导致无限阻塞,需通过破坏死锁四大条件避免。


💡 核心总结

  • 写多场景:用悲观锁(如 synchronized)保证数据安全。

  • 读多场景:用乐观锁(如 CAS)或共享锁提升吞吐量。

  • 避免饥饿:公平锁可保证线程公平获取,但会牺牲部分性能。

  • 避免死锁:优先使用可重入锁,控制锁的获取顺序。

ReentrantLock公平锁和非公平锁设计剖析

🧵 ReentrantLock 公平锁 vs 非公平锁 面试核心知识点

一、ReentrantLock 是公平锁还是非公平锁?
  • 可配置:ReentrantLock 通过构造参数控制,既可以是公平锁,也可以是非公平锁

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

  • 底层实现:内部类 Sync 继承 AQS,分为 FairSync(公平锁)和 NonfairSync(非公平锁)两个子类

  • 默认行为:默认使用非公平锁

  • 核心区别:公平锁的 lock() 方法多了 hasQueuedPredecessors() 限制,保证先到先得


二、什么是「锁饥饿」?

当多个线程争抢锁时:

  • 部分线程一直能抢到锁

  • 另一些线程(通常优先级较低)长期得不到 CPU 调度,永远拿不到锁

  • 这种现象就是 锁饥饿,非公平锁更容易引发


三、公平锁 vs 非公平锁 优缺点对比
维度 公平锁 非公平锁
核心逻辑 线程按申请顺序(FIFO)获取锁 线程随机抢占锁
锁饥饿 无,保证每个线程都能拿到锁 可能存在,低优先级线程易饥饿
性能 较低(涉及线程休眠 / 恢复、用户态 / 内核态切换) 更高(避免线程休眠恢复,CPU 利用率更高)
适用场景 需保证线程公平获取锁的场景 追求高吞吐量、业务性能优先的场景

四、为什么 Java 锁默认用非公平锁?
  • 性能优先:非公平锁避免了线程休眠和恢复的开销,CPU 利用率更高,吞吐量更大

  • 设计哲学:Java 锁默认更看重性能,公平锁则更看重锁资源的平均分配

  • 结论:两者各有优劣,需根据业务实际场景选择:

    • 追求公平、避免饥饿 → 用公平锁

    • 追求性能、高并发 → 用非公平锁


💡 面试回答模板

面试官:ReentrantLock 是公平锁还是非公平锁?

  • ReentrantLock 可以通过构造参数控制,既支持公平锁也支持非公平锁,默认是非公平锁。

  • 它内部通过继承 AQS 的 FairSync 和 NonfairSync 实现两种模式,公平锁会用 hasQueuedPredecessors() 保证先到先得。

面试官:什么是锁饥饿?

  • 锁饥饿是指在多线程争抢锁时,部分线程长期无法获取锁,一直处于等待状态,本质是线程调度和锁抢占机制导致的资源分配不均。

面试官:为什么设计公平锁和非公平锁?

  • 公平锁保证线程按顺序获取锁,避免饥饿,但性能较低,因为涉及线程休眠和恢复;

  • 非公平锁性能更高,CPU 利用率更好,但可能产生锁饥饿;

  • Java 默认用非公平锁,是因为业务场景更看重吞吐量,公平锁则用于需要严格保证公平性的场景。

synchoronized对象锁和类锁性能差异实践

一、核心概念区分
锁类型 关键字位置 锁对象 作用范围 核心特点
对象锁(实例锁) synchronized 加在实例方法上 当前对象实例(this) 同一个对象内部的同步方法 多线程访问不同对象时互不干扰
类锁(静态锁) synchronized 加在静态方法上 或 synchronized(XXX.class) 类的 Class 对象(如 Computer.class) 该类所有实例的静态同步代码块 多线程访问该类任何对象时都需竞争同一把锁

二、典型场景对比
  • 对象锁(synchronized method)

    public synchronized void watchMovie() { // 对象锁 System.out.println("看电影"); }

  • 示例:

  • 效果:同一时刻,同一个 Computer 对象只能有一个线程执行 watchMovie;不同对象的线程可并行执行。

  • 类锁(static synchronized method)

    public static synchronized void watchXdclass() { // 类锁 System.out.println("学课程"); }

  • 示例:

  • 效果:同一时刻,无论创建了多少个 Computer 对象,只有一个线程能执行该静态方法。

  • 混合使用(类锁 + 对象锁)

    // 线程1持有类锁(3秒) new Thread(computer::watchXdclass, "冰冰").start(); // 线程2同时可执行对象锁方法(无阻塞) new Thread(computer::watchMovie, "大利").start();

  • 类锁与对象锁是两把不同的锁,可以同时持有,互不影响。

  • 示例:


  • 三、7 大案例问题结论
    问题 结论 原因
    1. 同对象的 synchronized method 串行执行 共用同一把对象锁
    2. 同对象,一个带睡眠的 synchronized method 后执行的线程需等待 同一对象锁被占用,阻塞等待
    3. 两个对象的 synchronized method 并行执行 各自拥有独立对象锁,无竞争
    4. 同类的 static synchronized method 串行执行 共用同一把类锁
    5. 同类,一个带睡眠的 static synchronized method 后执行线程需等待 类锁被占用,阻塞等待
    6. 两个类对象的 static synchronized method 串行执行 类锁属于类,所有实例共享
    7. 同类对象,混合 static 与 实例方法 可并行执行 类锁 & 对象锁 是两把独立的锁

    四、阿里巴巴《Java 开发手册》核心强制规范
  • 锁粒度最小化

  • 能用无锁数据结构,就不要用锁;

  • 能用对象锁,就不要用类锁;

  • 能锁代码块,就不要锁整个方法。

  • 原因:减少锁竞争范围,降低锁持有时间,提升高并发场景的吞吐量。

  • 锁代码块与 RPC 调用

  • 尽可能使加锁的代码块工作量最小;

  • 严禁在锁代码块中调用 RPC 方法(远程调用耗时高,会长期持有锁,导致系统阻塞)。

  • 多资源加锁顺序

  • 同时对多个资源、数据库表、对象加锁时,保持一致的加锁顺序;

  • 否则可能造成死锁。


  • 💡 面试高频回答模板

  • 问:对象锁和类锁有什么区别?

  • 对象锁针对实例方法,锁的是当前对象,不同对象之间互不影响;

  • 类锁针对静态方法,锁的是Class 对象,所有实例共享同一把锁。

  • 它们是两把独立的锁,可以同时使用,互不干扰。

  • 问:为什么高并发场景要尽量缩小锁粒度?

  • 锁粒度越大,竞争越激烈,阻塞等待的线程越多,吞吐量越低;

  • 缩小锁粒度可减少锁持有时间,提升并发性能;

  • 但需注意:不要在锁内做 RPC 等耗时操作。


  • 🎯 总结

    • 优先对象锁:适用于大多数业务场景,并发性能更好。

    • 谨慎类锁:适用于全局状态控制、静态资源管理等场景,可能引发性能瓶颈。

    • 核心原则:能锁对象就不锁类,能锁代码块就不锁方法,减少锁竞争。

    synchronized底层锁原理分析实战

    🧵 synchronized 核心原理与字节码实现

    一、synchronized 本质定位

    synchronized 是 Java 解决线程安全的核心关键字,本质是非公平、可重入的悲观锁:

    • 每个对象都有一个锁(monitor)和等待队列

    • 锁只能被一个线程持有,其他线程需阻塞等待

    • 锁释放后,从等待队列唤醒线程,唤醒顺序不确定,不保证公平性

    • 支持可重入:同一线程可多次获取同一把锁


    二、两种使用形式 & 底层实现

    synchronized 有两种写法,底层都依赖 monitor(管程) 实现同步:

    形式 字节码表现 核心逻辑
    同步方法 字节码多 ACC_SYNCHRONIZED 标志位 线程访问方法时,检查标志位 → 获取 monitor → 执行方法 → 释放 monitor(隐式同步)
    同步代码块 字节码多 monitorenter / monitorexit 指令 进入代码块执行 monitorenter(计数器 + 1),退出执行 monitorexit(计数器 – 1),计数器为 0 时释放 monitor(显式同步)
    关键细节:
    • monitor 维护一个计数器:未被持有 → 0;同一线程重入 → 计数器递增

    • 只有计数器为 0 时,monitor 才会被释放,其他线程可竞争获取

    • 两种形式本质无区别,只是方法级同步是隐式实现,无需手动写字节码指令


    三、查看字节码的方法
  • 编译生成 .class 文件:

  • javac XXX.java

  • 反编译查看详细字节码:

  • javap -v XXX.class


    四、核心总结
    • 底层依赖 monitor:无论是方法级还是代码块级,最终都是通过对象的 monitor 实现互斥访问

    • 可重入性:通过 monitor 计数器实现,同一线程多次加锁不会死锁

    • 非公平性:唤醒等待线程时随机选择,不保证先到先得

    • 悲观锁:默认认为数据会被修改,加锁后独占访问


    💡 面试高频考点

  • 问:synchronized 是公平锁吗?

  • 不是,它是非公平锁,锁释放后唤醒线程的顺序是随机的,不保证等待时间最长的线程先获取锁。

  • 问:synchronized 为什么可重入?

  • 因为 monitor 维护了一个计数器,同一线程每次获取锁时计数器 + 1,释放时 – 1,计数器为 0 才释放锁,避免了嵌套调用时的死锁。

  • 问:同步方法和同步代码块底层有区别吗?

  • 没有本质区别,都是基于 monitor 实现,只是同步方法通过 ACC_SYNCHRONIZED 标志位隐式实现,同步代码块通过 monitorenter/monitorexit 显式实现。

  • 多线程里面的死锁案例和排查工具实战

    🧨 死锁(DeadLock)核心原理、排查与解决方案

    一、什么是死锁?

    死锁是指两个或两个以上的线程在执行过程中,因竞争资源或彼此通信等待而造成的一种阻塞现象。

    • 本质:多个线程互相持有对方需要的锁,形成循环等待,若无外力干预,程序将永远阻塞无法继续执行。

    • 线上风险:死锁发生诡异,且难以通过日志排查,是高并发系统的 “隐形杀手”。


    二、死锁的 4 个必要条件(破坏其一即可避免死锁)

    只有 4 个条件同时满足,才会发生死锁。只要破坏任意一个,死锁就不会发生。

    必要条件 核心解释
    互斥条件 资源是独占的,同一时刻只能被一个线程持有(如一把锁只能被一个线程拿到)。
    请求与保持条件 线程已经持有了一部分锁,同时又请求其他线程持有的锁,且不释放已占有的锁。
    不可抢占条件 资源一旦被线程拿到,不能被系统强制回收,只能由线程自己执行完释放。
    循环等待条件 多个线程形成环形依赖:线程 1 等线程 2 的锁,线程 2 等线程 3 的锁,线程 3 又等线程 1 的锁。

    三、案例代码:经典死锁场景

    public class DeadLockDemo { // 两个共享资源(对象锁) private static Object lockA = new Object(); private static Object lockB = new Object(); public void methodA() { synchronized (lockA) { // 线程1 获取 lockA System.out.println("我是A方法中获得了锁A"); try { Thread.sleep(2000); // 模拟业务耗时,保持锁不释放 } catch (InterruptedException e) { e.printStackTrace(); } // 尝试获取 lockB,但 lockB 可能被线程2占用 synchronized (lockB) { System.out.println("我是A方法中获得了锁B"); } } } public void methodB() { synchronized (lockB) { // 线程2 获取 lockB System.out.println("我是B方法中获得了锁B"); try { Thread.sleep(2000); // 模拟业务耗时 } catch (InterruptedException e) { e.printStackTrace(); } // 尝试获取 lockA,但 lockA 可能被线程1占用 synchronized (lockA) { System.out.println("我是B方法中获得了锁A"); } } } }

    死锁原因:线程 1 持有 lockA 等待 lockB,线程 2 持有 lockB 等待 lockA,形成循环等待。


    四、死锁的常见解决办法
  • 调整申请锁的范围(缩小粒度)
    • 不要在锁代码块中调用耗时操作(如 RPC、IO),缩短锁的持有时间

    • 尽可能只对最小需要同步的代码块加锁,而不是整个方法

  • 调整申请锁的顺序(破坏循环等待)
    • 核心原则:让所有线程按照相同的顺序获取锁

    • 示例:如果线程必须同时获取锁 A 和锁 B,那么所有线程都必须先申请锁 A,再申请锁 B。

    // 正确写法:统一按顺序获取锁 synchronized (lockA) { synchronized (lockB) { // 业务逻辑 } }

  • 定时锁等待(破坏不可抢占)
    • 使用 ReentrantLock.tryLock(timeout) 替代内置锁,超时后自动释放锁或退出。

  • 做好代码 Review
    • 高并发场景下,避免嵌套同步、多资源混合加锁。


    五、死锁排查方式(面试必问)
    方式一:JPS + Jstack(命令行神器)
  • 查找进程 ID:

  • jps

    找到运行中的 Java 进程号(PID)。

  • 生成堆栈快照:

  • jstack <PID>

    输出中会看到类似以下的 死锁提示:

    Found one Java-level deadlock: ============================= "Thread-1": waiting to lock monitor 0x00000000xxxx (object 0x00000000xxxx, a java.lang.Object), which is held by "Thread-0" "Thread-0": waiting to lock monitor 0x00000000xxxx (object 0x00000000xxxx, a java.lang.Object), which is held by "Thread-1"

    方式二:JConsole(图形化工具)
    • 打开命令行输入 jconsole 连接进程。

    • 切换到 【线程】 标签页,会直接显示 【检测到死锁】 提示,并展示死锁线程详情。


    💡 核心总结

    • 死锁本质:互斥、循环等待、不可抢占、请求保持,四条件同时成立。

    • 解决核心:控制锁的获取顺序(保持一致)是最高效的手段。

    • 排查手段:生产环境优先使用 jps + jstack 或 JConsole 快速定位。

    赞(0)
    未经允许不得转载:171主机测评 » 并发编程底层锁机制
    分享到: 更多 (0)

    评论 抢沙发

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