synchronize原理

本文详细介绍了Java中的`synchronized`关键字,包括其基于monitor enter/exit指令的锁机制,以及对象头的Mark Word在实现锁中的作用。文章还探讨了从无锁到重量级锁的升级过程,如偏向锁和轻量级锁的优化策略,旨在减少锁的开销。同时,解释了为何wait和notify方法必须在`synchronized`代码块中使用的原因,因为它们与监视器锁密切相关。

synchronized同步块使用了monitor enter和monitor exit指令实现同步,这两个指令,本质上都是对一个对象的监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。

线程执行到monitor enter指令时,会尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁,而执行monitor exit,就是释放monitor的所有权。

在Hotspot虚拟机中,对象在内存中的布局分为三块区域:对象头(Mark Word、Class Metadata Address)、实例数据和对齐填充;Java对象头是实现synchronized的锁对象的基础。一般而言,synchronized使用的锁对象是存储在Java对象头里。它是轻量级锁和偏向锁的关键。

Monitor是一个同步工具,它内置于每一个Object对象中,相当于一个许可证。拿到许可证即可以进行操作,没有拿到则需要阻塞等待。在hotspot虚拟机中,通过ObjectMonitor类来实现monitor。

在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 偏向锁 和 轻量级锁 以及其它各种优化之后变得在某些情况下并不是那么重了锁的级别从低到高逐步升级: 无锁->偏向锁->轻量级锁->重量级锁。自旋锁与自适应自旋线程的挂起和恢复会极大的影响开销。并且jdk官方人员发现,很多线程在等待锁的时候,在很短的一段时间就获得了锁,所以它们在线程等待的时候,并不需要把线程挂起,而是让他无目的的循环,一般设置10次。这样就避免了线程切换的开销,极大的提升了性能。而适应性自旋,是赋予了自旋一种学习能力,它并不固定自旋10次一下。他可以根据它前面线程的自旋情况,从而调整它的自旋,甚至是不经过自旋而直接挂起。

偏向锁

多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让其获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成01(表示当前是偏向锁)。如果没有设置,则使用CAS竞争锁。如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

轻量级锁

引入轻量级锁的主要目的是在多线程竞争不激烈的情况下,通过CAS竞争锁,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

重量级锁

重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。

锁升级

偏向锁升级为轻量级锁:在一个对象持有偏向锁得时候,另外一个线程访问该对象。那么偏向锁升级为轻量级锁。

轻量级升级为重量级锁:当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁

wait和notify为什么需要在synchronized里面?

wait方法的语义有两个,一个是释放当前的对象锁、另一个是使得当前线程进入阻塞队列,而这些操作都和监视器是相关的,所以wait必须要获得一个监视器锁。而对于notify来说也是一样,它是唤醒一个线程,既然要去唤醒,首先得知道它在哪里,所以就必须要找到这个对象获取到这个对象的锁,然后到这个对象的等待队列中去唤醒一个线程。

### Java `synchronized` 机制原理深入分析 #### 1. 的基本概念 在Java中,`synchronized` 是一种用于实现线程同步的关键字。它的主要作用是确保多个线程在访问共享资源时不会发生冲突[^3]。`synchronized` 的实现依赖于对象头中的 `Mark Word` 和 `Monitor`。 - **对象头**:每个Java对象都有一个对象头,其中包含 `Mark Word` 和类元数据地址。`Mark Word` 存储了的状态、线程ID等信息[^5]。 - **Monitor**:`Monitor` 是一种同步工具,它保证同一时刻只有一个线程可以进入被 `synchronized` 保护的代码块或方法。 #### 2. `synchronized` 的三种应用方式 ` synchronized` 可以应用于以下三种场景: - **修饰实例方法**:当 `synchronized` 修饰实例方法时,的对象是当前实例(`this`)。这意味着只有持有该实例的线程才能执行该方法[^1]。 - **修饰静态方法**:当 `synchronized` 修饰静态方法时,的对象是当前类的 `Class` 对象。所有线程在调用该静态方法时都需要获取类级别的[^3]。 - **修饰代码块**:通过指定一个明确的对象作为,`synchronized` 可以保护特定的代码块。例如: ```java synchronized (lock) { // 临界区代码 } ``` 这种方式提供了更细粒度的控制[^2]。 #### 3. 字节码指令实现 在JVM层面,`synchronized` 的实现依赖于两个字节码指令: - **`monitorenter`**:线程在进入 `synchronized` 块或方法时会执行此指令,尝试获取对象的。 - **`monitorexit`**:线程在退出 `synchronized` 块或方法时会执行此指令,释放对象的[^3]。 这两个指令的操作是原子的,确保了的获取和释放过程的安全性。 #### 4. 的状态升级 ` synchronized` 的状态会根据竞争情况动态升级,从偏向到轻量级再到重量级。以下是状态升级的过程: - **偏向**:当没有线程竞争时,JVM会假设只会被一个线程持有。此时,`Mark Word` 中存储的是偏向线程的ID,避免频繁的获取和释放操作[^5]。 - **轻量级**:当有线程竞争但竞争不激烈时,JVM会使用自旋的方式尝试获取。这种方式避免了线程切换的开销。 - **重量级**:当竞争非常激烈时,JVM会将线程挂起,进入操作系统级别的等待队列。 #### 5. 示例代码解析 以下是一个典型的 `synchronized` 使用示例: ```java public class SynchronizedExample { private int count = 0; public synchronized void increment() { count++; } public static void main(String[] args) throws InterruptedException { SynchronizedExample example = new SynchronizedExample(); Thread t1 = new Thread(() -> { for (int i = 0; i < 1000; i++) { example.increment(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 1000; i++) { example.increment(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("Count: " + example.count); } } ``` 在此示例中,`increment` 方法被 `synchronized` 修饰,因此两个线程在调用该方法时会互斥执行[^1]。 #### 6. 注意事项 - `synchronized` 的对象必须明确且一致。如果对象不同,可能会导致线程之间的竞争无法正确解决[^1]。 - 在高并发场景下,`synchronized` 的性能可能不如其他机制(如 `ReentrantLock`),因为它缺乏一些高级特性(如公平、条件变量等)[^4]。 --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值