Synchronized关键字实现原理
- Synchronized关键字实现原理
- 基础概念
- 原理实现
- Mark Word
- Monitor
- 同步代码块原理
- 同步方法原理
- synchronized锁的优化
- Lock锁与synchronized
- Reference
Synchronized关键字实现原理
基础概念
- 修饰的类或者对象的所有操作都是
原子
的。 - 是实现
线程同步
的关键字。 - 修饰执行的代码块必须获得对象sync Object。
- 执行前先获得类或者对象的锁,直到执行完才能释放,
中间过程无法被中断
。
原理实现
Mark Word
- Mark Word,用于存储
对象自身运行时的数据
,如哈希码(Hash Code),GC分代年龄,锁状态标志,偏向线程ID、偏向时间戳等信息,它会根据对象的状态复用自己的存储空间。它是实现轻量级锁和偏向锁的关键。 - 类型指针,对象会指向它的类的元数据的指针,虚拟机通过这个指针确定这个对象是哪个类的实例。
- Array length,如果对象是一个数组,还必须记录数组长度的数据。
Monitor
- 上面经常提到monitor,它
内置在每一个对象中,任何一个对象都有一个monitor与之关联
,synchronized在JVM里的实现就是基于进入和退出monitor来实现的,底层则是通过成对的MonitorEnter和MonitorExit指令来实现,因此每一个Java对象都有成为Monitor的潜质。所以我们可以理解monitor是一个同步工具。
同步代码块原理
-
底层实现原理,使用
javap -v xxx.class
命令进行反编译。 -
monitorenter,如果当前monitor的进入数为0时,线程就会进入monitor,并且把进入数+1,那么该线程就是monitor的拥有者(owner)。如果该线程已经是monitor的拥有者,又重新进入,就会把进入数再次+1。也就是
可重入
的。monitorexit,执行monitorexit的线程必须是monitor的拥有者,指令执行后,monitor的进入数减1,如果减1后进入数为0,则该线程会退出monitor。其他被阻塞的线程就可以尝试去获取monitor的所有权。monitorexit指令出现了两次,第1次为同步正常退出释放锁
;第2次为发生异步退出释放锁
;总的来说,synchronized的底层原理是通过monitor对象来完成的。
同步方法原理
-
实例方法:
public synchronized void hello(){ System.out.println("hello world"); }
-
同理使用
javap -v
反编译: -
可以看到多了一个标志位ACC_SYNCHRONIZED,作用就是一旦执行到这个方法时,就会先判断是否有标志位,如果有这个标志位,就会先尝试获取monitor,获取成功才能执行方法,方法执行完成后再释放monitor。在方法执行期间,其他线程都无法获取同一个monitor。归根结底还是对monitor对象的争夺,只是同步方法是一种隐式的方式来实现。
synchronized锁的优化
-
前面讲过JDK1.5之前,synchronized是属于
重量级锁
,重量级需要依赖于底层操作系统的Mutex Lock实现
,然后操作系统需要切换用户态和内核态
,这种切换的消耗非常大,所以性能相对来说并不好。既然我们都知道性能不好,JDK的开发人员肯定也是知道的,于是在JDK1.6后开始对synchronized进行优化,增加了自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁这些优化策略。
锁的等级从无锁,偏向锁,轻量级锁,重量级锁逐步升级,并且是单向的,不会出现锁的降级。
-
自适应性自旋锁:
阻塞自旋避免阻塞和唤醒的切换。
但是自旋占用CPU,所以要自旋一段时间就挂起
(参数-XX:PreBlockSpin
设置自旋锁的自旋次数,当自旋一定的次数(时间)后就挂起)。如果设置次数少了或者多了都会导致性能受到影响,而且占用锁的时间在业务高峰期和正常时期也有区别,所以在JDK1.6引入了自适应性自旋锁。自适应性
自旋锁的意思是,自旋的次数不是固定的,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
。表现是如果此次自旋成功了,很有可能下一次也能成功,于是允许自旋的次数就会更多,反过来说,如果很少有线程能够自旋成功,很有可能下一次也是失败,则自旋次数就更少。这样能最大化利用资源,随着程序运行和性能监控信息的不断完善,虚拟机对锁的状况预测会越来越准确,也就变得越来越智能。 -
锁消除:
在JVM编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁。
如果在实例方法中StringBuffer作为局部变量使用append()方法,StringBuffer是不可能存在共享资源竞争的,因此会自动将其锁消除。 -
锁粗化:如果
一系列的连续加锁解锁操作,可能会导致不必要的性能损耗
,所以引入锁粗话的概念。意思是将多个连续加锁、解锁的操作连接在一起,扩展成为一个范围更大的锁
。 -
偏向锁:JDK的开发人员经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。也就是说在很多时候我们是假设有多线程的场景,但是实际上却是单线程的。所以偏向锁是在单线程执行代码块时使用的机制。锁的争夺实际上是Monitor对象的争夺,还有每个对象都有一个对象头,对象头是由Mark Word和Klass pointer 组成的。一旦有线程持有了这个锁对象,标志位修改为1,就进入偏向模式,同时会把这个线程的ID记录在对象的Mark Word中,当同一个线程再次进入时,就不再进行同步操作,这样就省去了大量的锁申请的操作,从而提高了性能。一旦有多个线程开始竞争锁的话呢?那么偏向锁并不会一下子升级为重量级锁,而是先升级为轻量级锁。(
缓存线程ID,再次进入无需锁申请
) -
轻量级锁:如果获取偏向锁失败,也就是有多个线程竞争锁的话,就会升级为JDK1.6引入的轻量级锁,Mark Word 的结构也变为轻量级锁的结构。执行同步代码块之前,
JVM会在线程的栈帧中创建一个锁记录(Lock Record),并将Mark Word拷贝复制到锁记录中。然后尝试通过CAS操作将Mark Word中的锁记录的指针,指向创建的Lock Record。
如果成功表示获取锁状态成功,如果失败,则进入自旋获取锁状态。
自旋锁的原理在上面已经讲过了,如果自旋获取锁也失败了,则升级为重量级锁,也就是把线程阻塞起来,等待唤醒。
-
重量级锁:重量级锁就是一个悲观锁了,但是其实不是最坏的锁,因为升级到重量级锁,是因为线程占用锁的时间长(自旋获取失败),锁竞争激烈的场景,在这种情况下,让线程进入阻塞状态,进入阻塞队列,能减少cpu消耗。所以说在不同的场景使用最佳的解决方案才是最好的技术。synchronized在不同的场景会自动选择不同的锁,这样一个升级锁的策略就体现出了这点。(
直接阻塞状态,避免长时间自旋
) -
偏向锁:适用于单线程执行。
轻量级锁:适用于锁竞争较不激烈的情况。
重量级锁:适用于锁竞争激烈的情况。
Lock锁与synchronized
-
synchronized是Java语法的一个关键字,加锁的过程是在JVM底层进行。Lock是一个类,是JDK应用层面的,在JUC包里有丰富的API。
-
synchronized在加锁和解锁操作上都是自动完成的,Lock锁需要我们手动加锁和解锁。
-
Lock锁有丰富的API能知道线程是否获取锁成功,而synchronized不能。
-
synchronized能修饰方法和代码块,Lock锁只能锁住代码块。
-
Lock锁有丰富的API,可根据不同的场景,在使用上更加灵活。
-
synchronized是非公平锁,而Lock锁既有非公平锁也有公平锁,可以由开发者通过参数控制。
-
在锁竞争不是很激烈的场景,使用synchronized,语义清晰,实现简单,JDK1.6后引入了偏向锁,轻量级锁等概念后,性能也能保证。而在锁竞争激烈,复杂的场景下,则使用Lock锁会更灵活一点,性能也较稳定。(具体情况具体分析)
Reference
- https://www.cnblogs.com/aspirant/p/11470858.html(深入分析Synchronized原理(阿里面试题))
- https://zhuanlan.zhihu.com/p/343305760(synchronized底层原理是什么?)
- https://baijiahao.baidu.com/s?id=1714937369261300308(synchronized底层原理)
- https://github.com/yehongzhi/learningSummary(learningSummary)