JAVA 锁优化 笔记

高效并发是JDK一个非常重要的改进,HotSpot虚拟机开发团队花费大量的精力去实现各种锁的优化技术,如适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)等,这些技术都是为了线程间更高效地共享数据,以及解决竞争问题,提高程序执行效率。

自旋锁和自适应自旋
共享数据的锁定状态只会持续很短的时间,为了这一段时间挂起和恢复线程并不值得。如果机器有多个处理器,可以让多个线程同时并行执行,我们可以让后面请求锁的那个线程“稍等一段时间”,但是不放弃CPU执行时间,看看持有锁的线程是否很快就会释放掉锁。为了让线程等待,我们只需要让线程执行一个忙循环(自旋),这个技术就是所谓的自旋锁。

自旋锁在jdk1.4.2中已经引入,只不过默认是关闭状态,可以使用-XX:+UseSpinning参数开启,在jdk1.6中已经默认开启了。自旋等待不能代替阻塞,自旋要求有多处理器,自旋虽然避免了线程切换的开销,但是需要占用处理器时间,所以会白白消耗CPU资源,如果自旋时间非常长,就会带来资源的浪费。所以,自旋等待的时间必须有一定的限度,如果自旋次数超过了限定次数还没有成功获取锁,就应当使用传统的方式去挂起线程。自旋次数默认为10次,用户可以使用参数-XX:PreBlockSpin 配置。

jdk1.6引入自适应自旋锁,自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间和锁拥有者的状态来决定的。如果同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行,那么虚拟机会认为这次自旋也很有可能再次成功,进而允许自旋等待相对更长的时间。反之,如果对于某个锁,自旋很少获取到过,那么在以后获取这个锁的时候就会省略自旋的过程。有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况会预测的越来越准。

锁消除
锁消除的主要判定依据来源于逃逸分析的数据支持,判断堆上的数据都不会逃逸出去从而被其他线程访问,那就可以吧他们当做栈上数据对待,认为数据是线程私有的,加锁操作就可以忽略。具体可以看StringBuffer 在一个方法内部 定义,作用域只在方法内,锁会安全的消除掉,编译后所有的同步操作就会直接执行。

锁粗化
如果一系列操作都对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体内,就算没有线程竞争,频繁的互斥同步操作也会导致性能损耗。如果虚拟机探测到有这种操作,会把加锁同步的范围扩展到整个操作的序列外,只进行一次加锁和解锁。

轻量级锁
轻量级锁并不是用来替代重量级锁的,本意是在没有多线程竞争的前提下,减少传统重量级锁使用操作系统互斥量产生的性能损耗。主要依赖虚拟机对象头数据,进行CAS操作来实现加锁和解锁操作。[备注:后续文章会详细介绍JAVA 对象的内存布局]。对象头[Mark Word]中有2bit存储锁标志位,1bit固定为0,其他状态(轻量级锁定,重量级锁定,GC标记,可偏向)
01 ——> 未锁定状态 –> 存储 对象哈希码 对象分代年龄
00 –> 轻量级锁定 –> 指向锁记录指针
10 –> 重量级锁定 –> 指向重量级锁的指针
11 –> GC标记 –> 不记录任何消息
01 –> 可偏向 –> 偏向线程ID 偏向时间戳 对象分代年龄

代码进入同步快,如果同步对象没有锁定(01 状态),虚拟机首先在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced 前缀,即 Displaced Mark Word),然后虚拟机使用CAS操作尝试把对象的Mark Word 更新指向 Lock Record的指针。如果操作成功线程就拥有了该对象的锁,并把对象Mark Word 的标志位修改为”00″,表示对象处于轻量级锁定状态,如果更新失败,虚拟机首先检查对象的Mark Word是否指向当前线程的栈帧,如果是说明当前线程已经拥有这个对象的锁,那就可以直接进入同步块进行执行。否则,这个锁对象已经被其他线程抢占了。如果有2个以上线程竞争,那轻量级锁就不再有效,要膨胀为重量级锁,锁标志位变为”10″,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待的线程也会进入阻塞状态。

解锁过程也是通过CAS操作的,如果对象的Mark Word 仍然指向着线程的锁记录,那就用CAS 操作把对象当前的Mark Word 和 线程中复制的Displaced Mark Word 替换回来。如果替换成功整个同步过程就成功了,如果替换失败,说明其他线程正在尝试获取该锁,那就再释放锁的同时唤醒其他被挂起的线程。

轻量级锁提升性能主要依据:“对于绝大部分的锁,整个同步周期内都是不存在竞争的”,这只是一个经验数据。如果存在锁的竞争,出了额外的CAS操作,还有互斥量的开销,轻量级锁会比传统的锁性能差。

偏向锁

轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS都不做了。

偏向锁的“偏”,是偏向的意思,锁会偏向于第一个获取到它的线程,如果下面的操作,没有其他线程去获取该锁,则持有偏向锁的线程将永远不会同步。

启用偏向锁:-XX:+UseBiasedLocking jdk1.6默认开启。
第一次获取锁需要把状态标志设置 01 (偏向模式) ,同时使用CAS操作把当前线程ID记录在Mark Word中,CAS操作成功,持有偏向锁的线程每次进入这个锁的同步块时,虚拟机没有任何同步操作(Locking,UnLocking,Update Mark Word)
当其他线程去获取这个锁的时候,偏向模式结束,根据锁对象是否处于锁定状态,撤销偏向后恢复到未锁定(01)或者轻量级锁(00),后续和轻量级锁执行流程一样。

偏向锁可以提高带有同步但是无竞争的程序性能。它同样是带有权衡收益(Trade Off)的优化,也就是说,并不一定对程序有利,如果程序锁竞争比较激烈,那偏向就是多余的,具体问题具体分析,有时候关闭偏向锁性能反而提升了。