偏向锁、轻量级锁、重量级锁探究

同步代码块和同步方法

在 Java 中,关键字 synchronized 通常有两种使用方式,一是直接修饰在方法上定义同步方法,二是修饰单个对象,定义同步代码块:

public synchronized void syncMethod() {
  System.out.println("Sync method");
}

public void syncCodeBlock() {
  synchronized (this) {
    System.out.println("Sync code block");
  }
}

对于同步代码块来说,Javac 编译时会在同步代码块的前后插入 monitorenter 和 monitorexit 指令,同时保证只要执行了 monitorenter 指令,就必然会执行 monitorexit 指令。

比如说上面的 syncCodeBlock 方法,它的编译结果为:

public void syncCodeBlock();
  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           #5                  // String Sync code 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

可以看到,编译器在插入一个 monitorenter 后却插入了两个 monitorexit 指令,通过 Exception table 可以发现,当第 4 至 14 间的代码执行出现异常时,就会跳转到第 17 行执行, 此时,第 17 行后依然还有一个 monitorexit 指令保证同步代码块的退出。

但是对于同步方法来说,就不需要编译器添加 monitorenter 和 monitorexit 指令了,而是直接添加 ACC_SYNCHRONIZED 方法访问标志,方法的同步交由虚拟机完成:

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           #3                  // String Sync method
        5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        8: return

虽然说同步方法和同步代码块编译出来的结果不一样,但是,它们最后实现同步的方式还是一样的。

http://www.bubuko.com/infodetail-3413033.html#sec-3

Java对象头与Monitor

java对象头是实现synchronized的锁对象的基础,synchronized使用的锁对象是存储在Java对象头里的。

对象头包含两部分:Mark Word 和 Class Metadata Address

在这里插入图片描述

其中Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等以下是32位JVM的Mark Word默认存储结构

在这里插入图片描述

由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间,如32位JVM下,除了上述列出的Mark Word默认存储结构外,还有如下可能变化的结构:

在这里插入图片描述
https://www.cnblogs.com/deltadeblog/p/9559035.html

重量级锁

重量级锁指的就是一般意义上 synchronized 的同步方式,通过对象内部的监视器(monitor)实现,其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态到内核态的切换, 切换成本非常高。

获取重量级锁后,会在对象头中保存指向重量级锁对象的指针,并将锁标志位的值设为 10,当其他线程过来尝试获得锁时,就会进入等待,直到重量级锁释放。

由于将线程挂起同样需要系统调用,存在用户态和内核态之间的转换,为了减少这种操作,对于获取重量级锁失败的线程来说,还可以通过 自旋锁 来等待获取锁成功的线程执行完成释放锁

而自旋锁就是一个忙循环,因为很多同步块的执行时间并不是很长,因此通过一个忙循环等待来替代线程挂起是值得尝试的操作。

重量级锁synchronized的实现

重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。

在这里插入图片描述

上图简单描述多线程获取锁的过程,当多个线程同时访问一段同步代码时,首先会进入 Entry Set当线程获取到对象的monitor 后进入 The Owner 区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。

由此看来,monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因。

轻量级锁

获取释放重量级锁的消耗都是极为巨大的,如果临界区经常有几个线程同时访问,那么,这个消耗还可以接受,但是,如果临界区同一时间只有一个线程访问呢?这个时候还用重量级锁不就亏了?

因此,为了针对这一情况进行优化,虚拟机实现了轻量级锁,通过虚拟机自身在 用户态 下的 CAS 操作来替换获取释放重量级锁时的用户态内核态切换,其获取流程为:

  1. 判断当前对象是否处于无锁状态(偏向锁标志为 0,锁标志位为 01),若是,则在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word 的拷贝,否则执行步骤(3)
  2. 通过 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针,如果成功表示竞争到锁,将锁标志位变成 00,执行同步操作代码,如果失败则执行步骤(3)
  3. 判断当前对象的 Mark Word 是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块,否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁

在执行完同步代码后,轻量级锁会被主动释放,释放流程如下:

  1. 取出在获取轻量级锁保存在 Lock Record 中的数据
  2. 用 CAS 操作将取出的数据替换到当前对象的 Mark Word 中,如果成功,则说明释放锁成功,否则执行步骤 (3)
  3. 如果 CAS 操作替换失败,说明有其他线程尝试获取该锁,这时需要将该锁升级为重量级锁,并释放

轻量级锁的关键思路就在于通过 CAS 操作代替消耗大的系统调用,但是在频繁存在多个线程同时进入临界区的情况时,轻量级锁反而会带来额外的消耗。因此, 轻量级锁更适合不存在多个线程同时竞争同一个资源的情况。
在这里插入图片描述
在这里插入图片描述

轻量级锁

轻量级锁提升程序同步性能的依据是:对于绝大部分的锁,在整个同步周期内都是不存在竞争的(区别于偏向锁)。这是一个经验数据。如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁比传统的重量级锁更慢。

轻量级锁的加锁过程:

  • 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如图:

