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 都不做了。

  偏向锁的“偏”,是偏心的意思,它的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其它线程获取,则持有偏向锁的线程将永远不需要再进行同步。

  当有另外一个线程尝试获取这个锁时,偏向模式就结束,锁膨胀为轻量级锁。

  一个锁只能按照 偏向锁、轻量级锁、重量级锁的顺序逐渐升级(也有叫锁膨胀的),不允许降级。

参考: 通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量/重量级锁、读写锁、各种锁及其Java实现!