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