在这里插入图片描述

  • 拷贝对象头中的Mark Word复制到锁记录(Lock Record)中;

  • 拷贝成功后,虚拟机将使用CAS操作尝试将锁对象的Mark Word更新为指向Lock Record的指针,并将线程栈帧中的Lock Record里的owner指针指向Object的 Mark Word

  • 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图所示。

在这里插入图片描述

  • 如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。

在这里插入图片描述

偏向锁

虽然说轻量级锁通过 CAS 代替了系统调用减小了同步消耗,但是,如果临界区通常只有一个线程会进入呢?这时,是可以通过偏向锁进一步减小同步消耗的。

偏向锁通过如下措施进一步的减少了轻量级锁的消耗:

  1. 对象头中记录获取偏向锁成功的线程 ID,当该线程再次获取偏向锁时,发现线程 ID 一样,就可以直接通过判断执行同步代码,减少获取锁时的消耗
  2. 不主动释放偏向锁,仅在出现竞争时才是否偏向锁,减小释放锁的消耗

获取偏向锁的过程为:

  1. 检测 Mark Word 是否为可偏向状态(锁标志位为 01)
  2. 若为可偏向状态,则测试线程 ID 是否为当前线程 ID,如果是,则执行步骤 (5),否则执行步骤 (3)
  3. 如果线程 ID 不为当前线程 ID,则通过 CAS 操作竞争锁,竞争成功,则将 Mark Word 的线程 ID 替换为当前线程 ID,否则执行线程 (4)
  4. 通过 CAS 竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,撤销偏向锁,升级为轻量级锁,升级完成后被阻塞在安全点的线程继续往下执行同步代码
  5. 执行同步代码块

偏向锁不会主动释放只有当其它线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,释放过程为:

  1. 暂停拥有偏向锁的线程,判断锁对象是否还处于被锁定状态
  2. 撤销偏向锁,恢复到无锁状态(01)或者轻量级锁(00)的状态

偏向锁在 JDK 1.6 之后默认启用,可以通过 XX:-UseBiasedLocking=false 参数关闭偏向锁。

偏向锁

偏向锁是JDK6中引入的一项锁优化,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。

偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

当锁对象第一次被线程获取的时候,线程使用CAS操作把这个线程的ID记录在对象Mark Word之中,同时置偏向标志位1。以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要简单地测试一下对象头的Mark Word里是否存储着指向当前线程的ID。如果测试成功,表示线程已经获得了锁。

当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。根据锁对象目前是否处于被锁定的状态,撤销偏向后恢复到未锁定或轻量级锁定状态。

在这里插入图片描述

自旋锁与自适应自旋

Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间,对于代码简单的同步块(如被synchronized修饰的getter()和setter()方法),状态转换消耗的时间有可能比用户代码执行的时间还要长。

虚拟机的开发团队注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下“,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。

自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK1.6中已经变为默认开。自旋等待不能代替阻塞。自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间很长,那么自旋的线程只会浪费处理器资源。因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当使用传统的方式去挂起线程了。

JDK1.6中引入自适应的自旋锁,自适应意味着自旋的时间不在固定。而是有虚拟机对程序锁的监控与预测来设置自旋的次数。

自旋是在轻量级锁中使用的

自旋锁

首先,内核态与用户态的切换上不容易优化。但通过自旋锁,可以减少线程阻塞造成的线程切换(包括挂起线程和恢复线程)。

如果锁的粒度小,那么锁的持有时间比较短(尽管具体的持有时间无法得知,但可以认为,通常有一部分锁能满足上述性质)。那么,对于竞争这些锁的而言,因为锁阻塞造成线程切换的时间与锁持有的时间相当,减少线程阻塞造成的线程切换,能得到较大的性能提升。具体如下:

  • 当前线程竞争锁失败时,打算阻塞自己
  • 不直接阻塞自己,而是自旋(空等待,比如一个空的有限for循环)一会
  • 在自旋的同时重新竞争锁
  • 如果自旋结束前获得了锁,那么锁获取成功;否则,自旋结束后阻塞自己

如果在自旋的时间内,锁就被旧owner释放了,那么当前线程就不需要阻塞自己(也不需要在未来锁释放时恢复),减少了一次线程切换。

“锁的持有时间比较短”这一条件可以放宽。实际上,只要锁竞争的时间比较短(比如线程1快释放锁的时候,线程2才会来竞争锁),就能够提高自旋获得锁的概率。这通常发生在锁持有时间长,但竞争不激烈的场景中。

缺点

  • 单核处理器上,不存在实际的并行,当前线程不阻塞自己的话,旧owner就不能执行,锁永远不会释放,此时不管自旋多久都是浪费;进而,如果线程多而处理器少,自旋也会造成不少无谓的浪费。
  • 自旋锁要占用CPU,如果是计算密集型任务,这一优化通常得不偿失,减少锁的使用是更好的选择。
  • 如果锁竞争的时间比较长,那么自旋通常不能获得锁,白白浪费了自旋占用的CPU时间。这通常发生在锁持有时间长,且竞争激烈的场景中,此时应主动禁用自旋锁。

