farmerjohngit / myblog

有深度的Java技术博客
1.83k stars 287 forks source link

死磕Synchronized底层实现--概论 #12

Open farmerjohngit opened 5 years ago

farmerjohngit commented 5 years ago

关于synchronized的底层实现,网上有很多文章了。但是很多文章要么作者根本没看代码,仅仅是根据网上其他文章总结、照搬而成,难免有些错误;要么很多点都是一笔带过,对于为什么这样实现没有一个说法,让像我这样的读者意犹未尽。

本系列文章将对HotSpot的synchronized锁实现进行全面分析,内容包括偏向锁、轻量级锁、重量级锁的加锁、解锁、锁升级流程的原理及源码分析,希望给在研究synchronized路上的同学一些帮助。主要包括以下几篇文章:

死磕Synchronized底层实现--概论

死磕Synchronized底层实现--偏向锁

死磕Synchronized底层实现--轻量级锁

死磕Synchronized底层实现--重量级锁

更多文章见个人博客:https://github.com/farmerjohngit/myblog

大概花费了两周的实现看代码(花费了这么久时间有些忏愧,主要是对C++、JVM底层机制、JVM调试以及汇编代码不太熟),将synchronized涉及到的代码基本都看了一遍,其中还包括在JVM中添加日志验证自己的猜想,总的来说目前对synchronized这块有了一个比较全面清晰的认识,但水平有限,有些细节难免有些疏漏,还望请大家指正。

本篇文章将对synchronized机制做个大致的介绍,包括用以承载锁状态的对象头、锁的几种形式、各种形式锁的加锁和解锁流程、什么时候会发生锁升级。需要注意的是本文旨在介绍背景和概念,在讲述一些流程的时候,只提到了主要case,对于实现细节、运行时的不同分支都在后面的文章中详细分析

本人看的JVM版本是jdk8u,具体版本号以及代码可以在这里看到。

synchronized简介

Java中提供了两种实现同步的基础语义:synchronized方法和synchronized块, 我们来看个demo:

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 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                      // 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                       // monitorexit指令退出同步块
        14: goto          22
        17: astore_2
        18: aload_1
        19: monitorexit                       // monitorexit指令退出同步块
        20: aload_2
        21: athrow
        22: return
      Exception table:
         from    to  target type
             4    14    17   any
            17    20    17   any

  public synchronized void syncMethod();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED      //添加了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

}

从上面的中文注释处可以看到,对于synchronized关键字而言,javac在编译时,会生成对应的monitorentermonitorexit指令分别对应synchronized同步块的进入和退出,有两个monitorexit指令的原因是:为了保证抛异常的情况下也能释放锁,所以javac为同步代码块添加了一个隐式的try-finally,在finally中会调用monitorexit命令释放锁。而对于synchronized方法而言,javac为其生成了一个ACC_SYNCHRONIZED关键字,在JVM进行方法调用时,发现调用的方法被ACC_SYNCHRONIZED修饰,则会先尝试获得锁。

在JVM底层,对于这两种synchronized语义的实现大致相同,在后文中会选择一种进行详细分析。

因为本文旨在分析synchronized的实现原理,因此对于其使用的一些问题就不赘述了,不了解的朋友可以看看这篇文章

锁的几种形式

传统的锁(也就是下文要说的重量级锁)依赖于系统的同步函数,在linux上使用mutex互斥锁,最底层实现依赖于futex,关于futex可以看我之前的文章,这些同步函数都涉及到用户态和内核态的切换、进程的上下文切换,成本较高。对于加了synchronized关键字但运行时并没有多线程竞争,或两个线程接近于交替执行的情况,使用传统锁机制无疑效率是会比较低的。

在JDK 1.6之前,synchronized只有传统的锁机制,因此给开发者留下了synchronized关键字相比于其他同步机制性能不好的印象。

在JDK 1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。

在看这几种锁机制的实现前,我们先来了解下对象头,它是实现多种锁机制的基础。

对象头

因为在Java中任意对象都可以用作锁,因此必定要有一个映射关系,存储该对象以及其对应的锁信息(比如当前哪个线程持有锁,哪些线程在等待)。一种很直观的方法是,用一个全局map,来存储这个映射关系,但这样会有一些问题:需要对map做线程安全保障,不同的synchronized之间会相互影响,性能差;另外当同步对象较多时,该map可能会占用比较多的内存。

所以最好的办法是将这个映射关系存储在对象头中,因为对象头本身也有一些hashcode、GC相关的数据,所以如果能将锁信息与这些信息共存在对象头中就好了。

