从JDK 5升级到JDK 6后实现各种锁优化技术如下:
- 适应性自旋(Adaptive Spinning)
- 锁消除(Lock Elimination)
- 锁膨胀(Lock Coarsening)
- 轻量级锁(Lightweight Locking)
- 偏向锁(Biased Locking)
(注:自旋锁在4的时候就有了,不过4中默认不开启,6中默认开启)
在介绍各个锁是什么?解决了什么问题?如何实现的?这三个问题前,先让我们设想这么一种场景前提:
你所处一个班级,这个班级呢有一个谁都可以写,但被锁住的班级日记本。日记本钥匙放在了老师办公室且用完一定要归还给老师。
自旋锁
场景模拟
第一天,你想在日记本上写些东西,所以你到老师办公室去拿钥匙,但是此时钥匙已经被另一个同学拿去记录了,你一时拿不到,
这时你想到一般大家写日记都很快(在许多应用上,共享数据的锁定状态只会持续很短的时间 )。回教室(线程挂起 )然后等老师通知有钥匙了(锁释放 )再过来(线程恢复 ),中间要走很远的路(性能消耗 ),不划算。
不如就在这等10s(自旋锁:默认自旋10次 ),于是你花费10s(占用处理器时间 )进行等待,最终在10s内成功获取到日记,避免了来回跑的时间损失(线程切换)。
自旋锁的作用是什么?
自旋锁的作用就是让请求锁的线程"稍等一会",在等待期间不放弃处理器的执行时间,看看持有锁的线程会不会很快释放锁。
自旋锁为什么要这么做?
自旋锁之所以这么做,是因为互 斥同步中对性能影响最大就是阻塞实现,线程挂起和恢复都要转入内核态中完成,给并发性能带来了很大的压力。
同时,在许多应用上,共享数据的锁定状态只会持续很短的时间,为了这一小段时间去浪费性能不值得。
自旋锁怎么实现的?
自旋锁是通过让线程执行一个忙循环来实现的。
但其中有一个点,就是自旋锁不能代替阻塞,自旋锁虽然避免了线程挂起恢复(线程切换)的开销,但需要占用处理器的时间。
所以自旋等待时间一定要有限度,默认的自旋次数是10次,超过次数线程会通过传统方式进行挂起。
自旋次数可以通过-XX:PreBlockSpin来自行更改。
自旋锁可以引申适应性自旋。
自旋次数虽然可以指定但仍然是一个固定的值,并不灵活,所以jdk6之后引入了适应性自旋的概念。
适应性自旋
第二天,你又想去写日记了,但聪明的你想到,每天大家记录的东西不一样,记录所花费的时间也不一样,
所以你想如果上一位同学记录需要11s,而你只等10s就回班就很不划算。如果如果上一位同学记录需要1000s,那其实可以直接下次再来。
怎么大致估算等待时间呢(锁释放时间 )聪明的你于是想到,可以询问上一个同学等待的时间(上一个线程在同一个锁上的自旋时间 )。
如果他等到了日记本,那自己便大概率也可以等到,10秒等不到也可以适当多等一会,比如100s(上一次获取到了,相应增加自旋次数 )。
如果大家都没等到,那便等老师通知有钥匙了再过来(省略自旋来减少处理器资源的浪费)。
适应性自旋的作用是什么?
自旋等待次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的,
简单讲就是上一获取到的情况下,虚拟机便会认为再次获取到的概率较大,会相应增加自旋次数。
如果历史上很少获取到,虚拟机则可能省略自旋来减少处理器资源的浪费。
如此,程序运行时间越长,虚拟机的预测便会越准确。
锁消除
场景模拟
第三天老师改了规则,老师决定笔记本只有你能写,而你发现每次写之前还得跑办公室拿钥匙开锁很麻烦(线程切换 ),
这时你就想呀,老师制定的新规则下只有我能写(逃逸分析:堆上的所有数据都不会被其他线程访问到 ),
我把笔记本直接不锁了不就好了(锁消除)。
什么是锁消除?
锁消除是在一些代码要求同步,但实际没有共享数据竞争的情况下,忽略同步措施(对锁进行消除)。
为什么需要锁消除?
锁消除可以在不需要代码中错误同步的位置减少线程切换的消耗。
锁消除怎么实现的?
主要依赖于逃逸分析,如果判断到一段代码中,在堆上的所有数据都不会被其他线程访问到,那就可以进行锁消除。
锁消除的例子:
假如你在一个线程安全代码快中使用StringBuffer的对象进行字符串连接,
虚拟机会观察这个对象,经过逃逸分析后,发现这个对象的操作被限制在了该代码块中,那么就会忽略所有同步措施。
逃逸分析?
锁粗化
场景模拟
第四天,老师又把钥匙收了回去。
你发现自己上午总是得拿三次钥匙回来开锁才能把话写完(一连串操作都是对同一个对象反复加锁解锁 ),
所以你就想呀,我不如一次性把三次想写的东西都写了(锁粗化 ),省的来回跑拿钥匙(频繁互斥同步操作)
什么是锁粗化?
如果在循环体加锁或者一连串同步操作都是对同一个对象进行,那么细粒度锁升级为粗粒度锁。
为什么需要锁粗化?
上面可以看到这一连串操作都是对同一个对象反复加锁解锁,也就是说出现了频繁互斥同步操作,导致了不必要的性能浪费。
锁粗化怎么实现的?
如果虚拟机发现有一串零碎的操作都是对同一个对象加锁,那就会把锁同步范围扩展到操作序列的外部。
轻量级锁
场景模拟
第五天,老师发现来拿钥匙的总是同一个人,于是同意在下一个人来拿钥匙前(线程竞争 ),钥匙可以暂时留在这位同学这里(轻量级锁 )。
于是这位同学在每次写日志前,不需要跑办公室去拿钥匙(线程挂起恢复 ),只需要拿钥匙开一下箱子(CAS )即可。
只有另一个同学也来拿钥匙时才需要把钥匙归还老师(升级重量级锁:维护互斥量)。
什么是轻量级锁?
通过CAS操作避免使用互斥量产生的开销。
为什么需要轻量级锁?
对于绝大部分锁,在整个同步周期内都是不存在竞争的。
轻量级锁是怎么实现的?
锁对象上维护了一个锁标志位(01未锁定 00 轻量级锁 10 重量级锁)
加锁
首先,在代码即将进入同步块时,如果锁标志位为01(未锁定),那么虚拟机首先将在当前线程的栈帧中建立一个Lock Record(锁记录)空间,并存储锁对象目前的Mark Word拷贝。
然后,通过CAS操作(记录在Lock Record中的值与锁对象的Mark wod比较,相同则将锁对象的Mark Word更新为线程的Lock Record空间的指针)
如果CAS成功,则将锁标志位更新为00(轻量级锁定状态)
如果CAS失败,首先会检查锁对象的Mark Word是否是当前线程的Lock Record指针。
是,则直接进入同步块。
否,则说明至少2条线程在竞争锁。此时则膨胀为重量级锁,锁标志位更新为10(重量级锁定状态),
此时Mark Word更新为指向重量级锁(互斥量)的指针。
解锁
也是通过CAS,实际就是加锁倒着来一遍。
CAS成功,则把线程 Lock Record空间内的指针复制回锁对象的Mark Word
CAS失败,则说明有其他线程尝试获取锁,所以要在释放的同时唤起锁

