synchronized锁的实现原理及锁升级

synchronized同步

        Java中每一个对象都可以作为锁。具体表现为以下3种形式:

  1. 普通同步方法,锁是当前实例对象

  2. 静态同步方法,锁是当前类的class对象

  3. 同步方法块,锁是synchronized括号里配置的对象

          当一个线程访问同步代码块时,它首先是需要得到锁才能执行同步代码,当退出或者抛出异常时必须要释放锁,那么它是如何来实现这个机制的呢?我们先看一段简单的代码:

public class SyncTest {
	public void syncBlock() {
		synchronized (this) {
			System.out.println("hello block");
		}
	}
	public synchronized void syncMethod() {
		System.out.println("hello method");
	}
}

        当SyncTest.java被编译成class文件的时候,synchronized关键字和synchronized方法的字节码略有不同,我们可以用javap -v 命令查看class文件对应的JVM字节码信息,部分信息如下: 

........省略代码........
{
  public SyncTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public void syncBlock();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #3                  // String hello block
         9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: aload_1
        13: monitorexit
        14: goto          22
        17: astore_2
        18: aload_1
        19: monitorexit
        20: aload_2
        21: athrow
        22: return
      Exception table:
         from    to  target type
             4    14    17   any
            17    20    17   any
      LineNumberTable:
        line 3: 0
        line 4: 4
        line 5: 12
        line 6: 22
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 17
          locals = [ class SyncTest, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

  public synchronized void syncMethod();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #5                  // String hello method
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 8: 0
        line 9: 8
}

从上面的中文注释处可以看到,

同步代码块

        对于synchronized关键字而言,javac在编译时,会生成对应的monitorenter和monitorexit指令分别对应synchronized同步块的进入和退出,有两个monitorexit指令的原因是:为了保证抛异常的情况下也能释放锁,所以javac为同步代码块添加了一个隐式的try-finally,在finally中会调用monitorexit命令释放锁。

同步方法:

        而对于synchronized方法而言,javac为其生成了一个ACCSYNCHRONIZED关键字,在JVM进行方法调用时,发现调用的方法被ACCSYNCHRONIZED修饰,则会先尝试获得锁。

对象头

 synchronized用的锁是存在Java对象头里的,对象头如下图

  • Mark Word:存储了对象的hashcode 以及锁信息、GC年龄等。
  • Class Metadata Address:存储类的元信息--Class对象的指针(Class对象在方法区中)。
  • Array length:如果数组的话,还会再存储下数组的长度。

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

重量级锁

重量级锁是我们常说的传统意义上的锁,其利用操作系统底层的同步机制(线程阻塞需要从用户态切换到核心态)去实现Java中的线程同步。

重量级锁的状态下,对象的mark word为指向一个堆中monitor对象的指针

Monitor 有两个队列 WaitSet 和 _EntryList,存储ObjectWaiter列表(所有等待的线程都会被包装成ObjectWaiter);

① 线程申请owner Monitor对象,首先会被加入到 _EntryList ;

② 线程申请owner Monitor对象,进入到 Owner区域,此时count +1;

③线程调用wait方法,释放锁,进入到 WaitSet ,此时count -1

④ 线程再次申请owner

⑤ 线程处理完毕后释放资源并退出。

轻量级锁

        轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗

轻量级锁加锁

1、在代码进入同步块的时候,JVM首先会在当前线程的栈帧中建立一个名为锁记录的空间(LockRecord),用于存储锁对象头中目前的MarkWord的拷贝,官方称之为Displaced Mark Word,这时候线程堆栈与对象头的状态如下图

2、然后虚拟机使用CAS(CompareAndSwap原子操作)尝试将对象的MarkWord更新为指向LockRecord的指针

2.1、如果更新成功,那么这个线程就拥有了该对象的锁,并且对象MarkWord的锁标志变为00,表示对象处于轻量级锁定状态,如下图

2.2、如果更新失败,JVM会先检查对象的MarkWord是否指向当前线程的栈桢,如果是则说明当前线程已经拥有了这个对象的锁(锁重入),就可以继续进入同步块继续执行。否则说明该锁对象已经被其他线程抢占了,当前线程变尝试使用自旋来获取锁。如果自旋获取锁成功,则继续以轻量级锁进入同步块;否则,(1)锁膨胀,将当前锁对象头的MarkWord修改成重量级锁、(2)该线程进入阻塞状态。 

轻量级锁解锁

轻量级解锁时,使用原子的CAS操作将DisplacedMarkWord替换到对象头MarkWord中,

如果替换成功,则表示没有发生锁竞争,结束。

如果失败,说明有其他线程尝试过获取锁(并且将锁对象膨胀为重量级锁),存在锁竞争,释放锁并唤醒被挂起的线程。

整体流程图如下图:(解锁过程中,对象膨胀为重量级锁的具体细节不太清楚,锁对象的MarkWord中如何指向了其他线程创建的那个重量级锁)

偏向锁(整体的细节都不是很清楚,因为描述方式因为角度问题而不一致)

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。

获取锁

描述一、

当锁对象第一次被线程A获取的时候,JVM将会把对象头的MarkWord的是否是偏向锁设置为1(即偏向模式)。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的MarkWord之中。

如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,JVM都可以不在进行任何同步操作。

当有另外一个线程B去尝试获取这个锁时,偏向锁模式就宣告失败,进入全局安全点,暂停拥有偏向锁的线程,检查其是否活着。

如果线程A不处于活跃状态,则将对象头的MarkWord的【是否是偏向锁】设置为0,无锁定状态,然后升级为轻量级锁。

如果线程A处于活跃状态,锁对象目前处于被锁定状态,原偏向的线程继续拥有锁,当前线程B则走入到锁升级的逻辑里;

描述二、

case 1:当该对象第一次被线程获得锁的时候,发现是匿名偏向状态(下图中的左分支),则会用CAS指令,将mark word中的thread id由0改成当前线程Id。如果成功,则代表获得了偏向锁,继续执行同步块中的代码。否则,将偏向锁撤销,升级为轻量级锁(下图中的右边分支)。

case 2:当被偏向的线程再次进入同步块时,发现锁对象偏向的就是当前线程,在通过一些额外的检查后,会往当前线程的栈中添加一条Displaced Mark Word为空的锁记录Lock Record,然后继续执行同步块的代码,因为操纵的是线程私有的栈,因此不需要用到CAS指令;(由此可见偏向锁模式下,当被偏向的线程再次尝试获得锁时,仅仅进行几个简单的操作就可以了,在这种情况下,synchronized关键字带来的性能开销基本可以忽略。

case 3.当其他线程进入同步块时,发现已经有偏向的线程了,则会进入到撤销偏向锁的逻辑里,一般来说,会在safepoint中去查看偏向的线程是否还存活,如果存活且还在同步块中则将锁升级为轻量级锁(中间下面指向右边的箭头),原偏向的线程继续拥有锁,当前线程则走入到锁升级的逻辑里;如果偏向的线程已经不存活或者不在同步块中(中间上面指向右边的箭头),则将对象头的mark word改为无锁状态(unlocked),之后再升级为轻量级锁。

锁的优缺点对比

优点 缺点 适用场景
偏向锁 加锁和解锁不需要执行额外的消耗,就是直接操作锁对象头的MarkWord,和执行非同步相比仅存在纳秒级的差距 如果线程间存在锁竞争,则带来额外的锁撤销的消耗 适用于只有一个线程访问同步块场景
轻量级锁

使用CAS消除互斥量

竞争的线程不会阻塞,提高了程序的响应速度。

采用自旋,不需要因阻塞而从用户态切换到核心态。

使用自旋会消耗CPU

追求响应速度

同步块执行速度非常快

重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间缓慢

追求吞吐量

同步块执行时间较长

参考死磕Synchronized底层实现

死磕 Java 并发:深入分析 synchronized 的实现原理 

发布了243 篇原创文章 · 获赞 138 · 访问量 138万+

猜你喜欢

转载自blog.csdn.net/ystyaoshengting/article/details/104286354