在JVM中,对象在内存中除了本身的数据外还会有个对象头,对于普通对象而言,其对象头中有两类信息:mark word和类型指针。另外对于数组而言还会有一份记录数组长度的数据。

类型指针是指向该对象所属类对象的指针,mark word用于存储对象的HashCode、GC分代年龄、锁状态等信息。在32位系统上mark word长度为32bit,64位系统上长度为64bit。为了能在有限的空间里存储下更多的数据,其存储格式是不固定的,在32位系统上各状态的格式如下:

image

可以看到锁信息也是存在于对象的mark word中的。当对象状态为偏向锁(biasable)时,mark word存储的是偏向的线程ID;当状态为轻量级锁(lightweight locked)时,mark word存储的是指向线程栈中Lock Record的指针;当状态为重量级锁(inflated)时,为指向堆中的monitor对象的指针。

重量级锁

重量级锁是我们常说的传统意义上的锁,其利用操作系统底层的同步机制去实现Java中的线程同步。

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

一个monitor对象包括这么几个关键字段:cxq(下图中的ContentionList),EntryList ,WaitSet,owner。

其中cxq ,EntryList ,WaitSet都是由ObjectWaiter的链表结构,owner指向持有锁的线程。

1517900250327

当一个线程尝试获得锁时,如果该锁已经被占用,则会将该线程封装成一个ObjectWaiter对象插入到cxq的队列尾部,然后暂停当前线程。当持有锁的线程释放锁前,会将cxq中的所有元素移动到EntryList中去,并唤醒EntryList的队首线程。

如果一个线程在同步块中调用了Object#wait方法,会将该线程对应的ObjectWaiter从EntryList移除并加入到WaitSet中,然后释放锁。当wait的线程被notify之后,会将对应的ObjectWaiter从WaitSet移动到EntryList中。

以上只是对重量级锁流程的一个简述,其中涉及到的很多细节,比如ObjectMonitor对象从哪来?释放锁时是将cxq中的元素移动到EntryList的尾部还是头部?notfiy时,是将ObjectWaiter移动到EntryList的尾部还是头部?

关于具体的细节,会在重量级锁的文章中分析。

轻量级锁

JVM的开发者发现在很多情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此JVM引入了轻量级锁的概念。

线程在执行同步块之前,JVM会先在当前的线程的栈帧中创建一个Lock Record,其包括一个用于存储对象头中的 mark word(官方称之为Displaced Mark Word)以及一个指向对象的指针。下图右边的部分就是一个Lock Record

img

加锁过程

1.在线程栈中创建一个Lock Record,将其obj(即上图的Object reference)字段指向锁对象。

2.直接通过CAS指令将Lock Record的地址存储在对象头的mark word中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。如果失败,进入到步骤3。

3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分(Displaced Mark Word)为null,起到了一个重入计数器的作用。然后结束。

4.走到这一步说明发生了竞争,需要膨胀为重量级锁。

解锁过程

1.遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record

2.如果Lock RecordDisplaced Mark Word为null,代表这是一次重入,将obj设置为null后continue。

3.如果Lock RecordDisplaced Mark Word不为null,则利用CAS指令将对象头的mark word恢复成为Displaced Mark Word。如果成功,则continue,否则膨胀为重量级锁。

偏向锁

Java是支持多线程的语言,因此在很多二方包、基础库中为了保证代码在多线程的情况下也能正常运行,也就是我们常说的线程安全,都会加入如synchronized这样的同步语义。但是在应用在实际运行时,很可能只有一个线程会调用相关同步方法。比如下面这个demo:

import java.util.ArrayList;
import java.util.List;

public class SyncDemo1 {

    public static void main(String[] args) {
        SyncDemo1 syncDemo1 = new SyncDemo1();
        for (int i = 0; i < 100; i++) {
            syncDemo1.addString("test:" + i);
        }
    }

    private List<String> list = new ArrayList<>();

    public synchronized void addString(String s) {
        list.add(s);
    }

}

在这个demo中为了保证对list操纵时线程安全,对addString方法加了synchronized的修饰,但实际使用时却只有一个线程调用到该方法,对于轻量级锁而言,每次调用addString时,加锁解锁都有一个CAS操作;对于重量级锁而言,加锁也会有一个或多个CAS操作(这里的’一个‘、’多个‘数量词只是针对该demo,并不适用于所有场景)。

在JDK1.6中为了提高一个对象在一段很长的时间内都只被一个线程用做锁对象场景下的性能,引入了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只会执行几个简单的命令,而不是开销相对较大的CAS命令。我们来看看偏向锁是如何做的。