扩展
并不是所有情况下轻量级锁都会减少性能消耗,如果确实存在竞争,那么轻量级锁必将升级成重量级锁,这种情况下,CAS的开销就是额外产生的。
CAS(Compare-And-Swap)?
简单讲就是通过记录在记录在线程Lock Record中的值与锁对象的Mark wod比较,相同则将锁对象的Mark Word更新为线程的Lock Record空间的指针
通过这个操作来保证锁对象只能被一个对象持有
偏向锁
场景模拟
第六天,暂时保管钥匙的同学发现每次还是需要拿钥匙开一下箱子(CAS ),于是在自己家里拿来一个指纹锁暂时替代之前的锁(偏向模式:1 ),这样就不用每次都拿钥匙开锁了(避免CAS的开销 )。
当有其他同学也想写时,再把指纹锁拿掉(取消偏向模式:0 ),换回之前的钥匙锁,并把钥匙移交给另一个同学保管(升级为轻量级锁)。
什么是偏向锁?
锁对象会偏向第一个获取他的线程,如果一直是当前线程重复进入锁,没有线程竞争,那么偏向锁的线程则不需要同步。
为什么需要偏向锁?
偏向锁可以提高带有同步但无竞争的程序性能
简单讲偏向锁就是通过维护偏向状态,来减少当前线程重复进入锁带来CAS操作性能消耗。
偏向锁是怎么实现的?
产生偏向锁
线程第一次获取锁对象的时候,虚拟机会讲锁标志位设置为01(轻量级锁),同时把偏向模式设置为1(偏向模式)
同时进行轻量级锁加锁所产生的CAS操作(Mark Word -> Lock Record, 线程ID -> Mark Word)。
上诉CAS成功后,持有偏向锁的线程每次进入这个锁,都不再需要同步操作(加锁,解锁,Mark Word更新等)
结束偏向锁
一旦出现,线程竞争,偏向锁立刻结束,同时,偏向模式设置为0。锁标志位恢复成 01(未锁定) 00 (轻量级锁)。
后续像轻量级锁那样去执行。

拓展
如果程序中大多数的锁都总是被多个不同的线程访问,那偏向模式就是多余的。禁止反而会带来性能提升
使用参数-XX:-UseBiasedLocking可以禁止偏向锁优化。