Java锁优化
自旋锁与自适应锁
当两个线程都需要访问某个共享资源时,常用互斥同步(synchronized)来保证并发正确性,即共享数据在同一时刻只被一个线程使用,其余线程暂时挂起(即Java中的线程阻塞状态)。但假如共享数据的锁定只会持续很短的时间,为了这段时间去挂起和恢复线程并不划算,因此可以让后面请求锁的线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需要让线程执行一个忙循环(自旋),这就是自旋锁。
如果锁被占用的时间很短,自旋等待的效果就非常好,如果时间很长,只会白白占用处理器资源。自旋等待的时间必须要有一定的限度,如果超过了限定的次数仍然没有成功获得锁,那就应该使用传统的方式去挂起线程了,自旋次数默认是10次。
JDK1.6 中引入了自适应的自旋锁,自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及拥有者的状态决定的。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那虚拟机认为这次自旋也很有可能再次成功,从而允许自旋等待更长的时间。如果对于某个锁,自旋很少成功过,那以后甚至可能省略掉这个自旋。
锁消除
简单而言,锁消除是指即时编译器会对一些代码上要求同步,但实际上不可能存在数据竞争的锁进行消除。
锁粗化
原则上,同步代码块的范围是越小越好,但如果你反复对一个对象加锁解锁,甚至将加锁的操作放在循环里,那性能是非常低的,当虚拟机探测到这种情况时,会将加锁同步的范围扩展到整个操作序列的外部,这样只需加一次锁。
轻量级锁
轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
谈论轻量级锁,就要先说虚拟机对象头 Mark Word 的内容布局,Mark Word 中含有 2bit 的锁标志位,每种不一样的标志,Mark Word 中都存储不同的内容。
锁标志位 | 状态 | Mark Word 储存内容 |
---|---|---|
01 | 未锁定 | 对象哈希码、对象分代年龄 |
00 | 轻量级锁定 | 指向锁记录的指针 |
10 | 膨胀(重量级锁定) | 指向重量级锁(互斥量)的指针 |
11 | GC标记 | 空,不需要记录信息 |
01 | 可偏向 | 偏向线程ID、偏向时间戳、对象分代年龄 |
在代码进入同步块的时候,如果此同步对象没有被锁定(标志位01),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录的空间,用于存储锁对象目前的Mark Word 的拷贝。然后虚拟机使用 CAS 操作尝试将对象的 Mark Word 更新为指向锁记录的指针。如果这个操作成功了,那这个线程就有了该对象的锁,并且对象的 Mark Word 的标志位变为 00,表示处于轻量级锁定状态。
如果这个更新操作失败了,且检查 Mark Word 后发现是别的线程拥有了锁,那就说明锁对象被其它线程占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁。
轻量级锁能提升同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。
偏向锁
偏向锁的目的是消除数据在无竞争情况下的同步源于。如果说轻量级锁是在无竞争的情况下使用 CAS 操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连 CAS 都不做了。
偏向锁的“偏”,是偏心的意思,它的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其它线程获取,则持有偏向锁的线程将永远不需要再进行同步。
当有另外一个线程尝试获取这个锁时,偏向模式就结束,锁膨胀为轻量级锁。
一个锁只能按照 偏向锁、轻量级锁、重量级锁的顺序逐渐升级(也有叫锁膨胀的),不允许降级。