对象创建

当JVM启用了偏向锁模式(1.6以上默认开启),当新创建一个对象的时候,如果该对象所属的class没有关闭偏向锁模式(什么时候会关闭一个class的偏向模式下文会说,默认所有class的偏向模式都是是开启的),那新创建对象的mark word将是可偏向状态,此时mark word中的thread id(参见上文偏向状态下的mark word格式)为0,表示未偏向任何线程,也叫做匿名偏向(anonymously biased)。

加锁过程

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

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

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

由此可见,偏向锁升级的时机为:当锁已经发生偏向后,只要有另一个线程尝试获得偏向锁,则该偏向锁就会升级成轻量级锁。当然这个说法不绝对,因为还有批量重偏向这一机制。

解锁过程

当有其他线程尝试获得锁时,是根据遍历偏向线程的lock record来确定该线程是否还在执行同步块中的代码。因此偏向锁的解锁很简单,仅仅将栈中的最近一条lock recordobj字段设置为null。需要注意的是,偏向锁的解锁步骤中并不会修改对象头中的thread id。

下图展示了锁状态的转换流程:

img

另外,偏向锁默认不是立即就启动的,在程序启动后,通常有几秒的延迟,可以通过命令 -XX:BiasedLockingStartupDelay=0来关闭延迟。

批量重偏向与撤销

从上文偏向锁的加锁解锁过程中可以看出,当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时将偏向锁撤销为无锁状态或升级为轻量级/重量级锁。safe point这个词我们在GC中经常会提到,其代表了一个状态,在该状态下所有线程都是暂停的(大概这么个意思),详细可以看这篇文章。总之,偏向锁的撤销是有一定成本的,如果说运行时的场景本身存在多线程竞争的,那偏向锁的存在不仅不能提高性能,而且会导致性能下降。因此,JVM中增加了一种批量重偏向/撤销的机制。

存在如下两种情况:(见官方论文第4小节):

1.一个线程创建了大量对象并执行了初始的同步操作,之后在另一个线程中将这些对象作为锁进行之后的操作。这种case下,会导致大量的偏向锁撤销操作。

2.存在明显多线程竞争的场景下使用偏向锁是不合适的,例如生产者/消费者队列。

批量重偏向(bulk rebias)机制是为了解决第一种场景。批量撤销(bulk revoke)则是为了解决第二种场景。

其做法是:以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的mark word中也有该字段,其初始值为创建该对象时,class中的epoch的值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其mark word的Thread Id 改成当前线程Id。

当达到重偏向阈值后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。

End

Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。当条件不满足时,锁会按偏向锁->轻量级锁->重量级锁 的顺序升级。JVM种的锁也是能降级的,只不过条件很苛刻,不在我们讨论范围之内。该篇文章主要是对Java的synchronized做个基本介绍,后文会有更详细的分析。

josek43326 commented 5 years ago

关于 锁 能不能降级的问题;我在《java并发编程的艺术》这本书中2.2.2节的第四行中看到, “锁可以升级,但是不能降级”,与这篇文章的End里面的描述有点矛盾,想再次请教一下,谢谢啦

Geker commented 5 years ago

在32位系统上mark word长度为32字节,64位系统上长度为64字节 ### bytes->bits

farmerjohngit commented 5 years ago

@JoseK-43326 这是个好问题,先说结论,在openjdk的hotsopt jdk8u里是有锁降级的机制的,锁降级是什么时候加入到hotspot的这个我没去关注,所以我只说看过代码的jdk8u版本,另外根据R大的这个回答,我相信sunj dk也一样。

然后再详细说:

锁降级的代码在deflate_idle_monitors方法中,其调用点在进入SafePoint的方法SafepointSynchronize::begin()中。 在deflate_idle_monitors中会找到已经idle的monitor(也就是重量级锁的对象),然后调用deflate_monitor方法将其降级。

因为锁降级是发生在safepoint的,所以如果降级时间过长会导致程序一直处于STW的阶段。在这里有篇文章讨论了优化机制。jdk8中本身也有个MonitorInUseLists的开关,其影响了寻找idle monitor的方式,对该开关的一些讨论看这里

至于为什么《java并发编程的艺术》中说锁不能降级,我猜测可能该书作者看的jdk版本还没有引入降级机制。

farmerjohngit commented 5 years ago

@Geker 感谢指正

josek43326 commented 5 years ago

@farmerjohngit 感谢指教

gallant7 commented 5 years ago