使用-XX:-UseSpinning参数关闭自旋锁优化;-XX:PreBlockSpin参数修改默认的自旋次数。

自适应自旋锁

自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定:

  • 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100个循环。
  • 相反的,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能减少自旋时间甚至省略自旋过程,以避免浪费处理器资源。

自适应自旋解决的是“锁竞争时间不确定”的问题。JVM很难感知到确切的锁竞争时间,而交给用户分析就违反了JVM的设计初衷。自适应自旋假定不同线程持有同一个锁对象的时间基本相当,竞争程度趋于稳定,因此,可以根据上一次自旋的时间与结果调整下一次自旋的时间。

缺点

然而,自适应自旋也没能彻底解决该问题,如果默认的自旋次数设置不合理(过高或过低),那么自适应的过程将很难收敛到合适的值。

作者:猴子007
链接:https://www.jianshu.com/p/36eedeb3f912
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

总结

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

java偏向锁,轻量级锁与重量级锁的相互膨胀

解答1

偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁。
一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。

轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

解答2

先说一下膨胀的条件:
1.偏向锁膨胀:线程A持有锁,其他线程请求过锁就会膨胀,也就是说对于锁来说,只要有超过一个线程请求过锁,注意是请求过,偏向锁就会膨胀成重量级锁。
2.轻量级锁膨胀:线程A在运行中持有锁,线程B竞争锁,线程B会首先自旋,自旋超时之后会膨胀成轻量级锁,注意条件是发生竞争。
3.重量级锁是最高的级别,不存在膨胀。

解答3

首先简单说下先偏向锁、轻量级锁、重量级锁三者各自的应用场景:

  • 偏向锁:只有一个线程进入临界区;
  • 轻量级锁:多个线程交替进入临界区;
  • 重量级锁:多个线程同时进入临界区。

还要明确的是,偏向锁、轻量级锁都是JVM引入的锁优化手段,目的是降低线程同步的开销。比如以下的同步代码块:

synchronized (lockObject) {
    // do something
}

上述同步代码块中存在一个临界区,假设当前存在Thread#1和Thread#2这两个用户线程,分三种情况来讨论:

  • 情况一:只有Thread#1会进入临界区;
  • 情况二:Thread#1和Thread#2交替进入临界区;
  • 情况三:Thread#1和Thread#2同时进入临界区。

上述的情况一是偏向锁的适用场景,此时当Thread#1进入临界区时,JVM会将lockObject的对象头Mark Word的锁标志位设为“01”,同时会用CAS操作把Thread#1的线程ID记录到Mark Word中,此时进入偏向模式。所谓“偏向”,指的是这个锁会偏向于Thread#1,若接下来没有其他线程进入临界区,则Thread#1再出入临界区无需再执行任何同步操作。也就是说,若只有Thread#1会进入临界区,实际上只有Thread#1初次进入临界区时需要执行CAS操作,以后再出入临界区都不会有同步操作带来的开销。

然而情况一是一个比较理想的情况,更多时候Thread#2也会尝试进入临界区。若Thread#2尝试进入时Thread#1已退出临界区,即此时lockObject处于未锁定状态,这时说明偏向锁上发生了竞争(对应情况二),此时会撤销偏向,Mark Word中不再存放偏向线程ID,而是存放hashCode和GC分代年龄,同时锁标识位变为“01”(表示未锁定),这时Thread#2会获取lockObject的轻量级锁。因为此时Thread#1和Thread#2交替进入临界区,所以偏向锁无法满足需求,需要膨胀到轻量级锁。

再说轻量级锁什么时候会膨胀到重量级锁。若一直是Thread#1和Thread#2交替进入临界区,那么没有问题,轻量锁hold住。一旦在轻量级锁上发生竞争,即出现“Thread#1和Thread#2同时进入临界区”的情况,轻量级锁就hold不住了。 (根本原因是轻量级锁没有足够的空间存储额外状态,此时若不膨胀为重量级锁,则所有等待轻量锁的线程只能自旋,可能会损失很多CPU时间)
https://www.zhihu.com/question/53826114

参考博客

Java 偏向锁、轻量级锁和重量级锁

http://www.bubuko.com/infodetail-3413033.html#sec-3

java 偏向锁、轻量级锁及重量级锁synchronized原理

https://www.cnblogs.com/deltadeblog/p/9559035.html

浅谈偏向锁、轻量级锁、重量级锁

https://www.jianshu.com/p/36eedeb3f912

Java中的偏向锁,轻量级锁, 重量级锁解析

https://blog.csdn.net/lengxiao1993/article/details/81568130

进阶(Java 中的偏向锁、轻量级锁和重量级锁)

https://jacobchang.cn/lock-of-synchronized.html

参考视频

你知道偏向锁、轻量级锁、重量级锁、锁重入、锁自旋都是什么吗?让我来带你一探究竟

https://www.bilibili.com/video/BV14C4y187WP?from=search&seid=11094219381764706364

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值