synchronized 底层原理
以重量级锁为例,比如 T0、T1 两个线程同时执行加锁代码,已经出现了竞争(代码如下)
synchronized(obj) { // 加锁 ... } // 解锁
- 当执行到行1 的代码时,会根据 obj 的对象头找到或创建此对象对应的 Monitor 对象(C++对象)
- Monitor的核心结构包含:
- owner:指向持有该Monitor的线程
- WaitSet:存放出于wait状态的线程队列
- EntryList:存放出于阻塞状态的线程队列
- recursions:记录线程重入次数
- 检查 Monitor 对象的 owner 属性,用 Cas 操作去设置 owner 为当前线程,Cas 是原子操作,只能有一个线程能成功
- 假设 T0 Cas 成功,那么 T0 就加锁成功,将 recursions 置为1,若线程已持有该Monitor(可重入),则 recursions +1,然后T0线程继续执行 synchronized 代码块内的部分
- T1 这边 Cas 失败,会自旋若干次,重新尝试加锁,如果
- 重试过程中 T0 释放了锁,则 T1 不必阻塞,加锁成功
- 重试时 T0 仍持有锁,则 T1 会进入 Monitor 的等待队列EntryList,将来 T0 解锁后会唤醒它恢复运行(去重新抢锁)
synchronized 锁升级
synchronized 锁有三个级别:偏向锁、轻量级锁、重量级锁,性能从左到右逐渐降低
- 如果就一个线程对同一对象加锁,此时就用偏向锁
- 又来一个线程,与前一个线程交替为对象加锁,但只是交替,没有竞争,此时要升级为轻量级锁
- 如果多个线程加锁时发生了竞争,必须升级为重量级锁
【说明】
- 自 java 6 开始对 synchronized 提供了锁升级功能,之前只有重量级锁
- 但从 java 15 开始,偏向锁被标记为已废弃,将来会移除(因为实际带来的性能提升不明显,某些情况下反而影响性能)
对比 synchronized 和 volatile
并发编程需要从三个方面考虑线程安全,分别是:原子性、可见性、有序性
- volatile 修饰共享变量,可以保证它的可见性和有序性,但不能保证原子性(JMM模型)
- synchronized 代码块,不仅能保证共享变量的可见性、有序性,同时也能保证原子性
对比 synchronized 和 Lock
- synchronized 是关键字,Lock 是 Java 接口
- 前者底层是 C++ 代码实现锁,后者是 Java 自己的代码来实现锁
- Lock 功能更多,比如可以选择是公平锁还是非公平锁、可以设置加锁超时时间、可打断等
- Lock 的提供多种扩展实现(例如读写锁),可以根据场景选择更合适的实现
- Lock 释放锁需要调用 unlock 方法,而 synchronzied 在代码块结束无需显式调用就可以释放锁