您好!想请问一下,我看网上很多讲偏向锁的文章会把偏向锁的撤销和释放作为一个概念;还有的说偏向锁只有撤销锁的操作,没有释放锁的操作;您的文章里强调了释放和撤销的不同,请问您一下,关于这个您是怎么理解的,怎么去区分这两个操作?以及什么时候会触发这两个操作呐?期待您的回答 谢谢,还有简书里的也是我问的 您回答一个就可以啦谢谢

farmerjohngit commented 5 years ago

@gallant7 在偏向锁一文中已经说了:

这里说的撤销是指在获取偏向锁的过程因为不满足条件导致要将锁对象改为非偏向锁状态;释放是指退出同步块的过程,释放锁的逻辑会在下一小节阐述。请读者注意本文中撤销与释放的区别。

触发时机: 释放:对应的就是synchronized方法的退出或synchronized块的结束。 撤销:笼统的说就是多个线程竞争导致不能再使用偏向模式的时候。

gallant7 commented 5 years ago

synchronized不是重量级锁吗?它是有释放锁的过程的。是不是撤销锁的时候将对象头变为无锁态时是释放锁的过程?

farmerjohngit commented 5 years ago

synchronized不是重量级锁吗?它是有释放锁的过程的。是不是撤销锁的时候将对象头变为无锁态时是释放锁的过程?

synchronized有偏向锁、轻量级锁和重量级锁啊。。。 建议你先把文章看一遍吧

LYoGA commented 5 years ago

你好~文中描述到:如果偏向的线程已经不存活或者不在同步块中,则将对象头的mark word改为无锁状态(unlocked),之后再升级为轻量级锁; 想问下,当mark word置为无锁状态之后,下次线程获得锁的时候直接就升级成为轻量级锁了么,为什么不是还是偏向锁呢

farmerjohngit commented 5 years ago

@LYoGA 偏向锁的诞生背景是JVM的开发人员发现在很多场景下,加了synchronized的方法或代码块在运行过程中其实是单线程使用(比如一些工具类为了保证其提供方法的线程安全会加synchronized,但在我们的应用中可能就是单线程使用),所以JVM为了提高性能加入了偏向锁机制。

而你说的当mark word置为无锁状态之后,就代表该锁对象已经被多个线程使用,已经不满足偏向锁的适用场景了。如果下次获得锁的时候还设置为偏向锁,那可能会有频繁的锁状态切换,导致性能比重量级锁还低。

gezhiwei8899 commented 5 years ago

大佬厉害,如何把jdk的源码导入ide 打断掉调试有文章介绍么?

angiie commented 5 years ago

大佬厉害,如何把jdk的源码导入ide 打断掉调试有文章介绍么?

这个去看字节码生成的原理就能知道。

farmerjohngit commented 5 years ago

@gezhiwei8899 google下 很多文章

gezhiwei8899 commented 5 years ago

大佬厉害,如何把jdk的源码导入ide 打断掉调试有文章介绍么?

这个去看字节码生成的原理就能知道。

所错了是jvm源码,怎么导入 Clion 这个IDE工具 @farmerjohngit 大佬是不是C++ 也玩的很6

farmerjohngit commented 5 years ago

@gezhiwei8899 c++不怎么会,所以我也没倒入过ide,直接用gdb调试的。

wrbfly commented 5 years ago

请教一下, 偏向锁释放的时候具体做了什么呢? 另外,[是否偏向锁]这个标志位的具体含义一直没搞透彻,jvm如果配置了开启偏向锁就是1没有配置开启就是0么?

farmerjohngit commented 5 years ago

@wrbfly 偏向锁释放看这篇文章 https://github.com/farmerjohngit/myblog/issues/13 是否偏向锁 代表当前锁对象的状态

zhipingzhang commented 5 years ago

大佬,看了几篇文章,一直有个疑问,就是hashcode去哪了? 那新创建对象的mark word将是可偏向状态,此时mark word中的thread id(参见上文偏向状态下的mark word格式)为0,表示未偏向任何线程,也叫做匿名偏向(anonymously biased)。 这句我很难理解,也就是对象一创建出来就直接没hashcode吗?

joeyleeeeeee97 commented 5 years ago

大佬,看了几篇文章,一直有个疑问,就是hashcode去哪了? 那新创建对象的mark word将是可偏向状态,此时mark word中的thread id(参见上文偏向状态下的mark word格式)为0,表示未偏向任何线程,也叫做匿名偏向(anonymously biased)。 这句我很难理解,也就是对象一创建出来就直接没hashcode吗?

