锁的数据结构

同步代码块是使用monitorenter和monitorexit指令实现的,任何java对象都有一个monitor与之关联,当一个monitor被持有后,对象就处于锁定状态。

在运行期间,Mard Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变化为存储以下数据结构。

自旋锁

  通常我们称Sychronized锁是一种重量级锁,是因为在互斥状态下,没有得到锁的线程会被挂起阻塞,而挂起线程和恢复线程的操作都需要转入内核态中完成。同时,虚拟机开发团队也注意到,许多应用上的数据锁只会持续很多的一段时间,如果为了这段时间去挂起和恢复线程是不值得的,所以引入了自旋锁。 
  自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是 否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。其作用是为了解决某项资源的互斥使用。因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远 高于互斥锁。虽然它的效率比互斥锁高,但是它也有些不足之处: 
  自旋锁一直占用CPU,他在未获得锁的情况下,一直运行--自旋,所以占用着CPU,如果不能在很短的时 间内获得锁,这无疑会使CPU效率降低。 
  自旋锁在JDK6以后已经默认开启,可以通过-XX:+UseSpinning参数来开启。 
  但这显然并不是最好的一种方法,不挂起线程的代价就是该线程会一直占用处理器。如果锁被占用的时间很短,自旋等待的效果就会很好,反之,自旋会消耗大量处理器资源。因此,自旋的等待时间必须有一定的限度,如果超过限度还没有获得锁,就要挂起线程,这个限度默认是10次,可以使用-XX:PreBlockSpin改变。 
  在JDK6以后又引入了自适应自旋锁,也就说自旋的时间限度不是一个固定值了,而是由上一次同一个锁的自旋时间及锁的拥有者状态来决定。虚拟机认为,如果同一个锁对象自旋刚刚成功获得锁,那么下一次很可能获得锁,所以允许这次自旋锁自旋很长时间、而如果某个锁很少获得锁,那么以后在获取锁的过程中可能忽略到自旋过程。

锁升级

  在Java 6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java中,锁共有4种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。  

轻量级锁

  “轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。

轻量级锁的加锁过程

  1. 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock 
    Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark 
    Word。这时候线程堆栈与对象头的状态如图一所示。
  2. 拷贝对象头中的Mark Word复制到锁记录中。
  3. 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤(4),否则执行步骤(5)。
  4. 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark 
    Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图二所示。
  5. 如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

图1— 轻量级锁CAS操作之前堆栈与对象的状态 

图2— 轻量级锁CAS操作之后堆栈与对象的状态 

轻量级锁的解锁过程

  1. 通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。
  2. 如果替换成功,整个同步过程就完成了。
  3. 如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。

偏向锁

  引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。上面说过,轻量级锁是为了在线程交替执行同步块时提高性能(通过自旋锁,使无法获得锁的线程无需立即进入阻塞状态,而是在一定时间内循环以获得锁,减少挂起线程和恢复线程带来的消耗),而偏向锁则是在只有一个线程执行同步块时进一步提高性能(在无多线程竞争情况下,获得锁的线程不释放锁,以减少CAS操作)。

偏向锁获取过程

  1. 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。
  2. 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。
  3. 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark 
    Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。
  4. 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
  5. 执行同步代码。

偏向锁的释放

  偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。 
   

重量级锁、轻量级锁和偏向锁之间转换

三种锁的优缺点比较

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步块场景
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 如果始终得不到锁竞争的线程使用自旋会消耗CPU 追求响应时间,锁占用时间很短
重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间缓慢 追求吞吐量,锁占用时间较长


重量级锁不使用自旋,获取锁失败的线程直接进入阻塞状态 
轻量级锁使用自旋