Synchronized原理自编
Synchronized
-
性质?
- 线程安全: 满足可见性、有序性,原子性。
-
Java对象内存布局?
- 分别是对象头、实例数据以及填充数据。
- 对象头(object header):对象头又被分为两部分,分别为:Mark Word(标记字段)、Class Pointer(类型指针)。如果是数组,那么还会有数组长度
- 实例数据(Instance Data):主要是存放类的数据信息,父类的信息,对象字段属性信息。这部分内存按4字节对齐。
- 对齐填充(Padding):由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
-
java对象头和Synchronized联系
- Synchronized的状态主要存储在对象头的Mark Word(标记字段)中,中间是有个存储规则的,有32位规则和64位规则。
- Class Pointer(类型指针):即类型指针,是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
- Mark Word(标记字段):用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。
-
自旋锁
- 背景:由于在实际环境中,很多线程的锁定状态只会持续很短的一段时间,会很快释放锁,为了如此短暂的时间去挂起和阻塞其他所有竞争锁的线程,是非常浪费资源的。
- 获取到锁的线程在门外等待一会(自旋),但不放弃CPU的执行时间,这其实就是自旋锁。
- 所以我们需要对锁自旋的次数有所限制,如果自旋超过了限定的次数仍然没有成功获取到锁,就应该重新使用传统的方式去挂起线程了。在JDK定义中,自旋锁默认的自旋次数为10次,用户可以使用参数-XX:PreBlockSpin来更改。
-
锁粗化
- 背景:同步块的作用范围尽可能小,可以缩短阻塞时间,但是还有一种情况,有人在循环里面写上了synchronized关键字,为了降低短时间内大量的锁请求、释放带来的性能损耗,
- Java虚拟机发现了之后会适当扩大加锁的范围,以避免频繁的拿锁释放锁的过程。
-
锁消除
- 即时编译器通过对运行上下文的扫描,对不可能存在共享资源竞争的锁进行消除,从而节约大量的资源开销,提高效率
锁膨胀
锁膨胀方向: 无锁 → 偏向锁 → 轻量级锁 → 重量级锁
-
偏向锁:锁偏向于第一个获取他的线程,该锁一直没有被其他线程获取,可以通过CAS原子指令,尝试将对象头中的线程ID设置为当前线程ID。通过CAS减少无竞争场景下的同步开销。
-
轻量级锁:基于CAS(Compare and Swap)和自旋优化实现。通过栈帧中的锁记录(Lock Record)存储锁信息,通过CAS修改对象头(Mark Word)。将对象头的Mark Word替换为指向锁记录的指针,当锁被释放的时候被唤醒。
- Lock Record:每个线程进入同步块时,JVM在其栈帧中创建一个Lock Record,包含两个关键字段:Displaced Mark Word:保存锁对象当前的Mark Word副本。Owner Pointer(隐式):指向锁对象,用于后续解锁时关联对象。
-CAS(Compare-And-Swap):顾名思义比较并替换。这是一个由CPU硬件提供并实现的原子操作.可以被认为是一种乐观锁,会以一种更加乐观的态度对待事情,认为自己可以操作成功。当多个线程操作同一个共享资源时,仅能有一个线程同一时间获得锁成功,在乐观锁中,其他线程发现自己无法成功获得锁,并不会像悲观锁那样阻塞线程,而是直接返回,可以去选择再次重试获得锁,也可以直接退出。能保证原子性和可见性,有序性不能保证,必须自己实现voliate
CAS机制所保证的只是一个变量的原子性操作,无法保证整个代码块的原子性
- Lock Record:每个线程进入同步块时,JVM在其栈帧中创建一个Lock Record,包含两个关键字段:Displaced Mark Word:保存锁对象当前的Mark Word副本。Owner Pointer(隐式):指向锁对象,用于后续解锁时关联对象。
-
重量级锁
- 通过对象头的监视器(Monitor)管理锁,依赖操作系统的线程阻塞与唤醒机制。基于操作系统底层的互斥量(mutex)实现。
- 互斥量(mutex)的定义:全称为Mutual Exclusion Lock(互斥锁)。确保在任意时刻,只有一个线程能访问共享资源(如临界区代码或共享数据结构),防止并发冲突。
synchronized中的互斥量(mutex)应用
- 1.创建监视器(Monitor):每个Java对象在重量级锁状态下,其对象头的Mark Word会被替换为指向一个ObjectMonitor结构(监视器)的指针。
- ObjectMonitor内部包含:互斥量(mutex)由操作系统提供,用于控制线程进入临界区。等待队列(EntryList):存储竞争锁失败的线程。条件变量(WaitSet):存储调用wait()方法的线程。
- 2.线程竞争锁:线程尝试通过CAS操作获取锁,成功:直接进入同步代码执行。失败:调用操作系统提供的互斥量(如pthread_mutex_lock),将线程加入等待队列并阻塞。
- 3.锁释放与唤醒:持有锁的线程执行完同步代码后,调用ObjectMonitor::exit():通过互斥量解锁。唤醒等待队列中的线程(如通过pthread_cond_signal)。
-
源码解析,代码执行流程?
-【锁竞争(Enter)】【锁释放(Exit)】【等待与通知(Wait/Notify)】- 当一个线程访问方法时,会去检查是否存在ACC_SYNCHRONIZED标识符,如果存在,则先要获得对应的monitor锁,然后执行方法。当方法执行结束(不管是正常return还是抛出异常)都会释放对应的monitor锁。如果此时有其他线程也想要访问这个方法时,会因得不到monitor锁而阻塞。当同步方法中抛出异常且方法内没有捕获,则在向外抛出时会先释放已获得的monitor锁;
-
原理是什么?
-
monitor锁是什么?
- HotSpot虚拟机源码中,HotSpot虚拟机通过C++实现的ObjectMonitor类来支持Monitor机制,其源码位于objectMonitor.hpp文件中,monitor字面意思就是"监视器",也叫管程。它的出现是为了解决操作系统级别关于线程同步原语的使用复杂性,类似于语法糖,对复杂操作进行封装。
- 它的作用是:
- 【锁互斥】限制同一时刻,只有一个线程能进入monitor框定的临界区(持有权),达到线程互斥,保护临界区中临界资源的安全,实现程序线程安全的目的。
- 【wait/notify机制】它还具有管理进程,线程状态的功能,Java中Object类提供了notify和wait方法来对线程进行控制,其实它们都依赖于monitor对象,这就是wait/notify等方法只能在同步的块或者方法中才能被调用的根本原因
-
ObjectMonitor与Java对象的关系?
- 每个Java对象在内存中都与一个潜在的Monitor关联,但这种关联并非始终存在。只有当对象作为锁进入重量级锁状态时,其对象头(Mark Word)中的指针才会指向一个具体的ObjectMonitor实例
-
锁升级与ObjectMonitor的创建?
- 仅在升级为重量级锁时才会创建ObjectMonitor实例
- 偏向锁:标记线程ID于Mark Word,无竞争时直接进入同步代码。
- 轻量级锁:通过CAS操作在用户态管理锁记录(Lock Record),避免内核切换。
- 重量级锁:竞争激烈时,Mark Word指向ObjectMonitor实例,线程进入内核态阻塞。
-
synchronized 关键字与 wait()、notify() 和 notifyAll() 方法的结合?
- synchronized 关键字与 wait()、notify() 和 notifyAll() 方法的结合 是 Java 中实现经典的 等待/通知机制 (Wait/Notify Mechanism) 或 线程间通信 (Inter-Thread Communication) 的核心方式。这种机制允许线程在特定条件不满足时主动释放锁并进入等待状态,直到其他线程改变了条件并通知它。
- 核心组件的作用
-
synchronized 关键字:
- 作用: 提供互斥锁(监视器锁),确保在同一时间只有一个线程能进入临界区(被 synchronized 保护的代码块或方法)。
- 与等待/通知的关系: wait(), notify(), notifyAll() 必须在 已经获得对象监视器锁(即在 synchronized 代码块或方法内部)的情况下调用。否则会抛出 IllegalMonitorStateException。锁是这些方法工作的基础。
-
wait() 方法:
- 作用: 使当前线程主动释放它持有的那个对象的监视器锁(⚠️注意:只释放调用 wait() 的那个对象的锁,如果该线程持有多个对象的锁,其他锁不会被释放),然后该线程进入该对象的等待池 (Wait Set) 中,状态变为 WAITING 或 TIMED_WAITING(如果使用了带超时的 wait(long timeout))。
- 触发条件: 线程调用 wait() 通常是因为它发现某个条件不满足(例如,任务队列为空,缓冲区已满),无法继续执行下去。
- 后续动作: 线程会一直等待,直到发生以下情况之一:
-
另一个线程调用同一个对象的 notify() 或 notifyAll() 方法唤醒它。
-
等待时间超过了指定的超时时间(如果使用了带超时的 wait)。
-
线程被其他线程中断 (InterruptedException)。
-
-
notify() 方法:
-
作用: 从在该对象等待池中等待的线程里随机唤醒一个(具体哪个线程由 JVM 调度决定)。被唤醒的线程会尝试重新获取该对象的监视器锁。一旦成功获取锁,它就会从之前调用 wait() 的地方继续执行。
-
使用场景: 当某个条件可能只允许一个等待线程继续执行时(例如,缓冲区中只放入了一个新元素)。
-
-
notifyAll() 方法:
-
作用: 唤醒所有在该对象等待池中等待的线程。这些被唤醒的线程会一起竞争该对象的监视器锁。最终只有一个线程能成功获取锁并继续执行,其他线程虽然被唤醒但拿不到锁,会回到 BLOCKED 状态,等待锁可用。
-
使用场景: 当某个条件的改变可能允许多个等待线程继续执行时(例如,缓冲区有了足够的空闲空间,多个生产者线程可能都能继续生产),或者当不确定哪个线程应该被唤醒时(更安全但效率可能稍低)。通常更推荐使用 notifyAll() 以避免某些线程被“饿死”的风险。
-
-
点击查看代码
public class SharedResource {
private Integer value = null; // 共享资源(例如一个缓冲区槽位)
// 生产者方法
public synchronized void produce(int newValue) throws InterruptedException {
// 1. 检查条件:缓冲区是否已满(这里简化:有值就算满)
while (value != null) { // 必须用 while 循环检查条件!防止虚假唤醒
// 2. 条件不满足:生产者等待(释放锁)
wait();
}
// 3. 条件满足:生产数据
value = newValue;
System.out.println("Produced: " + newValue);
// 4. 生产后:通知可能正在等待的消费者(唤醒一个或所有消费者)
notifyAll(); // 或 notify(),但 notifyAll() 更安全
}
// 消费者方法
public synchronized int consume() throws InterruptedException {
// 1. 检查条件:缓冲区是否为空
while (value == null) { // 必须用 while 循环检查条件!
// 2. 条件不满足:消费者等待(释放锁)
wait();
}
// 3. 条件满足:消费数据
int consumedValue = value;
value = null; // 清空缓冲区
System.out.println("Consumed: " + consumedValue);
// 4. 消费后:通知可能正在等待的生产者(唤醒一个或所有生产者)
notifyAll(); // 或 notify(),但 notifyAll() 更安全
return consumedValue;
}
}