HotSpot JVM在第一次调用Object.hashCode或System.identityHashCode时计算身份hashCode,并将其存储在对象头中。随后的调用只是从头中提取以前计算的值。

snipercy commented 5 years ago

hi,

轻量级锁 解锁过程 1.遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。

请教下,这个遍历是具体怎么遍历呢?那么多栈帧

snipercy commented 5 years ago

因此偏向锁的解锁很简单,仅仅将栈中的最近一条lock record的obj字段设置为null。

为什么只需要将最近的lock record设置为null 就可以了啊?

ValiantYuan commented 5 years ago

作者你好,我之前看了你的系列文章受益匪浅,现在在知乎看到有人把你的文章搬运过去了没有说明是转发,不知道是不是你本人?知乎链接地址:https://zhuanlan.zhihu.com/p/76794925

farmerjohngit commented 5 years ago

@ValiantYuan 不是我。。 太无耻了,转发出处都不保留

hexinwei1 commented 5 years ago

一直有个问题,某个对象锁因为线程竞争激烈升级为重量级锁,之后某一段时间没有线程竞争了,这个锁的对象头的锁标志位会变成什么状态?①01-无锁状态,下次再有线程来重新偏向锁-->轻量级锁-->重量级锁 。还是 ②10-重量级锁状态,下次再有线程来直接获取重量级锁。

sangyue174 commented 5 years ago

楼主你好, 有一个疑问, 关于批量重偏向的, 上文有提到

每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的mark word中也有该字段,其初始值为创建该对象时,class中的epoch的值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其mark word的Thread Id 改成当前线程Id。

这一句epoch值和class的epoch不相等, 就代表已经偏向了其他线程有些不明白, 正常来看, 在发生批量重偏向时, 锁对象对应的class和该class对应的加有偏向锁对象mark word中的epoch同时+1, 这里无关线程, 只是所有该class的偏向锁对象, 那么这两个epoch应该是相等的, 又怎么会出现后边不相等就偏向了其他线程的结论呢??希望作者解下惑.

waterystone commented 5 years ago

@gallant7 在偏向锁一文中已经说了:

这里说的撤销是指在获取偏向锁的过程因为不满足条件导致要将锁对象改为非偏向锁状态;释放是指退出同步块的过程,释放锁的逻辑会在下一小节阐述。请读者注意本文中撤销与释放的区别。

触发时机: 释放:对应的就是synchronized方法的退出或synchronized块的结束。 撤销:笼统的说就是多个线程竞争导致不能再使用偏向模式的时候。

我也一直在想,偏向锁释放时,为什么占用线程不把对象头置为无锁状态呢?而是要等下一个线程加锁时才来清除。这是基于怎样的考量呢?

RemoteMountain commented 5 years ago

楼主你好, 有一个疑问, 关于批量重偏向的, 上文有提到

每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的mark word中也有该字段,其初始值为创建该对象时,class中的epoch的值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其mark word的Thread Id 改成当前线程Id。

这一句epoch值和class的epoch不相等, 就代表已经偏向了其他线程有些不明白, 正常来看, 在发生批量重偏向时, 锁对象对应的class和该class对应的加有偏向锁对象mark word中的epoch同时+1, 这里无关线程, 只是所有该class的偏向锁对象, 那么这两个epoch应该是相等的, 又怎么会出现后边不相等就偏向了其他线程的结论呢??希望作者解下惑.

你好,我理解的也是在发生批量重偏向的时候两者是相等的,所以不是很理解为什么会在下次获取锁的时候出现不等的情况。请问你搞明白了吗?

Shuai-Meng commented 4 years ago

你好~文中描述到:如果偏向的线程已经不存活或者不在同步块中,则将对象头的mark word改为无锁状态(unlocked),之后再升级为轻量级锁; 想问下,当mark word置为无锁状态之后,下次线程获得锁的时候直接就升级成为轻量级锁了么,为什么不是还是偏向锁呢

同问。

mazhimazh commented 4 years ago

楼主你好, 有一个疑问, 关于批量重偏向的, 上文有提到

每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的mark word中也有该字段,其初始值为创建该对象时,class中的epoch的值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其mark word的Thread Id 改成当前线程Id。

这一句epoch值和class的epoch不相等, 就代表已经偏向了其他线程有些不明白, 正常来看, 在发生批量重偏向时, 锁对象对应的class和该class对应的加有偏向锁对象mark word中的epoch同时+1, 这里无关线程, 只是所有该class的偏向锁对象, 那么这两个epoch应该是相等的, 又怎么会出现后边不相等就偏向了其他线程的结论呢??希望作者解下惑.

