一,并发锁
1、锁是什么?
锁是一种同步机制,用于在多线程(或多进程)环境中,控制对共享资源的访问。
⭕共享资源:好比一个单人使用的公共卫生间
⭕锁:相当于卫生间的门锁机制
⭕线程:就像排队等待使用卫生间的人
2、为什么需要锁?
核心原因是:为了防止并发访问共享资源时出现的数据不一致、逻辑错误或系统崩溃。 如果没有锁,多个线程同时读写同一份数据,就会引发一系列严重问题。最主要的问题是 “竞态条件”。
✔️比如:A在使用公共卫生间(单人间)时,一旦从内部锁上门,就表示该空间已被占用,此时B不得强行进入。
3、举个栗子🌰🌰:
@Slf4j public class SyncThreadDemo { private static int count = 0; public static void add() { count++; } public static void reduce() { count--; } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 100; i++) { add(); } }, "t1"); Thread t2 = new Thread(() -> { for (int i = 0; i < 100; i++) { reduce(); } }, "t2"); t1.start(); t2.start(); t1.join(); t2.join(); log.info("计算后结果:{}", count); } }输出结果:
[main] INFO com.nl.threadway.SyncThreadDemo - 计算后结果:-44✏️ 两个线程同时对同一变量进行100次增减操作时,若不加锁控制,最终结果可能是正数、负数、零
二、synchronized的使用
1、举个栗子🌰🌰:
private static Object lock = new Object(); private static int count = 0; public static void add() { synchronized (lock) { count++; } } public static void reduce() { synchronized (lock) { count--; } }✔️只需为这两个方法的代码块添加 synchronized 关键字,即可确保输出结果始终为 0
三、synchronized原理分析
1、实现原理
- 在 Java 中,每个对象都有一个内置的Monitor监视器锁(也称为内部锁或互斥锁)
- 当线程进入 synchronized 修饰的方法或代码块时,它会自动获取这个锁。
- 退出时(正常退出或抛出异常),会自动释放锁。 synchronized是JVM内置锁,基于Monitor机制实现,依赖底层操作系统的互斥原语Mutex(互斥量),它是一个重量级锁,性能较低
1.1、synchronized的JVM 字节码指令分析
在 IntelliJ IDEA 中安装jclasslib插件后,可以查看 add 方法对应的 JVM 字节码指令,其中出现的 monitorenter 和 monitorexit 指令清晰地表明该方法基于 Monitor 机制实现。
解析:
⭕monitorenter:进入obj的监视器
⭕monitorexit :退出obj的监视器
1.2、当synchronized代码块执行过程中发生异常时,系统会如何处理?
细心的读者可能会注意到字节码中出现了两个monitorexit指令。这是编译器自动生成的异常处理机制,确保即使在异常情况下锁也能被正确释放,具体逻辑如下:
try { monitorenter // 临界区代码 } finally { monitorexit // 保证一定会执行 }2、Monitor(管程/监视器)机制详解
- Monitor是管程,也可以称为监视器,是一种用于多线程同步的机制,它确保在同一时刻只有一个线程可以执行管程中的某个子程序(或代码块)
- 管程是基于MESA模型实现的,管程中引入了条件变量的概念,而且每个条件变量都对应有一个等待队列
- MESA 模型中,条件变量可以有多个,Java 语言内置的管程里只有一个条件变量。模型如下图所示:
✔️入口等待队列:多线程进入的时排队,只允许一个线程进入管程内部,其他线程等待
✔️条件变量和等待队列:解决线程同步的问题
3、synchronized的锁Object对象
在Java中,每个对象都可以作为监视器锁(Monitor Lock),但这不是说对象本身就是锁,而是说每个Java对象内部都有一个与锁相关的ObjectMonitor数据结构 Object的方法wait、notify、notifyAll都是调用native 的方法实现
public final native void wait(long timeout) throws InterruptedException; public final native void notify(); public final native void notifyAll()⭕方法定义在 Object 类中,是所有 Java 对象都拥有的方法
⭕方法被标记为 native,意味着其实现是在 JVM 的本地代码中(通常是 C/C++ 实现)
⭕具体的实现依赖于底层操作系统和 JVM 实现
4、ObjectMonitor结构
ObjectMonitor() { // 用来保存锁对象的mark word的值。因为object里面已经不保存mark word的原来的值了,保存的是ObjectMonitor对象的地址信息。当所有线程都完成了之后,需要销毁掉ObjectMonitor的时候需要将原有的header里面的值重新复制到mark word中来。 _header = NULL; _count = 0; _waiters = 0, // 锁的重入次数 _recursions = 0; // 指向的是对象的地址信息,方便通过ObjectMonitor来访问对应的锁对象。 _object = NULL; // 标识拥有该monitor的线程(当前获取锁的线程) _owner = NULL; // 等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点 _WaitSet = NULL; _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构) _cxq = NULL ; FreeNext = NULL ; //队列用来获取锁的缓冲区,存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失败的线程) //将cxq和waitSet中的数据 移动到entryList进行排队。这个统一获取锁的入口。一般是cxq 或者waitSet数据复制过来进行统一排队。 _EntryList = NULL ; _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; _previous_owner_tid = 0; }⚠️需要重要关注的属性
⭕_recursions和_owner :线程 可重入的实现
⭕_EntryList:入口等待队列,将符合条件的cxq和waitSet中的数据 移动到entryList进行排队
⭕_WaitSet :等待队列
⭕_cxq: 多线程竞争时,会先进入这个队列
5、重量级锁实现原理
synchronized 的底层实现基于 monitor 对象、CAS 操作和 mutex 互斥锁机制。其内部维护两种队列:
- 等待队列(cxq 和 EntryList):存储未获取锁的线程
- 条件等待队列(waitSet):存放调用 wait() 方法的线程
5.1、_WaitSet等待队列调用 notifyAll()
当线程释放锁或调用 notify 时,会唤醒对应队列中的线程重新竞争锁。由于线程的阻塞和唤醒需要操作系统介入,涉及用户态与内核态的切换,这种系统调用的高开销使得 synchronized 被称为重量级锁。
🩸提醒:
⭕为了避免多个线程同时竞争锁带来的性能问题,系统采用了两个链表结构:_cxq 是一个基于 CAS 操作的单向链表,用于临时存储并发竞争的线程;而 _EntryList 是一个 双向链表,在每次唤醒操作时会将部分线程节点从 _cxq 迁移过来,从而减轻 _cxq 链表的尾部竞争压力
使用notifyAll()而非notify()的主要考虑是避免线程饥饿问题。虽然一次唤醒一个线程确实能减少上下文切换次数,但notify()的随机唤醒机制可能导致某些线程长时间得不到执行
package com.nl; import lombok.extern.slf4j.Slf4j; @Slf4j public class SimpleNotifyLost { final static Object lock = new Object(); static volatile boolean condition = false; public static void main(String[] args) throws InterruptedException { new Thread(() -> { log.debug("A开始执行"); synchronized (lock) { log.debug("A获取锁"); try { // 让线程在lock上一直等待下去 lock.wait(); log.debug("A被唤醒"); if (condition) { lock.wait(); } condition = true; log.debug("A执行结束"); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("A执行完成"); } }, "A").start(); new Thread(() -> { log.debug("B开始执行"); synchronized (lock) { log.debug("B获取锁"); try { // 让线程在lock上一直等待下去 lock.wait(); log.debug("B被唤醒"); if (!condition) { lock.wait(); } log.debug("B执行结束"); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("B执行完成"); } }, "B").start(); // 主线程两秒后执行 Thread.sleep(2000); synchronized (lock) { // 随机唤醒一个线程 lock.notify(); // 唤醒lock所有等待线程 // lock.notifyAll(); } } }执行结果如下: A可以执行完成
23:08:34.091 [A] DEBUG com.nl.SimpleNotifyLost - A开始执行 23:08:34.091 [B] DEBUG com.nl.SimpleNotifyLost - B开始执行 23:08:34.093 [A] DEBUG com.nl.SimpleNotifyLost - A获取锁 23:08:34.093 [B] DEBUG com.nl.SimpleNotifyLost - B获取锁 23:08:36.104 [A] DEBUG com.nl.SimpleNotifyLost - A被唤醒 23:08:36.104 [A] DEBUG com.nl.SimpleNotifyLost - A执行结束 23:08:36.104 [A] DEBUG com.nl.SimpleNotifyLost - A执行完成B被随机唤醒,获取到锁,但是不满足条件,就一直阻塞了,然后一直持有锁,也不释放
23:08:56.708 [B] DEBUG com.nl.SimpleNotifyLost - B开始执行 23:08:56.708 [A] DEBUG com.nl.SimpleNotifyLost - A开始执行 23:08:56.710 [B] DEBUG com.nl.SimpleNotifyLost - B获取锁 23:08:56.710 [A] DEBUG com.nl.SimpleNotifyLost - A获取锁 23:08:58.709 [B] DEBUG com.nl.SimpleNotifyLost - B被唤醒如果使用的是notifyAll
23:12:52.114 [B] DEBUG com.nl.SimpleNotifyLost - B开始执行 23:12:52.114 [A] DEBUG com.nl.SimpleNotifyLost - A开始执行 23:12:52.115 [B] DEBUG com.nl.SimpleNotifyLost - B获取锁 23:12:52.115 [A] DEBUG com.nl.SimpleNotifyLost - A获取锁 23:12:54.125 [A] DEBUG com.nl.SimpleNotifyLost - A被唤醒 23:12:54.126 [A] DEBUG com.nl.SimpleNotifyLost - A执行结束 23:12:54.126 [A] DEBUG com.nl.SimpleNotifyLost - A执行完成 23:12:54.126 [B] DEBUG com.nl.SimpleNotifyLost - B被唤醒 23:12:54.127 [B] DEBUG com.nl.SimpleNotifyLost - B执行结束 23:12:54.127 [B] DEBUG com.nl.SimpleNotifyLost - B执行完成⚠️⚠️提醒:
⭕notify():唤醒一个处于等待状态的线程。若该线程发现条件未满足(如所需资源已被其他线程占用),它可能再次进入等待状态(即重新调用 wait())。此时若无其他线程调用 notify(),可能导致所有等待线程陷入无限等待。
⭕notifyAll():唤醒所有等待线程,使每个线程都能检查当前条件。满足条件的线程继续执行,不满足条件的则重新进入等待状态。这种方法有效防止了仅唤醒单个线程时可能出现的信号丢失问题(即被唤醒线程因条件不满足而无法执行)。但缺点是会导致较多的上下文切换开销。
6、_EntryList队列线程的唤醒(非公平)
synchronized的唤醒机制并不是完全由JVM控制的,而是依赖于操作系统的线程调度。具体过程如下:
- 当锁释放时,JVM会从_EntryList中选取一个线程,将其标记为候选的下一个锁持有者(这个选取过程并不保证公平,可能是通过某种策略,比如默认是非公平的,可能选择最后进入的线程,以减少上下文切换开销)
- JVM会唤醒这个被选中的线程。注意,这里的唤醒是指将线程从阻塞状态变为可运行状态,然后线程会尝试获取锁。
- 被唤醒的线程并不一定能立即获得锁,因为此时可能有一个新的线程(不在_EntryList中)正在尝试获取锁,并且可能成功获取。所以,被唤醒的线程需要重新竞争锁。
四、总结
synchronized 是 JVM 内置的基于 Monitor 实现的锁机制。在高并发场景下,由于频繁的锁竞争和上下文切换,其性能开销较大,属于重量级锁。为此,官方对 synchronized 进行了锁膨胀升级的优化过程,这部分内容我们将在后续详细讲解。
synchronized加锁的方式在并发量大时性能会受影响,而 CAS 这种无锁的操作方式能有效解决这个问题。