早期版本synchronized性能较低的原因
在早期版本中,synchronized是一种重量级锁,其底层由Monitor实现,而Monitor又依赖于操作系统的Mutex Lock。线程获取到锁后,需要切换状态,而操作系统在实现线程的切换时,需要从用户态转为核心态,这是一个非常耗时,非常重的操作。因此在之前,synchronized是一种重量级锁。
JDK1.6之后对synchronized的优化
现在的synchronized已经没有之前那么笨重了,在虚拟机层面,对synchronized做了较大的优化,引入了自旋锁、适应性自旋锁、锁消除、锁粗化,可以减少锁操作的开销。
自旋锁
有时候,获取到锁的线程执行的操作耗时极短,为了这么点微不足道的时间,将接下来等待锁的线程挂起非常的不值得。挂起线程的操作需要在核心态完成,从用户态切换到核心态,耗时比较严重。
因此现在增加这么一样操作,让等待锁的线程执行忙循环等待,不停地去尝试获取锁,像一种自旋的操作,故称之为自旋锁。
如果之前线程占有锁的时间极短,那么自旋锁的性能将非常的好。但若是占有锁的时间较长,那么自旋锁将白白消耗CPU的资源,在自旋次数到了之后,将会被挂起。
在jdk1.4的时候,自旋锁默认关闭;jdk1.6之后,自旋锁默认开启,默认自旋10次,当然也可以使用PreBlockSpin来修改自旋次数。
自旋锁的痛点在于:无法在不同场景中,确定出一个可靠的自旋次数。因此,衍生出来适应性自旋锁。
适应性自旋锁
在适应性自旋锁中,自旋的次数不再固定,一般由之前自旋的次数和锁持有者的状态决定。
如果在一个锁对象上,之前的线程都能通过自旋来获取到锁,并且没有超过自旋次数,那么虚拟机认为,通过自旋获取到锁的概率很大,下一次会增加自旋的次数。相反的,如果之前很少有线程通过自旋获取到锁,那么虚拟机会减少自旋的次数,减少到一定次数后,甚至会直接放弃自旋,升级为重量级锁。
可以看出,适应性自旋锁十分机智。
锁消除
从字面意思上可以看出,这是一种直接去除锁的方法,简单粗暴。
对于那些根本不可能存在锁竞争却又包含锁的情况,虚拟机会直接消除这个锁,避免无意义的锁请求。比如我在纯单线程中对某个方法或者变量加锁,或者调用内部实现有锁的对象(Vector、StringBuffer与HashTable等),虚拟机会直接消除毫无意义的加锁。
锁粗化
在上一文中【多线程】浅说Synchronized,我们谈到了synchronized的应用-双重检验锁的优化过程,强调将加锁的范围尽量限制得小一些,直到存在锁竞争的实际区域才加锁,这样程序运行更加高效。
但是,如果存在这样的一种情况:反复的对同一个对象执行加锁解锁的操作,也会导致CPU资源的过度消耗。
锁粗化,就是将反复的加锁操作粗化成一个范围更大的锁,这样加锁只有一次。
例如,在循环内部,调用StringBuffer的append操作(关于StringBuffer,可以参考我的另外一篇文章【JAVA】String、StringBuilder、StringBuffer三者的区别),每次append都需要加锁,虚拟机检测到这种情况后,首先会对append脱锁,然后进行锁粗化,将锁的范围扩大到循环外部。
锁的状态
锁的状态有以下几种:
- 无锁状态
- 偏向锁状态
- 轻量级锁状态
- 重量级锁状态
其中,无锁状态对应于锁消除,Monitor对应于重量级锁,也就是1.6之前的synchronized。
偏向锁
偏向锁的核心要义就体现在“偏”字上,这个锁偏向第一个获取到它的线程。
在大部分情况下,不存在激烈的锁竞争,总是由同一个线程获取到该锁。那么为了减少同一个线程获取锁带来的开销,就引入了偏向锁。
如果一个线程不断的获取到了锁,那么该锁就进入偏向锁状态。当这个线程再次请求锁时,无需做任何同步操作,直接获取到锁。
当然,偏向锁适用于基本无锁竞争的情况,当锁竞争激烈时,偏向锁就失去了作用,会升级为轻量级锁。
轻量级锁
在偏向锁的状态下,此时又出现了一个线程,与偏向线程竞争该锁,此时该锁会升级为轻量级锁。
举个例子,比如创建一个线程1执行同步print()方法打印奇数,这时候的锁状态为偏向锁。此时,再创建一个线程2同样执行同步print()方法打印偶数,偏向锁就会升级为轻量级锁。线程1打印某个奇数时,线程2并没有被挂起,而是处于一种自旋状态,这种自旋效率很高。可是,当我再创建100个线程时,同样执行同步print()方法,自旋的效率将会变得十分低下,此时轻量级锁会升级为重量级锁,即使用Monitor来进行同步。
锁的升级
无锁、偏向锁、轻量级锁与重量级锁,会随着锁竞争的升级而升级。
从一开始的偏向锁,产生锁竞争后,升级为轻量级锁,自旋失败后,升级为重量级锁,一般来说,锁的升级是单向的。