你好,我理解的也是在发生批量重偏向的时候两者是相等的,所以不是很理解为什么会在下次获取锁的时候出现不等的情况。请问你搞明白了吗?

我也是同样的问题,请问你们理解了吗

GarsonChan commented 4 years ago

楼主你好, 有一个疑问, 关于批量重偏向的, 上文有提到

每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的mark word中也有该字段,其初始值为创建该对象时,class中的epoch的值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其mark word的Thread Id 改成当前线程Id。

这一句epoch值和class的epoch不相等, 就代表已经偏向了其他线程有些不明白, 正常来看, 在发生批量重偏向时, 锁对象对应的class和该class对应的加有偏向锁对象mark word中的epoch同时+1, 这里无关线程, 只是所有该class的偏向锁对象, 那么这两个epoch应该是相等的, 又怎么会出现后边不相等就偏向了其他线程的结论呢??希望作者解下惑.

你好,我理解的也是在发生批量重偏向的时候两者是相等的,所以不是很理解为什么会在下次获取锁的时候出现不等的情况。请问你搞明白了吗?

我也是同样的问题,请问你们理解了吗

批量重偏向中,epoch自增针对的是 klass 和 被当前存活的thread持有的Oop锁对象。而还存在一种锁对象是:在批量重偏向时,没有被任何thread持有(也就是当前没有thread在执行对应的synchronize代码),但之前被thread持有过。所以,这种锁对象的markword是偏向状态的,但它的epoch与klass的epoch不相等。在下一次其他thread准备持有它时,不会因为当前thread的threadId和锁对象markword中的threadId不同而升级为轻量级锁,而是直接CAS成偏向当前thread的markWord(因为锁对象的epoch与klass的epoch不同),从而达到批量重偏向的优化效果。

coolxbin commented 4 years ago

作者你好,关于偏向锁加锁过程的case2,有这样一个描述:

"case 2:当被偏向的线程再次进入同步块时,发现锁对象偏向的就是当前线程,在通过一些额外的检查后(细节见后面的文章),会往当前线程的栈中添加一条Displaced Mark Word为空的Lock Record中,然后继续执行同步块的代码,因为操纵的是线程私有的栈,因此不需要用到CAS指令;由此可见偏向锁模式下,当被偏向的线程再次尝试获得锁时,仅仅进行几个简单的操作就可以了,在这种情况下,synchronized关键字带来的性能开销基本可以忽略。" 其中,这一句“会往当前线程的栈中添加一条Displaced Mark Word为空的Lock Record中”,我有疑问,不知您是否可以交流解答一下: 在看了偏向锁的源码之后,发现锁已经偏向了当前线程,没有做任何处理,源代码如下:

