简介

synchronized是Java中的关键字,是一种同步锁,可作用于一段代码或方法,既可以保证可见性,又能够保证原子性。Java中的每一个对象都可以作为锁:

  • 对于同步方法,锁是当前实例对象。
  • 对于静态同步方法,锁是当前对象的Class对象。
  • 对于同步方法块,锁是Synchonized括号里配置的对象。

当一个线程试图访问同步代码时,它首先必须得到锁,退出或抛出异常时必须释放锁,而没获取到锁的线程将被阻塞。

synchronized实现原理

每个对象有一个监视器锁(monitor),当monitor被占用时就会处于锁定状态。

代码块同步是使用monitorentermonitorexit指令实现的,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

  1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
  2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
  3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

而对于monitorexit指令,指令执行时monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。

方法同步不是使用monitorentermonitorexit指令来完成,但同样是基于进入和退出monitor对象来实现,在方法执行期间,其他任何线程都无法再获得同一个monitor对象。

锁的升级

JDK1.6为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”,所以在JDK1.6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。

偏向锁

Hotspot的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。因此,引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的加锁解锁开销,即退出临界区的时候不需要主动释放锁,因为下一次要进入临界区的往往都是同一个线程。

偏向锁获取过程:

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

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态,如果线程仍然活着,将锁升级为标准的轻量级锁。

轻量级锁

轻量级锁的加锁过程:

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

在最后一步线程获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的,由于自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。JDK采用适应性自旋的方式解决这个问题,指定自旋的次数,如果超过次数如果还没获取到锁就进入阻塞状态。线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。

轻量级锁的解锁过程:

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

锁的对比

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

参考资料