CASE(_monitorenter): {
        ...
       // 如果各bit全为0,表示锁对象的偏向锁已经被当前线程获取
        if  (anticipated_bias_locking_value == 0) {
              // already biased towards this thread, nothing to do
              if (PrintBiasedLockingStatistics) {
                (* BiasedLocking::biased_lock_entry_count_addr())++;
              }
              // 获取成功,不做任何处理
              success = true;
         }

所以,想请教下,您此处所述的处理Lock Record的源代码是在哪个位置,我估计是我漏掉了。 先谢谢啦!

zongshoujin commented 4 years ago

@gallant7 在偏向锁一文中已经说了:

这里说的撤销是指在获取偏向锁的过程因为不满足条件导致要将锁对象改为非偏向锁状态;释放是指退出同步块的过程,释放锁的逻辑会在下一小节阐述。请读者注意本文中撤销与释放的区别。

触发时机: 释放:对应的就是synchronized方法的退出或synchronized块的结束。 撤销:笼统的说就是多个线程竞争导致不能再使用偏向模式的时候。

作者你好,这边的‘释放’之后另一个线程再次进来是偏向锁? 还是轻量锁?

shiyuhang0 commented 4 years ago

偏向锁加锁case2:"当被偏向的线程再次进入同步块时,发现锁对象偏向的就是当前线程,在通过一些额外的检查后(细节见后面的文章),会往当前线程的栈中添加一条Displaced Mark Word为空的Lock Record中,然后继续执行同步块的代码" 这里会加往栈中加Lock Record吗。我看后面的源码解析没有啊

aLibeccio commented 4 years ago

因此偏向锁的解锁很简单,仅仅将栈中的最近一条lock record的obj字段设置为null。

为什么只需要将最近的lock record设置为null 就可以了啊?

因为在偏向锁的情况下每次锁重入和第一次尝试访问临界区时创建的 Lock Record 都只有 obj 字段有值(指向锁对象),而 lock 字段(等价于 displaced header)是没有值的(为 null),即每一个 Lock Record 都是等价的,那么我只需要取最近的一个 Lock Record 释放就好了,没有必要遍历到最后取最高位那个 Lock Record 释放。

aLibeccio commented 4 years ago

因此偏向锁的解锁很简单,仅仅将栈中的最近一条lock record的obj字段设置为null。

为什么只需要将最近的lock record设置为null 就可以了啊?

因为在偏向锁的情况下每次锁重入和第一次尝试访问临界区时创建的 Lock Record 都只有 obj 字段有值(指向锁对象),而 lock 字段(等价于 displaced header)是没有值的(为 null),即每一个 Lock Record 都是等价的,那么我只需要取最近的一个 Lock Record 释放就好了,没有必要遍历到最后取最高位那个 Lock Record 释放。

只不过我真的没找到到底什么时候创建的 Lock Record

aLibeccio commented 4 years ago

因此偏向锁的解锁很简单,仅仅将栈中的最近一条lock record的obj字段设置为null。

为什么只需要将最近的lock record设置为null 就可以了啊?

因为在偏向锁的情况下每次锁重入和第一次尝试访问临界区时创建的 Lock Record 都只有 obj 字段有值(指向锁对象),而 lock 字段(等价于 displaced header)是没有值的(为 null),即每一个 Lock Record 都是等价的,那么我只需要取最近的一个 Lock Record 释放就好了,没有必要遍历到最后取最高位那个 Lock Record 释放。

重新看了一遍后发现解释得有问题,主要原因还是偏向锁每一次加锁都从低到高找最后一个空闲的 Lock Record 管理,那么锁重入的时候必定关联的 Lock Record 会在第一次加锁时关联的 Lock Record 之前(前提是不会在后面分配新的空闲的 Lock Recrod),所以锁释放的时候也从低到高取第一个 Lock Record 释放就好,满足锁重入时的对应关系,后加锁的先释放。

qiufujian commented 4 years ago

@LYoGA 你好~文中描述到:如果偏向的线程已经不存活或者不在同步块中,则将对象头的mark word改为无锁状态(unlocked),之后再升级为轻量级锁;想问下,当mark word置为无锁状态之后,下次线程获得锁的时候直接就升级成为轻量级锁了么,为什么不是还是偏向锁呢

我的理解是只有对象是可偏向状态时,才会进入偏向锁状态!所以,在JVM开启了偏向锁模式后,一个新创建的对象应该都是可偏向状态,即标志位是 1-01。如果偏向锁撤销,标志位变成了0-01,说明这个对象已经不能使用偏向锁了,此时的情形和JVM关闭偏向锁模式之后的情形相类似。

shiyuhang0 commented 4 years ago

问个小问题:lock record的存储位置是哪,是栈吗,还是哪。

107350qin commented 4 years ago

为什么图片显示不了呢?

RecordaLi commented 4 years ago

大佬,看了几篇文章,一直有个疑问,就是hashcode去哪了? 那新创建对象的mark word将是可偏向状态,此时mark word中的thread id(参见上文偏向状态下的mark word格式)为0,表示未偏向任何线程,也叫做匿名偏向(anonymously biased)。 这句我很难理解,也就是对象一创建出来就直接没hashcode吗?

当一个对象已经计算过identity hash code,它就无法进入偏向锁状态; 当一个对象当前正处于偏向锁状态,并且需要计算其identity hash code的话,则它的偏向锁会被撤销,并且锁会膨胀为轻量级锁或者重量锁;

QiangJinWang commented 4 years ago

大佬,看了几篇文章,一直有个疑问,就是hashcode去哪了? 那新创建对象的mark word将是可偏向状态,此时mark word中的thread id(参见上文偏向状态下的mark word格式)为0,表示未偏向任何线程,也叫做匿名偏向(anonymously biased)。 这句我很难理解,也就是对象一创建出来就直接没hashcode吗?

当一个对象已经计算过identity hash code,它就无法进入偏向锁状态; 当一个对象当前正处于偏向锁状态,并且需要计算其identity hash code的话,则它的偏向锁会被撤销,并且锁会膨胀为轻量级锁或者重量锁;

hashcode并不是对象实例化完就计算好的,是调用计算之后放在mark word里的。

Hunter-Chen commented 3 years ago

@gallant7 在偏向锁一文中已经说了:

这里说的撤销是指在获取偏向锁的过程因为不满足条件导致要将锁对象改为非偏向锁状态;释放是指退出同步块的过程,释放锁的逻辑会在下一小节阐述。请读者注意本文中撤销与释放的区别。

触发时机: 释放:对应的就是synchronized方法的退出或synchronized块的结束。 撤销:笼统的说就是多个线程竞争导致不能再使用偏向模式的时候。

我也一直在想,偏向锁释放时,为什么占用线程不把对象头置为无锁状态呢?而是要等下一个线程加锁时才来清除。这是基于怎样的考量呢?

释放的时候就清除的话,下一个线程进来,就不知道这个对象曾经偏向过了。 下一个线程看到这个对象曾经偏向过,就会去清除,然后升级成轻量级锁。 而不是自己变成这个对象的新的偏向锁。当有两个线程锁过一个对象的时候,就已经不适合使用偏向锁了。

FFFYYYZZZ007 commented 3 years ago

大佬,看了几篇文章,一直有个疑问,就是hashcode去哪了? 那新创建对象的mark word将是可偏向状态,此时mark word中的thread id(参见上文偏向状态下的mark word格式)为0,表示未偏向任何线程,也叫做匿名偏向(anonymously biased)。 这句我很难理解,也就是对象一创建出来就直接没hashcode吗? image

AriseFX commented 3 years ago

解锁过程的过程为什么会发生竞争,不理解

XHxin commented 3 years ago

据我所知,Java的方法调用会把当前方法打包成一个栈帧压入线程的虚拟机栈中,方法执行完之后栈帧出栈,如下图的位置 image

文章说的“在当前的线程的栈帧中创建一个Lock Record”(如下图),请问一下这个栈帧是位于上图的哪一个区域。 image

但是下图又说是当前线程栈中创建一个Lock Record,这里是否有一处是笔误呢,如果下图是正确的,当前线程栈指的是图一的线程的虚拟机栈吗。 image

第二个问题:是不是遇到Synchronized关键字就会执行_monitorenter指令? image

最后一个问题:如果只要遇到Synchronized关键字都会执行_monitorenter指令,那下图红框部分代码应该是不管偏向锁、轻量锁或重量锁应该都会执行吧,这里注释code1说“找到一个空闲的Lock Record”,那也就是说不管偏向、轻量或重量锁都会先找一下有没有空闲的Lock Record? 这块代码关于Lock Record的关系使我很疑惑,轻量锁的时候不是遇到Synchronized都会创建一个Lock Record吗。 image

binaryCodeSequence commented 3 years ago

对于 JVM 我感觉,没必要了解的这么深入,尤其是对这些锁的处理流程,

个人感觉知道个大概,,能过面试就行了,,,毕竟 Java 框架 还有那么多要学,,吃透了 Spring 全家桶,你也很厉害了。

@XHxin

XHxin commented 3 years ago

我感觉自己的记忆力比一般人差,没有真正理解的东西我一段时间之后就忘了,所以希望可以真正吃透它 @binaryCodeSequence

old-fan-kk commented 3 years ago

请问一下,对于偏向锁的加锁过程:

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

对应下一篇文章偏向锁的code5:如果偏向的线程是自己不是什么也不做吗

croatoanlulus commented 3 years ago

@gezhiwei8899 google下 很多文章

@gallant7 在偏向锁一文中已经说了:

这里说的撤销是指在获取偏向锁的过程因为不满足条件导致要将锁对象改为非偏向锁状态;释放是指退出同步块的过程,释放锁的逻辑会在下一小节阐述。请读者注意本文中撤销与释放的区别。

触发时机: 释放:对应的就是synchronized方法的退出或synchronized块的结束。 撤销:笼统的说就是多个线程竞争导致不能再使用偏向模式的时候。

我也一直在想,偏向锁释放时,为什么占用线程不把对象头置为无锁状态呢?而是要等下一个线程加锁时才来清除。这是基于怎样的考量呢?

释放的时候就清除的话,下一个线程进来,就不知道这个对象曾经偏向过了。 下一个线程看到这个对象曾经偏向过,就会去清除,然后升级成轻量级锁。 而不是自己变成这个对象的新的偏向锁。当有两个线程锁过一个对象的时候,就已经不适合使用偏向锁了。

如果释放的时候就把对象头设置成无锁状态,下一次获取锁的线程如果刚好是释放锁的线程的话就没法进行重入了,需要再通过一次CAS获取锁,这样违背了偏向锁的设计初衷。