多线程编程(9)之死磕Synchronized原理

这篇文章有点长,大家仔细点看,上篇博客已经介绍了https://blog.csdn.net/jokeMqc/article/details/115524141synchronized的原理,但是我总体看了下来感觉对于synchronized还是比较的模糊,然后本篇主要是把JVM源码下载下来看具体Synchronized的具体实现原理,这里跟大家分享一下。

目录

一、内存模型

1.1为什么要有内存模型

1.2CPU和缓存一致性

1.3处理器优化跟指令重排

1.4 并发编程问题

1.5什么是内存模型

1.6什么是JAVA内存模型

1.7Java内存模型的实现

二、Synchronized原理

2.1目标

2.2monitorenter

2.3monitorenter流程分析

2.4 小结

3.monitorexit

3.1monitorexit流程分析

3.2 同步方法

3.3小结

3.4面试题之synchronized与Lock的区别

4.深入JVM源码-monitor监视器锁

4.1目标

4.2 JVM源码下载

4.3IDE(Clion)下载

4.4monitor监视器锁

4.5monitor竞争

4.6 monitor等待

4.7monitor释放

4.8monitor是重量级锁

4.8.1内核态与用户态

4.8.2 monitor是重量级锁

4.9synchronized优化锁升级过程

4.9.1synchronized优化-对象的布局

4.9.2对象头

4.9.3 Mark Word

4.10 synchronized优化-偏向锁

4.10.1 什么是偏向锁

4.10.2 偏向锁的原理

4.10.3偏向锁的撤销

4.10.4 偏向锁的好处

4.11 synchronized优化-轻量级锁

4.11.1什么是轻量级锁

4.11.2轻量级锁原理

4.11.3轻量级锁分析

4.11.4 轻量级锁的释放

4.11.5 轻量级锁好处

4.12synchronized优化-自旋锁

4.12.1适应性自旋锁


一、内存模型

1.1为什么要有内存模型

在介绍Java内存模型之前,先来看一下到底什么是计算机内存模型,然后再来看Java内存模型在计算机内存模型的基础上做了哪些事情。要说计算机的内存模型,就要说一下一段古老的历史,看一下为什么要有内存模型。内存模型,英文名Memory Model,他是一个很老的老古董了。他是与计算机硬件有关的一个概念。那么我先给你介绍下他和硬件到底有啥关系。

1.2CPU和缓存一致性

我们应该都知道,计算机在执行程序的时候,每条指令都是在CPU中执行的,而执行的时候,又免不了要和数据打交道。而计算机上面的数据,是存放在主存当中的,也就是计算机的物理内存啦。刚开始,还相安无事的,但是随着CPU技术的发展,CPU的执行速度越来越快。而由于内存的技术并没有太大的变化,所以从内存中读取和写入数据的过程和CPU的执行速度比起来差距就会越来越大,这就导致CPU每次操作内存都要耗费很多等待时间。

所以,人们想出来了一个好的办法,就是在CPU和内存之间增加高速缓存。缓存的概念大家都知道,就是保存一份数据拷贝。他的特点是速度快,内存小,并且昂贵。那么,程序的执行过程就变成了:

当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

而随着CPU能力的不断提升,一层缓存就慢慢的无法满足要求了,就逐渐的衍生出多级缓存。按照数据读取顺序和与CPU结合的紧密程度,CPU缓存可以分为一级缓存(L1),二级缓存(L3),部分高端CPU还具有三级缓存(L3),每一级缓存中所储存的全部数据都是下一级缓存的一部分。

1.3处理器优化跟指令重排

上面提到在在CPU和主存之间增加缓存,在多线程场景下会存在缓存一致性问题。除了这种情况,还有一种硬件问题也比较重要。那就是为了使处理器内部的运算单元能够尽量的被充分利用,处理器可能会对输入代码进行乱序执行处理。这就是处理器优化。了现在很多流行的处理器会对代码进行优化乱序处理,很多编程语言的编译器也会有类似的优化,比如Java虚拟机的即时编译器(JIT)也会做指令重排

可想而知,如果任由处理器优化和编译器对指令重排的话,就可能导致各种各样的问题。

1.4 并发编程问题

前面说的和硬件有关的概念你可能听得有点蒙,还不知道他到底和软件有啥关系。但是关于并发编程的问题你应该有所了解,比如原子性问题可见性问题有序性问题

其实,原子性问题,可见性问题和有序性问题。是人们抽象定义出来的。而这个抽象的底层问题就是前面提到的缓存一致性问题、处理器优化问题和指令重排问题等。我们说,并发编程,为了保证数据的安全,需要满足以下三个特性:

  • 原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。
  • 可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
  • 有序性即程序执行的顺序按照代码的先后顺序执行。

有没有发现,缓存一致性问题其实就是可见性问题。而处理器优化是可以导致原子性问题的。指令重排即会导致有序性问题。所以,后文将不再提起硬件层面的那些概念,而是直接使用大家熟悉的原子性、可见性和有序性。

1.5什么是内存模型

前面提到的,缓存一致性问题、处理器器优化的指令重排问题是硬件的不断升级导致的。那么,有没有什么机制可以很好的解决上面的这些问题呢?

所以,为了保证并发编程中可以满足原子性、可见性及有序性。有一个重要的概念,那就是——内存模型。

为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。他解决了CPU多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性和有序性。

1.6什么是JAVA内存模型

前面介绍过了计算机内存模型,这是解决多线程场景下并发问题的一个重要规范。那么具体的实现是如何的呢,不同的编程语言,在实现上可能有所不同。我们知道,Java程序是需要运行在Java虚拟机上面的,Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。

Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。

而JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步。这里面提到的主内存和工作内存,读者可以简单的类比成计算机内存模型中的主存和缓存的概念。特别需要注意的是,主内存和工作内存与JVM内存结构中的Java堆、栈、方法区等并不是同一个层次的内存划分,无法直接类比。《深入理解Java虚拟机》中认为,如果一定要勉强对应起来的话,从变量、主内存、工作内存的定义来看,主内存主要对应于Java堆中的对象实例数据部分。工作内存则对应于虚拟机栈中的部分区域。

所以,再来总结下,JMM是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。

1.7Java内存模型的实现

了解Java多线程的朋友都知道,在Java中提供了一系列和并发处理相关的关键字,比如volatilesynchronizedfinalconcurren包等。其实这些就是Java内存模型封装了底层的实现后提供给程序员使用的一些关键字。

二、Synchronized原理

2.1目标

通过 javap 反汇编 学习 synchronized的原理。

编写一个简单的synchronized代码,如下:

package com.xxx.demo04_synchronized_monitor;

public class Demo01{
  //依赖的锁对象
  private static Object obj = new Object();

  @Override
  public void run(){
    for(int i = 0; i < 1000; i++){

      //synchronized同步代码块;且在代码块当中做了简单的打印操作;
      //重点是看synchronized在反汇编之后形成的字节码指令
      synchronized( obj ){
        System.out.println("1");
      }
    }
  }

  //编写了一个synchronized修饰的方法
  //synchronized修饰代码块与synchronized修饰方法反汇编之后的结果是不太一样的;
  public synchronized void test(){
    System.out.println("a");
  }
}

要看 synchronized的原理,但是 synchronized是一个关键字,看不到源码。可以将class文件进行反汇编。JDK自带的一个工具: javap,对字节码进行反汇编,查看字节码指令。

C:\Users\13666\IdeaProjects\Xxx\Synchronized\target\classes\com\xxx\demo04_synchronized_monitor\javap -p -v Demo01.class
# -p 是显示包括所有的私有的
# -v 就是详细的来进行显示

反汇编之后得到的字节码指令如下所示:

      LineNumberTable:
          line 3: 0
      LocalVariableTable:
          Start     Length      Slot     Name     Signature
              0          5         0     this      Lcom/xxx/demo04_synchronized_monitor/Demo01;
  
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2,  locals=3;   args_size=1
          0: getstatic           #2        // Field obj:Ljava/lang/Object;
          3: dup
          4: astore_1
          5: monitorenter
          6: getstatic           #3        // Field java/lang/System.out:Ljava/io/PrintStream;
          9: 1dc                 #4        // String 1
         11: invokevirtual       #5        // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         14: aload_1
         15: monitorexit
         16: goto                24
         19: astore_2
         20: aload_1
         21: monitorexit
         22: aload_2
         23: athrow
         24: return
      Exception table:
          from     to    target   type
             6      16       19    any
            19      22       19    any
      LineNumberTable:
        line 7: 0
        line 8: 6
        line 9: 14
        line 10: 24
      LocalVariableTable:
        Start      Length     Slot   Name    Signature
            0          25        0   args    [Ljava/lang/String;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 19
          locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
......

主要是看观看这一块的字节码指令与Demo01.java当中的代码做对比分析:

  • 首先0: getstatic 代表的是获取得到静态的成员变量Object obj的值;即private static Object obj = new Object();
  • 同步代码块开始的地方即synchronized(obj){ 对应于字节码指令当中的5: monitorenter指令;
  • 那么接着往下字节码指令当中的6: getstatic 其实对应着java代码层中的System.out.println("1");该句中的out变量;到时候进行执行其实是进行执行的println()方法;
  • 那么结束的时候就需要注意了,同步代码块synchronized结束的地方也就是synchronized(obj){ 的返回花括号}处,即对应着字节码指令当中的15: monitorexit

那么字节码指令monitorenter与monitorexit这两个字节码指令分别有什么含义呢?

2.2monitorenter

首先来看一下JVM规范中对于 monitorenter 和 monitorexit 的描述:https://docs.oracle.com/javase/specs/jvms/se8/html/index.htmlhttps://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter(Oracle官方 java虚拟机规范文档;Java虚拟机有好多种Oracle公司有,IBM公司也有,淘宝也有;java虚拟机是一套规范;该文档指的就是java虚拟机的规范)

翻译过来:

每个对象都与一个监视器关联。 且只有在拥有者的情况下,监视器才被锁定,其他线程无法来获取该monitor。 执行monitorenter的线程尝试获得与objectref关联的监视器的所有权,如下所示:

  1. 若monitor的进入数为0,线程可以进入 monitor,并将 monitor的进入数 置为1。 当前线程成为 monitor的 owner(所有者)。
  2. 若线程已拥有 monitor的所有权,允许它 重入 monitor,则进入monitor的进入数加1。
  3. 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权。

2.3monitorenter流程分析

  1. 通过刚才的描述可以知道synchronized是需要传一个对象进来也就是对象锁;即synchronized(obj)
  2. 真正的锁并非是这个传入进来的该对象obj;而是该对象obj会去关联一个叫做monitor的东西,而这个叫做monitor的东西才是真正的锁;
  3. 这个对象monitor并非是手动式使用代码进行创建的;当拿到一个对象放到同步代码块的参数当中来;即synchronized(obj)中来的时候,JVM会去进行检查该对象obj是否有进行关联monitor对象;如果JVM进行检测得到该对象obj没有进行关联monitor对象的话那么就会去创建一个与之关联的monitor对象; 而且该monitor对象还需要进行注意的是monitor并不是一个java对象;monitor而是一个C++对象;

在monitor对象当中又这么几个内容是需要注意的,monitor当中有两个比较重要的成员变量;一个是owner:指的是用有锁的线程;另一个是recursions:指的是记录获取锁的次数;

当JVM执行到monitorenter的时候;那么会找到该对象Object obj上的monitor对象看这个Object obj所关联的monitor锁对象是否被别的线程所拿走了;如果别的线程没有拿走即没有竞争走;那么当前该线程就来获取该monitor锁对象;首先先会将monitor锁对象中的成员变量owner变成当前线程;

现在假设t1线程来执行到同步代码块synchronized(obj),那么当t1线程来执行同步代码块的时候就会找到该Object obj对象所关联的monitor对象;看这个所关联的monitor有没有被其他的锁给获取得到;如果别的线程没有竞争得到该monitor锁对象,那么该锁对象monitor的owner成员变量属性就会变成当前线程t1; 那么这也是t1线程第一次进入同步代码块当中来;所以monitor其成员变量recursions(记录获取锁的次数,计数器)也会进行改变取值进行++操作;即当前由0变为1;接着t1线程就拥有了这把锁,即Object obj所关联的锁对象monitor从而进入了同步代码块synchronized(obj){}当中;在t1进入到同步代码块中时如果同步代码块当中依然存在有同步代码块,即嵌套的同步代码块时并且锁对象依然还是Object obj的话则;synchronized具有可重入特性;那么这个时候t1线程就会重入到嵌套同步代码块中去;重入的话就会将Object obj所关联的monitor锁对象的成员变量属性取值recursions计数器的取值进行++操作;即由次数1变为次数2;那么也就是说出一次同步代码块计数器recursions进行--操作即次数减一操作,由2变为1;这种类似;

另外当t1线程进入同步代码块时,并执行到输出打印语句“1”时;此时CPU切换到了t2线程上;那么t2线程同时会来进行执行Runnable当中所实现的run()方法内容;那么t2线程也会来进行竞争获取得到这把锁;即Object obj所关联的锁对象monitor;那么此时由于线程t1并没有进行释放锁操作,CPU就开始进行切换到了线程t2上来了;那么这个时候t2线程就会发现Object obj所关联的monitor锁对象当中的成员变量属性取值为并不是当前线程t2而是线程t1;那么通过这个观察也就知道当前获取锁的线程是哪一个线程了;那么此时t2线程就会进入阻塞状态;那么这个就是monitorenter的原理;

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

Synchronized 锁的管理就是依托于Monitor,当线程owner Monitor的时候则拥有进行同步操作的权利,线程进入同步块调用 monitorenter指令,退出同步块则调用monitorexit,释放对Monitor的持有。

2.4 小结

synchronized的锁对象会关联一个monitor,这个monitor不是主动进行创建的,而是JVM的线程执行到这个同步代码块时,会检查发现到对象没有monitor,那么此时就会创建monitor;monitor内部有两个重要的成员变量;owner: 拥有这把锁的线程;recursions: 会记录线程拥有锁的次数;当一个线程拥有monitor之后其他的线程只能够进行等待;

3.monitorexit

首先来看一下 JVM规范 中对于 monitorenter和monitorexit的描述:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorexit

翻译过来:

  1. 能执行 monitorexit 指令的线程 一定是 拥有当前对象的 monitor的所有权的线程。
  2. 执行 monitorexit时 会将 monitor的进入数减1。 当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。

3.1monitorexit流程分析

  1. 假设线程t1获取拿到Object obj所关联的对象锁monitor之后从而进入到同步代码块当中走到要结束同步代码块的位置,即synchronized(obj){所对应返回}花括号的位置处,即将要释放锁的位置;
  2. t1线程执行完同步代码块之后就会执行字节码指令15: monitorexit 指令;
  3. 遇到这个指令之后,就会去找到这个锁对象Object obj真正的锁对象monitor;找到之后会进行对monitor的成员变量的属性取值owner以及recursions进行更改赋值;
  4. 那么此时线程t1仍然在拥有着这把锁;所以此时monitor的owner依然是当前线程t1,不需要改变;而线程t1即将要出同步代码块,则recursions计数器就需要进行减一操作;即赋值为0,由1变为0;当monitor锁对象的成员变量recursions计数器取值变为0时也就代表着当前线程t1释放了当前其所拥有的这一把锁;那么同时monitor锁对象的成员变量owner属性取值即锁的拥有者也不存在了;

3.2 同步方法

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.11.10可以看到 同步方法 在反汇编之后,会增加ACC_SYNCHRONIZED修饰。会隐式地调用 monitorenter和monitorexit。在执行 同步方法 之前会调用monitorenter,在执行完同步方法后会调用monitorexit;

查看反汇编之后的字节码指令-同步方法:

 

----------------------------------------------------------------------
public synchronized void test();
  descriptor: ()V
  flags: ACC_PUBLIC, ACC_SYNCHRONIZED
  Code:
    stack=2,    locals=1,     args_size=1
        0: getstatic         #3        // Field java/lang/System.out:Ljava/io/PrintStream;
        3: 1dc               #6        // String a
        5: invokevirtual     #5        // Method java/io/PrintStream.println(Ljava/lang/String;)V
    LineNUmberTable:
      line 13: 0
      line 14: 0
    LocalVariableTalbe:
      Start       Length       Slot     Name     Signature
          0            9          0     this     Lcom/xxx/demo04_synchronized_monitor/Demo01;
......
----------------------------------------------------------------------
对应的代码:
public synchronized void test(){
  System.out.println("a");
}
----------------------------------------------------------------------
同步方法test()内部并没有形成monitorenter以及monitorexit;
只是对该方法做了一个标识;即观察flags:标识;
ACC_SYNCHRONIZED:那么该标识的作用即在于(在JVM规范当中也有相关说明)同步方法会被JVM隐式地调用monitorenter和monitorexit;
也就是说java虚拟机的线程执行到同步方法的时候它会自动的去调用字节码指令monitorenter;
当执行完同步方法之后也会自动的去调用字节码指令monitorexit;
即所谓隐式调用;

3.3小结

通过 javap反汇编,看到synchronized使用变成了monitorenter和monitorexit两个字节码指令;真正的锁是monitor;每个锁对象都会关联一个monitor(监视器,monitor才是真正的锁对象),monitor内部有两个重要的成员变量owner(owner:会保存获得锁的线程)和recursions(会保存线程获得锁的次数);执行monitorenter那么线程就会来进行竞争monitor这把锁;抢到monitor这把锁之后;就会将monitor当中的成员变量owner的取值改为当前抢到锁的该线程;以及拥有锁的次数recursions变为1;如果再次进入同步代码块即嵌套同步代码块也是同样的这一把锁;那么(可重入特性)重入即monitor的成员变量recursions的取值就会进行加一操作;当执行到monitorexit时,那么monitor的成员变量recursions计数器就会进行减一操作;当monitor的计数器recursions减到0时,那么当前拥有该锁monitor的现场称就会去进行释放锁;

3.4面试题之synchronized与Lock的区别

  1. ynchronized是关键字,而Lock是一个接口(ReentrantLock为其实现类)。
  2.  synchronized会自动释放锁,而Lock必须手动释放锁。
  3.  synchronized是不可中断的,而Lock可以是不可中断的也可以是可中断的。synchronized:当有一个线程执行到synchronized同步代码块当中执行代码的时候;另外一个线程由于没有锁只能够在同步代码块外侧进行等待操作,这个等待的线程是不能够被中断的,它会一直等待获取锁;而Lock有两种处理方式:可以中断也可以不中断;一种是可中断式的采用tryLock()的方式;
  4. synchronized能锁住方法和代码块,而Lock只能锁住代码块
  5. synchronized是非公平锁,ReentrantLock可以控制是否是公平锁。

4.深入JVM源码-monitor监视器锁

4.1目标

synchronized是java当中的一个关键字;通过java是看不到synchronized关键字源码的;synchronized是由JVM直接来进行支持的;现在通过JVM源码的方式来进行分析synchronized原理;JVM的源代码是使用C++编写的;那么介绍IDE工具来方便查看JVM的源代码;synchronized本质是通过monitor然后来进行同步操作的;那么会详细介绍monitor的结构;另外当存在有多个线程来执行synchronized的时候,只存在有一个线程竞争得到锁,那么这个时候就会来进行介绍monitor的竞争;以及线程没有竞争得到锁是如何处理的也就是monitor等待;以及synchronized执行完成之后线程要释放锁;那么线程是如何来进行释放的呢?这里就牵扯到了monitor释放;最后还会介绍到monitor是一个重量级锁,monitor其性能开销比较大;

带着这些问题,通过JVM源码 分析 synchronized的原理。

4.2 JVM源码下载

http://openjdk.java.net/
选中左边菜单栏中[Source Code]下的子菜单栏[Mercurial]
点击完之后得到的就是jdk所有源代码的一个托管结构;
那么在这当中找到jdk8;
找到的页面:http://hg.openjdk.java.net/jdk8
页面中就是jdk8的源码;
选择hotspot进行下载,Hotspot是jdk自带的虚拟机;
进行跳转页面:http://hg.openjdk.java.net/jdk8/hotspot
看到左侧菜单栏选择zip格式进行下载源代码;
Source Code ---> Mercurial ---> jdk8 ---> hotspot ---> zip

java 是开源的;那么java开源部分的代码就属于在openjdk这个项目当中;那么在该网站当中就能够下载得到其源代码;

jvm所有的源码都在其src目录下;
.ideea
.jcheck
agent
cmake-build-debug
make
src
.hg_archival.txt
.hgignore
.hgtags
ASSEMBLY_EXCEPTION
CMakeLists.txt
LICENSE
README
THIRD_PARTY_README
--------------------------------------
src下的目录又分成几个部分:
cpu
os
os_cpu
share

----------------------------------------
----------------------------------------

第一部分cpu;cpu跟虚拟机相关的一些代码;
其下目录有
sparc
x86
zero
----------------------------------------
第二部分os: 操作系统(也就是虚拟机在不同的操作系统当中存在有一些特别的代码)
其下目录有
bsd
linux
posix
solaris
windows
-------------------------------------------
第三部分os_cpu: 与cpu相关也与操作系统相关的一些特殊代码
其下目录有
bsd_x86
bxd_zero
linux_sparc
linux_x86
linux_zero
solaris_sparc
solaris_x86
windows_x86
-------------------------------------------------------
第四部分share: 即公共的JVM源码
其下目录有
tools //工具类
vm    //所有JVM公共的源码都在vm当中
-------------------------------------------------------
-------------------------------------------------------
vm目录下又分有一些子目录:

adlc
asm
c1
ci
classfile
code
compiler
gc_implementation
gc_interface
interpreter
libadt
memory
oops
opto
precompiled
prims
runtime
Xusage.txt

-----------------------------------------------------
-----------------------------------------------------
主要关注vm文件夹下的
oops:即面向对象即一些类的描述类的结构都存储放在该文件夹下;
runtime文件夹:主要是一些线程还有一些monitor锁都在该文件夹下。
---------------------------------------------------------

4.3IDE(Clion)下载

-----------------------------------
下载c++的ide:
https://www.jetbrains.com/
          |
          ↓
https://jetbrains.com/clion/
-----------------------------------

导入java虚拟机的源码:
File-->new Project;
导入文件夹当中的源码;
该文件夹即为当前该目录的文件夹;
.ideea
.jcheck
agent
cmake-build-debug
make
src
.hg_archival.txt
.hgignore
.hgtags
ASSEMBLY_EXCEPTION
CMakeLists.txt
LICENSE
README
THIRD_PARTY_README

4.4monitor监视器锁

(JVM底层由C++实现)可以看出 无论是 synchronized代码块 还是 synchronized方法,(最终需要一个java对象;而java对象又会关联到一个monitor监视器锁的东西;真正的同步是靠monitor监视器锁来实现的;那么monitor监视器锁的结果是什么样的?)其线程安全的语义实现 最终依赖一个叫 monitor的东西,那么这个神秘的东西是什么呢?

在HotSpot虚拟机中,monitor是由ObjectMonitor实现的。其源码是用c、c++来实现的,位于HotSpot虚拟机源码 ObjectMonitor.hpp文件中(src/share/vm/runtime/objectMonitor.hpp)。

ObjectMonitor主要数据结构如下:

-------------------------------------------------------------------
# 构造器,给很多的成员变量赋值(让其与java源代码组合起来进行分析比较方便)
ObjectMonitor() {
  _header              = NULL;
  _count               = 0;
  _waiters             = 0;
  _recursions          = 0; // 线程的重入次数
  _object              = NULL; //存储该monitor的对象
  _owner               = NULL; //标识拥有该monitor的线程
  _WaitSet             = NULL; //处于wait状态的线程,会被加入到_WaitSet
  _WaitSetLock         = 0;
  _Responsible         = NULL;
  _succ                = NULL;
  _cxq                 = NULL; // 多线程竞争锁时的单项列表
  FreeNext             = NULL;
  _EntryList           = NUll; //处于等待锁block状态的线程,会被加入到该列表
  _SpinFreq            = 0;
  _SpinClock           = 0;
  OwnerIsThread        = 0;
}
----------------------------------------------------------------------
public static void main(String[] args){
  synchronized(obj){
    System.out.println("1");

    obj.wait();
  }
}
----------------------------------------------------------------------
分析:
Java代码:synchronized(obj){ 同步代码块需要一个java对象,类对象都可以;
那么这个java对象new出来是肯定存放在java内存结构当中的堆中的;
那么对象当中存在一些什么呢?
以前知道对象当中存在有成员变量;
堆中会存放对象中的成员变量;也叫示例数据;
那么其实;除了示例数据以外还会有一个叫做对象头的内容;
那么这个对象头的作用就在于保存对应的monitor对象;
这个对象会关联一个monitor对象;那么这个monitor对象如何来的呢?
monitor时候由C++的类ObjectMonitor.hpp所造的这样一个对象;
首先在ObjectMonitor.hpp当中进行查看一些比较重要的成员变量;

_recursions;_recursions记录线程拿了几次锁了;即线程锁重入的次数;
举个例子:
假设线程第一次进入同步代码块中;获取得到锁对象;即synchronized(obj){;
那么此时锁对象所关联的monitor对象当中的成员属性中的_recursions就会记录拿了一次;
那么假设该同步代码块当中还存在了一个同步代码块即嵌套同步代码块以及同样的一把锁;
那么这个_recursions计数器就会执行加1操作;
假设该嵌套同步代码块当中还存在有一个同步代码块且依旧是同样的一把锁,
那么这个_recursions计数器就又会执行加1操作;
接着如果当前线程出一个同步代码块那么计数器_recursions就会减1;
再出一个同步代码块_recursions减1;
直到_recursions减到0为止就说明该线程就把该锁给释放掉了;

_object: 存储该monitor的对象;
即该_object会进行存储java对象即 synchronized(obj){}中的obj对象;
也就是说是相互引用的;
即java对象当中的obj会引用monitor对象;而monitor对象中的成员变量属性_object取值也会引用着java对象;

_owner: 指的是标识拥有该monitor的线程(即指的就是线程的拥有者)
假设线程A抢到锁进入到了同步代码块当中来了;
那么到时候ObjectMonitor当中的_owner所指的就是当前该线程A;

_WaitSet: 处于wait状态的线程,会被加入到_WaitSet
Wait是等待的意思;Set代表集合;
WaitSet是用来存放处于wait状态的线程的集合
比如说,当前线程A获取得到锁之后从而进入同步代码块执行同步代码块中的代码;
执行obj.wait()方法;那么当前该锁住就将进入无限的等待中;
那么无限等待的线程就会被放置在_WaitSet集合当中去;

_cxq: 多个线程在竞争锁时的单向列表;
假设当前有一个t1线程进入到同步代码块当中;
由于t1线程是第一次先进入同步代码块当中来的;
所以抢得到锁从而进入同步代码块;
假设又来了另外一个线程即t2线程;
那么t2线程是没有抢到锁的;
那么没有抢到锁的线程依旧要等待;
那么这些没有抢到锁的线程去哪里进行等待;
或者说用什么来进行保存这些正在等待的线程呢?
准备要等待的线程会进入到_cxq该变量当中去,_cxq是一个单向列表;
那么此时假如还有一个线程进来叫做t3线程;
那么这个t3线程执行同步代码块也抢不到锁,也会先进行进入这个_cxq单向列表当中;
假设线程t1往下继续进行执行;执行完同步代码块那么就会将锁进行给释放掉了;
那么此时就有可能是由线程t1、t2、t3都有可能获取抢到锁;
那么还是假设线程t1进行抢到了锁;
那么这就是线程t1第二次抢到了锁;也就是t1线程第二次进入同步代码块了;那么这个时候t1线程拿着锁进入同步代码块当中;
那么上一次放在在等待中的_cxq单向列表中的等待中的线程t2、t3此时就会进入到_EntryList变量当中去;

_EntryList: 处于等待锁block状态的线程,会被加入到该列表
即就处于BLOCK状态的线程就会被添加入到_EntryList变量当中来;
假设t1在执行的时候,这个时候又来了一个线程叫做t4;
那么t4线程进入同步代码块由于没有抢到锁会先进入_cxq单向列表当中去;
假设再来了一个线程叫做t5;那么t5也要执行同步代码块也没有获取得到锁从而也会被放置到_cxq单向列表中去;
假设t1线程执行完成出了同步代码块了;假设t1又再次争抢获取得到锁进入了同步代码块当中;那么上一次在_cxq单向列表中进行等待的两个线程t4、t5就会进入到_EntryList当中去;即变为BLOCK状态;
-----------------------------------------------------------------
分析Monitor结构图
Monitor主要由三个组件进行构成: _owner、_EntryList、_WaitSet
等待一轮之后依旧没有抢到锁的线程被放置到_EntryList当中来了;
那么还有就是执行obj.wait()方法的被放置放到_WaitSet单项列表中去;
当线程拥有者在执行那么执行完就会出同步代码块;
那么当线程拥有者在执行完成出同步代码块的时候
有可能会是_EntryList当中正在阻塞的线程竞争获取拿到锁变成monitor当中的_owner;
也有可能会是_WaitSet当中处于Wait的线程被别的操作所唤醒了;
那么它也有可能会获得锁变成线程的user即monitor结构中的_owner;

竞争到锁的线程、处于阻塞状态的线程、处于等待状态的线程;
  1. _owner: 初始化为NUll,当有线程占有该monitor时,owner标记为该线程的唯一表示。当线程释放monitor时,owner又恢复到NULL。owner是一个临界资源,JVM是通过CAS操作来保证其线程安全的。
  2. _cxq: 竞争队列,所有请求锁的线程首先会被放在这个队列中(单向链接)。_cxq是一个临界资源,JVM通过CAS原子指令来修改_cxq队列。 修改前 _cxq的旧值 填入了 node的next字段, _cxq指向新值(新线程)。因此 _cxq是一个后进先出的stack(栈)。
  3. _EntryList: _cxq队列中 有资格成为 候选资源的 线程 会被移动到该队列中。
  4. _WaitSet: 因为调用wait方法而被阻塞的 线程会被放在该队列中。

ObjectMonitor 的数据结构包含三种队列: _cxq、_WaitSet和 _EntryList,他们之间的关系转换可以用下图表示:

4.5monitor竞争

java代码举例:
----------------------------------------
public static void main(String[] args){
  synchronized(obj){
    System.out.println("1");
  }
}
----------------------------------------

假设有一个t1线程;再来一个t2线程;可能这两个线程t1、t2都会来进行执行同步代码块synchronized(obj),那么这个时候就会处于monitor竞争状态;线程执行同步代码块就会出现竞争的现象;之前反汇编javap看到synchronized会变成两条字节码指令monitorenter以及monitorexit;当线程进入同步代码块synchronized时会执行monitorenter字节码指令;线程执行完同步代码块中的内容即退出同步代码块时会执行monitorexit字节码指令;这个monitorenter字节码指令最终会调用到InterpreterRuntime.cpp代码中的一个方法;

  1. 执行 monitorenter时,会调用 InterpreterRuntime.cpp
    (位于:src/share/vm/interpreterRuntime.cpp)的InterpreterRuntime::monitorenter 函数。

具体代码可参见 HotSpot源码。

# 截取的部分代码
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime:monitorenter(JavaThread*  thread, BasicObjectLock* elem))
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
  if(PrintBiasedLockingStatistics){
    Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
  }
  Handle h_obj(thread, elem->obj());
  assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
          "must be NULL or an object");

  /**
  重点代码if(){}else{}块
  UseBiasedLocking是不是使用了偏向锁;
  这个UseBiasedLocking条件其实是JVM可以进行设置的一个启动参数;
  如果进行设置了启用偏向锁那么就会走if(){}块当中的代码;
  如果没有设置启用偏向锁那么则会走else{}块中的代码;
  也就是所要分析的monitor重量级锁的过程叫做slow_enter慢进入;
  */
  if(UseBiasedLocking){
    //Retry fast entry if bias is revoked to avoid unnecessary inflation
    ObjectSynchronizer::fast_entry(h_obj, elem->lock(), true, CHECK);
  }else{
    ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
  }
  assert(Universe::heap()->is_in_reserved_or_null(elem->obj)),
          "must be NULL or an object");
  1.  
  2. 对于重量级锁,monitorenter的函数中(slow_enter最终)会调用ObjectSynchronizer::slow_enter
  3. 最终调用 ObjectMonitor::enter(对象监视器锁monitor的enter方法,说明还是要回到ObjectMonitor.cpp)(位于:src/share/vm/runtime/objectMonitor.cpp),源码如下:
void ATTR ObjectMonitor::enter(TRAPS){
  // The following code is ordered to check the most common cases first
  // and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors.
  Thread * const Self = THREAD;
  void * cur ;

  /**
  通过 CAS 操作尝试把 monitor 的 _owner 字段设置为当前线程
  在ObjectMonitor::enter进入的时候会调用Actomic当中的cmpxchg_ptr;
  Atomic::cmpxchg_ptr(Self, &_owner, NULL)
  该函数属于linux系统内存当中的一个函数最终会依赖CPU去做原子赋值操作;
  CAS是一个原子的赋值操作;
  作用就是将monitor对象当中的_owner设置成这个当前线程Self;
  看其是否能够设置成功
  */
  cur = Atomic::cmpxchg_ptr(Self, &_owner, NULL);
  if(cur == NULL){
    // Either ASSERT _recursions == 0 or explicitly Set _recursions = 0.
    assert (_recursions == 0 ,   "invariant") ;
    assert (_owner      == Self, "invariant") ;
    // CONSIDER: set or assert OwnerIsThread == 1
    return ;
  }


  //线程重入;recursions++
  /**
    如果上一步骤当中设置monitor对象中的_owner设置成当前这个线程Self成功;
    并且当前线程Self的线程名字,即之前的_owner保存的线程就是当前线程Self的线程的名字,即名称一致的话;
    那么这样就意味着锁重入;即重新又进入了一个代码块有获取得到了同一把锁;
    那么这个时候进行monitor对象当中的_recursions变量进行++操作,即该线程的重入次数;并且return;
    说明当前线程竞争到该锁;
  */
  if(cur == Self){
    // TODO-FIXME: : check for integer overflow! BUGID 6557169
    _recursions ++;
    return ;
  }

/**
如果当前线程第一次来抢monitor该锁;
如果当前线程是第一次进入该monitor,如果抢到锁了;
设置_recursions为1,并且将_owner设置为当前线程;
最后返回即表示当前线程竞争到该锁;
*/
if(Self -> is_lock_owned((address)cur)){
  assert (_recursions == 0, "internal state error");
  _recursions = 1;
  // Commute error from a thread-specific on-stack BasicLockObject address to
  // a full-fledged "Thread *".
  _owner = Self;
  OwnerIsThread = 1;
  return ;
}

  // 省略一些代码
  /**
  那么如果经过以上操作当前线程都没有抢到锁的话;
  则就将进入到该for循环当中;
  假设第一个线程t1抢到锁进入到了同步代码块当中;
  那么第二个线程t2就由于t1线程已经抢到锁且当前时间内没有进行释放锁的缘故第二个线程没有抢到锁;
  那么抢不到锁的第二个线程t2就会执行方法EnterI(THREAD);
  最终进入到monitor对象的成员变量_cxq单向列表当中进行等待获取锁;
  */
  for (;;){
    jt->set_suspend_equivalent();
    // cleared by handle_special_suspend_equivalent_condition()
    // or java_suspend_self()

    // 如果获取锁失败,则等待锁的释放
    EnterI(THREAD);

    if(!ExitSuspendEquivalent(jt)) break;


    //
    // we have acquired the contended monitor, but while we were
    // waiting another thread suspended us. We don't want to enter
    // the monitor while suspend because that would surprise the
    // thread that suspended us.
    //

      _recursions = 0;
    _succ = NULL;
    exit(false, Self);

    jt->java_suspend_self();
  }
  Self->set_current_pending_monitor(NULL);
}

此处省略锁的自旋优化等操作,统一放在后面synchronized优化中说。以上代码的具体流程概括如下:

  1. 通过 CAS 尝试把monitor的 _owner字段设置为当前线程(即把monitor的_owner成员变量的属性取值设置为竞争的该线程;如果设置成功则说明该线程竞争到了锁)
  2. 如果设置之前的_owner指向当前线程,说明当前线程再次进入monitor,即重入锁,执行recursions++,记录重入的次数;(如果在这之前的上一次竞争当前线程获取得到了该锁,那么现在当次竞争当前线程又竞争到了该锁;两把锁一样;那么说明是锁重入;)
  3. 如果当前线程是第一次进入该monitor,设置recursions为1,_owner为当前线程,该线程成功获得锁并返回。
  4.  如果获取锁失败,则等待锁的释放(进入阻塞等待状态,即进入到monitor对象的成员变量_cxq单向列表队列);

4.6 monitor等待

当前在竞争monitor对象锁的时候会发现;有一个线程会竞争到monitor并且让线程接着往下执行;但是有一些线程竞争不到monitor那么这个时候它会执行EnterI(THREAD)这个函数;也就是说没有抢到锁的线程会进入等待处于阻塞状态;那么接下来也就是学习monitor等待过程;也就是EnterI(THREAD)方法;

竞争失败等待调用的是 ObjectMonitor对象的EnterI(THREAD)方法(位于:/src/share/vm/runtime/ObjectMonitor.cpp)源码如下所示:

# 截取部分代码分析
void ATTR ObjectMonitor::EnterI(THREAD){
  Thread * Self = THREAD;

  // Try the lock - TATAS
  if(TryLock (Self) > 0){
    assert (_succ        != Self              , "invariant");
    assert (_owner       == Self              , "invariant");
    assert (_Responsible != Self              , "invariant");
    return ;
  }

  if(TrySpin (Self) > 0){
    assert (_succ        == Self              , "invariant");
    assert (_owner       != Self              , "invariant");
    assert (_Responsible != Self              , "invariant");
    return ;
  }
}

  // 省略部分代码

  // 当前线程被封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TS_CXQ
  ObjectWaiter node(Self);
  Self->ParkEvent->reset();
  node._prev  = (ObjectWaiter *) 0xBAD;
  node.TState = ObjectWaiter::TS_CXQ;

  // 通过CAS把node节点push到_cxq列表中
  ObjectWaiter * nxt;
  for(;;){
    node._next = next = _cxq;
    if(Atomic::cmpxchg_ptr(&node, &_cxq, nxt) == nxt) break;

    // Interference - the CAS failed because _cxq changed. Just retry.
    // As an optional optimization we retry the lock.
    if(TryLock(Self) > 0){
        if(TryLock (Self) > 0){
          assert (_succ        != Self              , "invariant");
          assert (_owner       == Self              , "invariant");
          assert (_Responsible != Self              , "invariant");
          return ;
        }
     }

      // 省略部分代码
      for(;;){
        // 线程在挂起前做一下挣扎,看能不能获取到锁
        if(TryLock (Self) > 0)break;
        assert (_owner != Self, "inveriant");

        if((SyncFlags & 2) && _Reponsible == NULL){
          Atomic::cmpxchg_ptr (Self, &_Reponsible, NULL);
        }

        // park self
        if(_Responsible == Self || (SynchFlags & 1)){
          TEVENT (Inflated enter - park TIMED);
          Self->_ParkEvent->park((jlong)RecheckInterval);
          // Increase the RecheckInterval, but clamp the value.
          RecheckInterval *= 8;
          if(RecheckInterval > 1000) RecheckInterval = 1000;
        }else{
          TEVENT (Inflated enter - park UNTIMED);
          // 通过park将当前线程挂起,等待被唤醒
          Self->_ParkEvent->park();
        }

        if(TryLock(Self) > 0) break;
        // 省略部分代码
      }

      // 省略部分代码
  }



---------------------------------------------------------------
java代码:
public static void main(String[] args){
  synchronized(obj){
    System.out.println("1");
    obj.wait();
  }
}
---------------------------------------------------------------
看图说话:

假设t1线程没有竞争得到锁;则t1线程就将要去处于等待状态;
那么这个处于等待这个动作是如何做的呢?

首先这个t1线程会进入EnterI(THREAD)方法当中;
进入EnterI(THREAD)方法之后;
那么会先去进行执行TryLock(Self)尝试获取锁操作;
即t1线程虽然没有抢到锁但是它还会继续TryLock(THREAD)进行尝试一下,最后做一下挣扎;
如果在这个挣扎过程中抢到了锁则后面接着执行;
如果在这个挣扎过程中没有抢到锁那么就会继续往下走会执行函数TrySpin(Self)函数;
那么函数TrySpin(Self)函数的作用就在于自旋;
自旋即意味着进入一个循环当中多次进行抢一抢锁看能不能够抢到该锁;
即再次挣扎一下看能不能再次抢救一下;
如果经过TryLock尝试获取锁以及TrySpin自旋之后还是没有办法获取得到锁;
那么这个时候就会走到下面来;
就会将当前这个没有抢到锁且经过TryLock以及TrySpin后依然没有抢到锁的t1线程放到ObjectWaiter中来并进行封装起来;
ObjectWaiter即一个等待的线程;
并且会将当前这个没有抢到锁的线程状态设置为ObjectWaiter当中的TS_CXQ状态取值;
接着通过CAS把即将要等待的线程t1线程push到_cxq单向列表当中去;
但是可能有一个线程抢到也有其他很多线程没有抢到锁;
那么这些很多没有抢到锁的线程都要被push到这个_cxq节点上面来;
那么因此这个没有抢到锁的线程要被push到_cxq结点上去的这一操作也可能会成功也有可能会失败;
所以这一操作即没有抢到锁的线程都要被push到_cxq结点上去也是使用到了一个for循环加上Atomic::cmpxchg_ptr该内核函数;即CAS一次不行就再重试再重试;直到几个没有成功获取得到锁的线程都被挂在结点_cxq单向列表上;
另外在每次重试的时候,都会再去进行重试挣扎一下即TryLock看能不能抢到锁;
那么经过for(;;)循环之后这几个没有获取得到锁的线程都会被挂在_cxq结点单向列表上;
当没有获取得到锁的线程放到结点_cxq单向列表当中之后;那么还要将该线程进行挂起操作;即下面的代码;
在挂起的时候首先它也还是会去进行TryLock尝试挣扎抢救一下去获取锁,看能不能获取得到锁;得不到锁则继续往下走;继续往下走存在if(){}else{}块但是无论走哪一块最终都会导致当前线程执行park(),park就是把当前线程进行挂起;
那么在当前该线程被挂起之后那么该线程就不会再进行执行了,那么这个时候就只有等待别的线程来进行唤醒的时候才会进行继续执行;
那么不管是从if(){}或者是else{}块当中的点开始进行被唤醒,当该线程被唤醒之后都会去进行尝试抢锁即TryLock(Self);看是否能够获取得到锁;

当该线程被唤醒时,会从挂起的点继续执行,通过ObjectMonitor::TryLock尝试获取锁,TryLock方法实现如下:

# 截取部分代码
int ObjectMonitor::TryLock(Thread * Self){
  for(;;){
    void * own = _owner;
    if(own != null) return 0;
    if(Atomic::cmpxchg_ptr (Self, &_owner, NULL) == NULL){
      // Either guarantee _recursions == 0 or set _recursions = 0.
      assert (_recursions == 0,    "invariant");
      assert (_owner      == Self, "invariant");
      // CONSIDER: set or assert that OwnerIsThread == 1
      return 1;
    }
    // The lock had been freen momentarily, but we lost the race to the lock.
    // Interference -- the CAS faild.
    // we can either return -1 or retry.
    // Retry doesn't make as much sense because the lock was just acquired.
    if(true) return -1;
  }
}
-----------------------------------------------------------------
尝试锁也是使用的CAS操作去进行做一个判断;
如果尝试获取得到锁那么就会返回1;
如果尝试没有获取得到锁那么就会返回-1;

小结:

首先第一个;当线程没有抢到锁那么就将会被放到_cxq单向列表当中去;以上代码的具体流程如下:

  1. 当前线程被封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TS_CXQ。
  2. 在for循环中(用CAS尝试把当前该线程放到_cxq的一个节点上去;因为同时有多个线程往单向列表_cxq当中放,所以使用了for循环,CAS多次尝试),通过CAS把node节点push到_cxq列表中,同一时刻可能有多个线程把自己的node节点push到_cxq列表中
  3. (没有抢到锁的线程在放到_cxq节点上之前)node结点push到_cxq列表之后,通过自旋尝试获取锁,如果还是没有获取得到锁,则通过park将当前线程挂起(park内核函数,让当前线程进行挂起那么其实也就相当于阻塞状态需要别的线程进行唤醒才能够继续往下执行),等待被唤醒。
  4.  当该线程被唤醒时,会从挂起的点继续执行,通过ObjectWaiter::TryLock尝试获取锁。

4.7monitor释放

当某个持有锁的线程执行完同步代码块时,会进行锁的释放,给其他线程机会执行同步代码块。在HotSpot中,通过推出monitor的方式实现锁的释放,并通知被阻塞的线程,具体实现位于ObjectMonitor的exit方法中。(位于:/src/share/vm/runtime/ObjectMonitor.cpp),源码如下所示:

-------------------------------------------------------------------
# 截取部分代码
void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS){
  Thread * Self = THREAD;

  // 省略部分代码
  if(_recursions != 0){
    _recursions--;    // this is simple recursive enter
    TEVENT (Inflated exit - recursive);
    return ;
  }

  // 省略部分代码

  ObjectWaiter * w = NULL;
  int QMode = Knob_QMode;

  // qmode = 2:直接绕过EntryList队列,从_cxq队列中获取线程用于竞争锁
  if(QMode == 2 && _cxq != NULL){
    w = _cxq;
  assert ( w != NULL, "invariant");
  assert ( w->TState == ObjectWaiter::TS_CXQ, "invariant");
  ExitEpilog(Self, w);
  return ;
  }

  // qmode=3:cxq队列插入EntryList尾部;
  if(QMode == 3 && _cxq != NULL){
    w = _cxq;
    for(;;){
      assert (w != NULL, "Invariant");
      ObjectWaiter * u = (ObjectWaiter *)Atomic::cmpxchg_ptr (NULL, &_cxq, w);
      if( u == w ) break;
      w = u;
    }
    assert( w != NULL , "invariant");

    ObjectWaiter * q = NULL;
    ObjectWaiter * p;
    for( p = w ;  p != NULL ; p = p->_next){
      guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant");
      p->TState = ObjectWaiter::TS_ENTER;
      p->prev = q;
      q = p;
    }

    ObjectWaiter * Tail;
    for ( Tail = _EntryList; Tail != NULL && Tail->_next != NULL; Tail = Tail->_next);
    if(Tail == NULL){
      _EntryList = w;
    }else{
      Tail->_next = w;
      w->_prev = Tail;
    }
  }


  // qmode=4: cxq队列插入到_EntryList头部
  if(QMode == 4 && _cxq != NULL){
    w = _cxq;
    for(;;){
      assert (w != NULL, "Invariant");
      ObjectWaiter * u = (ObjectWaiter *)Atomic::cmpxchg_ptr(NULL, &_cxq, w);
      if(u == w) break;
      w = u;
    }
    assert (w != NULL , "invariant");

    ObjectWaiter * q = NULL;
    ObjectWaiter * p;
    for( p = w; p != NULL ; p ->_next){
      guarantee(p->TState == ObjectWaiter::TS_CXQ, "Invariant");
      p->TState = ObjectWaiter::TS_ENTER;
      p->_prev = q;
      q = p;
    }

    if(_EntryList != NULL){
      q->_next = _EntryList;
      _EntryList->_prev = q;
    }
    _EntryList = w;
  }

  w = _EntryList;
  if(w != NULL){
    assert (w->TState == ObjectWaiter::TS_ENTER, "invariant");
    ExitEpilog(Self, w);
    return ;
  }

  w = _cxq;
  if(w == NULL) continue;

  for(;;){
    assert (w != NULL, "Invariant");
    ObjectWaiter * u = (ObjectWaiter *)Atomic::cmpxchg_ptr(NULL, &_cxq, w);
    if(u == w) break;
    w = u;
  }
  TEVENT(Inflated exit - drain cxq into EntryList);

  assert( w          != NULL , "invariant");
  assert( _EntryList != NULL , "invariant");

  if(QMode == 1){
    // QMode == 1: drain cxq to EntryList,reversing order
    // we also reverse the order of the list
    ObjectWaiter * s = NULL;
    ObjectWaiter * t = w;
    ObjectWaiter * u = NULL;
    while(t != NULL){
      guarantee(t->TState == ObjectWaiter::TS_CXQ, "invariant");
      t->TState = ObjectWaiter::TS_ENTER;
      u = t->_next;
      t->_prev = u;
      t->_next = s;
      s = t;
      t = u;
    }
    _EntryList = s;
    assert(s != NULL, "invariant");
  }else{
    // QMode ==0 or QMode == 2
    _EntryList = w;
    ObjectWaiter * q = NULL;
    ObjectWaiter * p;
    for(p = w; p != NULL; p = p->_next){
      guarantee(p->TState == ObjectWaiter::TS_CXQ, "Invariant");
      p->TState = ObjectWaiter::TS_ENTER;
      p->_prev = q;
      q = p;
    }
  }

  if (_succ != NULL) continue;

  w = _EntryList;
  if(w != NULL){
    guarantee(w -> TState == ObjectWater::TS_ENTER, "invariant");
    ExitEpilog(Self, w);
    return ;
  }
}
-------------------------------------------------------------------
java代码:
public static void main(String[] args){
  synchronized(obj){
    System.out.println("1");
    obj.wait();
  }
}
------------------------------------------------------------------
第一部分:什么时候释放monitor分析:
获得锁的线程t1执行完同步代码块当中的代码之后,
那么就需要出同步代码块;
在出同步代码块的时候就会进行monitor的释放操作;
-----------------------------------------------------------------
第二部分:monitor释放的过程是怎么样的分析:
在exit的释放过程当中;
如果_recursions计数器不等于0;
那么_recursions就会去做一个--即减一操作;再去return;
那么这个其实对应着的是重入锁;
_recursions当不为0的情况下会进行--即减一操作;
如果_recursions等于0的情况下那么就表示线程完全出了同步代码块,
且把锁释放返回了;
那么这个时候除了释放锁之外还需要做一个操作;
即去唤醒之前正在等待阻塞中的线程;那么需要唤醒哪一个线程呢?
这个时候就需要注意了;有两个链表当中都存放有需要被唤醒的线程;
即一个是_cxq;另外一个是EntryList;
那么是随机唤醒某一个线程即有可能合适唤醒_cxq列表当中的线程也有可能是唤醒_EntryList当中的线程;

所以继续往下看;
ObjectWaiter是之前被阻塞进行等待的一个线程的一个封装;
需要记住的是w,找到了w即找到了需要被唤醒的线程;

在exit当中提供很多种模式;
模式一:
如果QMode等于2;那么这个时候会让w等于_cxq的首节点即链表头;
那么也就取到了这个要被唤醒的线程;
那么这一种方法绕过了_EntryList当中被阻塞的线程直接取_cxq列表当中的线程作为要被唤醒的线程对象;
另外方法ExitEpilog(Self,w)方法就是去作为唤醒线程的方法;
模式二:
如果QMode等于3;首先要被唤醒的线程w也是等于_cxq的首节点;
并且会将_cxq的这些节点放到_EntryList的尾部去;
模式三:
如果QMode等于4;则那么首先这个要被唤醒的线程也是等于_cxq的首节点;
并且会将_cxq列表当中的元素插入到_EntryList列表的头部;
etc...

那么最后就找到了w,即需要被唤醒的线程;
那么也会去调用到方法ExitEpilog(Self, w);
即唤醒w线程;
ExitEpilog即为具体唤醒的过程;
  1. 退出同步代码块时 会让_recursions减1,当_recursions的值减为0时,说明(线程完全退出了同步代码块中)线程释放了锁;
  2. 释放完锁之后需要唤醒线程)根据不停的策略(策略不同唤醒不同的线程)(由QMode指定),从_cxq或_EntryList中获取头节点,通过ObjectMonitor::ExitEpilog方法唤醒该节点封装的线程,唤醒操作最终由unpack完成(将之前park的线程进行唤醒),实现如下:
-----------------------------------------------------------------
# 截取部分代码
void ObjectMonitor::ExitEpilog(Thread * Self, ObjectWaiter * wakee){
  assert( _owner == Self, "invariant");

  _succ = Knob_SuccEnabled ? wakee->_thread : NULL;
  ParkEvent * Trigger = wakee->_event;

  wakee = NULL;

  // Drop the lock
  OrderAccess::release_store_ptr(&_owner, NULL);
  OrderAccess::fence();           // ST _owner vs LD in unpark()

  if(SafepointSynchronize::do_call_back()){
    TEVENT(unpack before SAFEPOINT);
  }

  DTRACE_MONITOR_PROBE(contended__exit, this, object(), Self);
  Trigger->unpark();  // 唤醒之前被park()挂起的线程

  // Maintain stats and report events to JVMTI
  if (ObjectMonitor::_synch_Parks != NULL){
    ObjectMonitor::_sync_Parks->inc();
  }
}
-----------------------------------------------------------------
分析:
最重要的代码在于Trigger-unpark();
unpark的含义即代表:
假设找到线程t1要被唤醒;
那么找到这个线程t1之后;调用unpark()方法就会将线程t1进行唤醒;
那么也就是说这个t1线程就又能够有有机会去获取竞争得到锁从而进入到同步代码块当中去;
执行完unpark之后就会将需要唤醒的线程进行唤醒;
唤醒完成之后就会进入到之前park()让该线程挂起的代码行中;
那么后面当unpark()将该需要唤醒的线程唤醒之后,就又会去进行执行TryLock尝试获取锁的代码;抢到锁则进入到同步代码块当中去;

被唤醒的线程,会回到void ATTR ObjectMonitor::EnterI(TRAPS)的第600行,继续执行monitor的竞争。

# 截取部分代码
// park self
if(_REsponsible == Self || (SynchFlags & 1)){
  TEVENT (Inflated enter - park TIMED);
  Self->_ParkEvent->park((jlong) RecheckInterval);
  // Increase the RecheckInterval , but clamp the value.
  RecheckInterval *= 8;
  if(RecheckInterval > 1000) RecheckInterval = 1000;
}else{
  TEVENT (Infalted enter - park UNTIMED);
  Self->_ParkEvent->park();
}

if(TryLock(Self) > 0) break;

4.8monitor是重量级锁

synchronized代码块在代码执行的时候效率比较低;因为synchronized所关联的锁对象monitor是一个重量级锁;

可以看到 ObjectMonitor 的函数调用中 会涉及到 Atomic::cmpxchg_ptr,Atomic::inc_ptr等内核函数,执行同步代码块,没有竞争到锁的对象会执行调用park()被挂起,竞争到锁的线程执行完成退出同步代码块时(即当其他线程退出同步代码块时)会调用unpark()唤醒上次那些没有竞争到锁从而被park()挂起的线程;(这个park和unpark也属于内核函数;即也就是说synchronized在执行的时候会涉及到大量的内核函数的执行,而内核函数的执行就会涉及到操作系统中用户态和内核态的一个切换;)这个时候就会存在操作系统 用户态和内核态的转换,这种转换会消耗大量的系统资源。

4.8.1内核态与用户态

要想了解用户态和内核态还需要先了解一下Linux系统的体系架构:

内核(操作系统的内核,而内核本质上也是一种应用程序;作用是来控制计算机的硬件资源的,比如说控制硬盘、那么还有可能控制内存等控制网络等相关的一些硬件设备比如说网卡、声卡、键盘、鼠标等);系统调用、shell、公用函数库;

应用程序(用户空间):自己写的程序被称为普通的应用程序;用户空间其实指的是自己所编写的应用程序所运行的那一块内存空间就成为用户空间;而应用程序在用户空间进行运行的时候就有可能会涉及到一些硬件资源的调用;那么这个时候就需要靠内核来进行去操作硬件资源;那么用户空间去调用内核的时候,就需要通过系统调用才能够进行;那么系统调用的作用即在于让在用户空间的应用程序能够去调用内核的一些内核函数;那么系统调用可以看做是提供内核的接口供外层的应用程序来进行调用。

从上图可以看出,linux操作系统的体系架构分为:用户空间(应用程序的活动空间)和内核。内核:本质上可以理解为一种软件,控制计算机的硬件资源,并提供上层应用程序运行的环境。用户空间:上层应用程序活动的空间。应用程序的执行必须依托于内核提供的资源,包括CPU资源、存储资源、I/O资源等。

所有进程初始都运行于用户空间,此时即为用户运行状态(简称:用户态);当应用程序需要用到键盘、需要去读取文件、需要通过网络去发送一些资源的时候那么说白了也就是需要用到计算机的一些硬件资源的时候;那么这个时候就需要通过系统调用到内核来帮助执行;普通的应用程序在用户空间当中运行那么就称之为用户态;当应用程序如果需要调用内核的一些功能,即通过系统调用来进行调用内核当中的一些功能;那么这个时候应用程序就会进入内核态;那么用户态与内核态的切换是需要系统调用来进行的;

系统调用的过程可以简单理解为:

  1. 用户态程序将一些数据值放在寄存器中,或者使用参数创建一个堆栈,以此表明需要操作系统提供的服务。
  2. 用户态程序执行系统调用。
  3. CPU切换到内核态,并跳到 位于内存指定位置 的指令。
  4. 系统调用处理器(system call handler)会读取程序放入内存的数据参数,并执行程序请求的服务。
  5. 系统调用完成后,操作系统会重置CPU为用户态并返回系统调用的结果。

4.8.2 monitor是重量级锁

在虚拟机规范对 monitorenter 和 monitorexit 的行为描述中,有两点是需要特别注意的。首先,synchronized同步块 对 同一条线程来说是可重入的,不会出现自己把自己锁死的问题。其次,同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。

4.9synchronized优化锁升级过程

在JDK1.5之前synchronized只包含有一种锁即monitor重量级锁;所以在JDK1.5之前其效率是比较低的,另外在JDK的源码当中大量的使用到了synchronized;包括java开发的时候也会经常使用到synchronized;虚拟机开发团队就意识到了这个问题;因此在JDK1.6这个版本当中对synchronized做了重要改进;在JDK1.6当中synchronized就不仅仅只有monitor这一种重量级的锁了;包括偏向锁、轻量级锁、适应性自旋、锁消除、锁优化等机制;另外到转变成重量级锁之前会有一个适应性自旋的过程进行抢救一下;这些机制的目的就是为了能够让synchronized的效率得到提升;

在JDK1.6之后不是说直接就会变成重量级锁了;而是无锁 —→ 偏向锁 —→ 轻量级锁 —→ 重量级锁

  1. 首先对象是无锁状态;然后如果需要进行加锁那么就会进行添加一个偏向锁;
  2. 如果偏向锁无法满足不行的话就会换成轻量级锁;
  3. 如果轻量级锁不行的话就有可能会进入适应性自旋的过程;
  4. 如果通过适应性自旋依然没有抢到锁则换成重量级锁;

4.9.1synchronized优化-对象的布局

JDK1.6时对synchronized做了很多的优化;锁升级过程为 无锁 —→ 偏向锁 —→ 轻量级锁 —→ 重量级锁,那么这个锁升级的过程当中就会遇到锁存在有很多的状态;那么这些锁的状态存在在哪里呢?

这些锁的状态也就存在在java对象的布局当中;对象总共由三部分组成:对象头、实例数据(成员变量等)和对齐数据;

术语参考:http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下图所示:

4.9.2对象头

synchronized锁可能有很多的状态,那么这些状态都是靠对象头来进行存储的;在Hotspot虚拟机当中对象头又分为两种;一种是普通对象的对象头即instanceOopDesc;另外一种是描述数组的对象头即arrayOopDesc;那么当前仅关心普通对象的对象头即instanceOopDesc;

Hotspot采用 instanceOopDesc和arrayOopDesc来描述对象头;arrayOopDesc对象用来描述数组类型。instanceOopDesc的定义的在Hotspot源码的 instanceOop.hpp 文件中,另外, arrayOopDesc的定义对应 arrayOop.hpp。

instanceOop.hpp记录了对象头的信息;instanceOopDesc当中没有很多代码;但是可以发现instanceOop继承了父类oopDesc;那么这个时候可以去看下其父类oopDesc中的代码;

# 截取部分代码
class instanceOopDesc : public oopDesc{
  public:
    // aligned header size.
    static int header_size(){return sizeof(instanceOopDesc)/HeapWordSize; }

    // If compressed. the offset of the fields of the instance may not be aligned.
    static int base_offset_in_bytes(){
      /* offset computation code breaks if useCompressedClassPointers
         only is true
      */
      return (UseCompressedOops && UseCompressedClassPointers) ? klass_gap_offset_in_bytes() : sizeof(instanceOopDesc);
    }

    static bool contains_field_offset(int offset, int nonstatic_field_size){
      int base_in_bytes = base_offset_in_bytes();
      return (offset >= base_in_bytes && (offset-base_in_bytes) < nonstatic_field_size = heapOopSize);
    }
};

从instanceOopDesc代码中可以看到 instanceOopDesc继承自oopDesc,oopDesc的定义在Hotspot源码中的 oop.hpp文件中;

  class oopDesc{
      friend class VMStructs;
    private :
      volatile markOop _mark;
      union _metadata{
        Klass*       _klass; # 没有开启指针压缩时的类型指针
        narrowKlass  _compressed_klass; # 开启了指针压缩
      } _metadata;
  
      // Fast access to barrier set. Must be initialized.
      static BarrierSet* _bs;

      // 省略其他代码
  };

在openjdk当中Klass*与narrowKlass有相关的介绍;

klass pointer
The second word of every object header.
Points to another object(a metaobject) which
describes the layout and behavior of the original object.
For java objects, the “Klass” contains a C++ style “vtable”.
mark word
The first word of every object header. Usually a set of bitfields including synchronization state and identity hash code. May also be a pointer (with characteristic low bit encoding) to synchronization related information. During GC, may contain GC state bits.
object header
Common structure at the beginning of every GC-managed heap object. (Every oop points to an object header.) Includes fundamental information about the heap object's layout, type, GC state, synchronization state, and identity hash code. Consists of two words. In arrays it is immediately followed by a length field. Note that both Java objects and VM-internal objects have a common object header format.

在 普通示例对象 中,oopDesc的定义包含两个成员,分别是_mark_metadata

  • _mark表示对象标记、属于markOop类型,也就是接下来要讲解的Mark Word,它记录了对象和锁有关的信息;
  • _metadata表示类元信息,类元信息存储的是对象指向它的类元数据(Klass)的首地址,其中Klass表示普通指针;

_compressed_klass表示压缩类指针。对象头由两部分组成;一部分用于存储自身的运行时数据,称之为Mark Word;另一部分是类型指针,即对象指向它的类元数据的指针。

4.9.3 Mark Word

Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,占用内存大小与虚拟机位长一致。

Mark Word对应的类型是markOop。源码位于 markOop.hpp中。

在32位状态下以及64位状态下每一位分别代表什么?其实这个就是我在其他博客看到的每个人的说法都不一样,但是在JVM源码中就已经给我们介绍了。

--------------------------------------------------------------------
// Bit-format of an object header (most significant first , big endian layout below):
//
// 32 bits:
// ---------
//           hash:25 -------------->| age:4   biased_lock:1 lock:2 (normal object)
//           JavaThread*:23 epoch:2   age:4   biased_lock:1 lock:2 (biased object)
//           size;32 ------------------------------------------->| (CMS free block)
//           PromotedObject*:29 ------------> promo_bits:3 ------>| (CMS promoted object)
//
// 64 bits:
// ----------
// unused:25 hash:31 -->| unused:1    age:4   biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1    age:4   biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 --------------------->| promo_bits:3----->| (CMS promoted object)
// size:64 ---------------------------------------------------->| (CMS free block)

# 偏向锁的时候其第三位是1;(0、1、2、3的顺序)
//   [JavaThread* | epoch | age | 1 | 01]   lock is biased toward given thread
//   [0           | epoch | age | 1 | 01]   lock is anonymously biased
//
// - the two lock bits are used to describe three states: locked/unlocked and monitor.
//
# 当第三位为0的时候则表示不是偏向锁;那么就看其后两位;00表示轻量级锁;01表示无锁;10即monitor表示重量级锁;
//   [ptr             | 00]  locked         ptr points to real header on stack           # 轻量级锁
//   [header      | 0 | 01]  unlocked       regular object header                        # 无锁
//   [ptr             | 10]  monitor        inflated lock (header is wapped out)         # 重量级锁
//   [ptr             | 11]  marked         used by markSweep to mark an object
//                                          not valid at any other time
--------------------------------------------------------------------

|---------------------------------------------------------------------------------|--------------------|
|                              Mark Word(64 bits)                                 |        State       |
|---------------------------------------------------------------------------------|--------------------|
| unused:25 | identity_hashcoder:31 | unused:1 | age:4 | biased_lock:1 | lock:2   |        Normal      |
|---------------------------------------------------------------------------------|--------------------|
| thread:54 |        epoch:2        | unused:1 | age:4 | biased_lock:1 | lock:2   |        Biased      |
|---------------------------------------------------------------------------------|--------------------|
|                   ptr_to_lock__record:62                             | lock:2   | Lightweight Locked |
|---------------------------------------------------------------------------------|--------------------|
|               ptr_to_heavyweight_monitor:62                          | lock:2   | Heavyweight Locked |
|---------------------------------------------------------------------------------|--------------------|
|                                                                      | lock:2   | Marked  for  GC    |
|---------------------------------------------------------------------------------|--------------------|


64位虚拟机下,Mark Word64bit大小的,其存储结构如下:(在不同的锁情况下每一位的作用不一样)

4.10 synchronized优化-偏向锁

4.10.1 什么是偏向锁

偏向锁是JDK6中的重要引进,因为HotSpot作者经过研究实践发现,代码)在大多数情况下,锁不仅不存在多线程竞争而且总是由同一线程多次获得(即由同一个线程反复的得到锁释放锁得到锁释放锁;如果一上来就是重量级锁的话那么得到锁就需要花费性能释放锁也需要花费性能),为了让线程获得锁的代价更低,引进了偏向锁。
减少不必要的CAS操作。

偏向锁的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是这个锁会偏向于第一个获得它的线程,会在对象头存储锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及ThreadID即可。

如果是偏向锁则倒数第三个字节会变成1;另外由前面56个字节当中的前54个字节会来保存偏向锁的id;另外56个字节当中其余两个字节用来保证Epoch即时间;

但是需要注意的是这个偏向锁仅限用于没有竞争的状态;也就是说反复是同一个线程获得锁释放锁;

示例:

package com.xxx.demo07_biased_lock;

import org.openjdk.jol.info.ClassLayout;

public class Demo01{
  public static void main(String[] args){
    MyThread mt = new MyThread();
    mt.start();
  }
}

class MyThread extends Thread{
  static Object obj = new Object();

  @Override
  public void run(){
    for( int i = 0; i < 5; i++){
      synchronized(obj){
        // ...
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
      }
    }
  }
}
// 循环5次并且进出同步代码块就只有一个线程;
// 那么这种情况就适合使用偏向锁;
// 即反复是同一个线程进入同步代码块的情况;
// 但是如果遇到有线程来进行竞争那么即立即要撤销掉偏向锁从而升级到轻量级锁;

4.10.2 偏向锁的原理

当线程第一次访问同步代码块并获取锁时,偏向锁处理流程如下:

  1. 检测Mark Word是否为 可偏向状态,即是否为偏向锁1,锁标识为为01。
  2. 若为 可偏向状态,则测试线程ID是否为当前线程ID,如果是,执行同步代码块,否则执行步骤(3)
  3. 如果测试线程ID不为当前线程ID,则通过CAS操作将Mark Word的线程ID替换为当前线程,执行同步代码块

首先虚拟机会进行检查这个偏向锁的状态即倒数第三位取值是否为0,是0的话那么也就代表着可以进行偏向,将倒数第三位修改成1表示偏向锁;那么虚拟机也就会将其倒数两位数改成01;即变成偏向锁;那么还会通过CAS操作将对象的Mark Word当中的前54位修改为当前获取得到偏向锁的线程的Thread ID;那么到此时这个偏向锁就设置成功了;

那么以后的话线程退出同步代码块的时候并不需要做任何操作而下一次循环当中重新进入同步代码块时,只需要进行判定一下THREAD ID即Mark Word当中的前54位当中的取值即THREAD ID与当前该要获取锁的线程的THREAD ID是否相同;如果是一样的话那么则进入同步代码块当中;无需其他操作;所以偏向锁它在一个线程的情况下其效率还是很高的;

这面的例子大家首先需要引入jol这个jar。

下面这个例子大家可以发现其他的的位数是倒着来显示的,这是为什么?其实这个里面有一个大端的显示方法,在下面的例子中也说了。

package com.xxx.demo07_biased_lock;

import org.openjdk.jol.info.ClassLayout;

public class Demo01{
  public static void main(String[] args){
    MyThread mt = new MyThread();
    mt.start();
  }
}

class MyThread extends Thread{
  static Object obj = new Object();

  @Override
  public void run(){
    for( int i = 0; i < 5; i++){
      synchronized(obj){
        // ...
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
      }
    }
  }
}
/**
线程进过5次循环每一次循环都进入到同步代码块当中;
对象锁为obj;此时只有一个线程按道理应该是使用的偏向锁;
另外在同步代码块当中进行打印的对象的对象布局信息;以此用来检查是否存在偏向锁的标记;
*/
------------------------------------------------------------
打印结果:
com.lang.Object object internals:
 OFFSET   SIZE   TYPE  DESCRIPTION         VALUE
      0      4        (object header)     d8 ed 44 29 (11011000 11101101 01000100 00101001) (692383192)
      4      4        (object header)     00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8      4        (object header)     e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12      4    int   LockObj.x           0
 Instance size: 16 bytes
 Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
------------------------------------------------------------
分析:
0      4        (object header)     d8 ed 44 29 (11011000 11101101 01000100 00101001) (692383192)
当中的
d8 ed 44 29 (11011000 11101101 01000100 00101001) (692383192)
当中的
d8 11011000 即最终的第8个字节即
    8       7        6        5        4         3        2       1
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
其实就是
    1
00000000
显示到前面去了即
    1       2        3        4        5         6        7       8
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
这个样子;
即d8 11011000代表的就是是否是偏向锁,即锁的标志位;
那么d8其8位当中的最后三位即000;那么000是属于偏向锁吗;不是;
那么为什么不是偏向锁呢?原因在于:偏向锁虽然在jdk1.6的时候偏向锁是开启的;
但是这个默认开启的偏向锁并不是立马可以进行使用的;
所以这个时候又需要添加一个JVM参数即在VM options处填入参数:-XX:BiasedLockingStartupDelay=0;
让其原始值为0即程序一启动那么偏向锁就生效;
那么再次运行尝试:
-------------------------------------------------------------------
再次运行打印结果:
com.lang.Object object internals:
 OFFSET   SIZE   TYPE  DESCRIPTION         VALUE
      0      4        (object header)     05 90 61 27 (00000101 10010000 01100001 00100111) (660705285)
      4      4        (object header)     00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8      4        (object header)     e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12      4    int   LockObj.x           0
 Instance size: 16 bytes
 Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
-----------------------------------------------------------------
分析:
程序首先由主线程从main方法开始进行执行并在main方法当中创建一个新线程叫做mt并启动它;用t1表示新线程mt;
那么新线程t1就会执行到run方法当中去进行5次循环并在每次循环当中进入同步代码块且打印其对象布局中对象头等对象布局信息;
此时只有一个线程没有其他线程来进行竞争操作;
那么此时t1第一次来进行执行同步代码块synchronized的时候就会去看这个对象头obj当中它的锁标记是什么;
那么一开始的时候该对象头是属于无锁状态即倒数第二位为0(表示不为偏向锁状态;即无锁状态);
那么这个时候由于要将无锁状态改为偏向锁状态那么此时就会将倒数第三位改成1(即偏向锁状态;表示转成偏向锁状态);
另外前56位当中的前54位会被用来保存这个偏向锁的id;以及前56位当中的后两位用来进行保存Epoch即相关时间;
那么这个时候就让t1线程进入了同步代码块当中(其实也就表示设置好了偏向锁,要进入同步代码块当中进行执行;然后进行打印对象布局信息中即101;
也就是0      4        (object header)     05 90 61 27 (00000101 10010000 01100001 00100111) (660705285)中的05 00000101其8位的倒数三位101;也就表示转变成了偏向锁状态);
当执行完成打印对象布局信息即也就是完成同步代码块中的任务要出同步代码块时,退出同步代码块时这个偏向锁就并不用做什么事情;
那么当下一次循环时(总共有5次循环),该线程t1又要从同步代码块当中来进行获取锁;
它就会发现对象锁obj当中即为一个偏向锁的状态;
并且只要去进行对比一下这个线程Id即前56位当中的54位的值是否取值相等即THREAD id是否与当前这个要获取锁的线程的THREAD id相同;
如果是,那么就直接进入到同步代码块当中;
即有了偏向锁之后,那么后面的循环中当前该线程再次去获取锁就变得很简单了只需要(对比一下THREAD ID是否一致即可)两个动作
(1: 检查倒数第三位是否是偏向锁标志,如果是则执行第二个动作;如果不是则将倒数第三位的数值改为1即表示偏向锁;2: 首先检查前56位当中的前54位的THREAD ID是否有取值;如果没有取值则设置为当前的该线程ID;如果存在取值THREAD ID则将当前要获取锁的线程进行与前54位中的THREAD ID进行比较,如果相同则直接可以进入到同步代码块当中);
退出循环之后每次再次进行循环需要进入同步代码块时之前对比一下THREAD ID是否一致即可;
所以偏向锁可以看到在一个线程执行同步代码块的情况下其效率是非常高的(较之重量级锁要高很多);

4.10.3偏向锁的撤销

一旦存在有两个线程来进行锁的时候那么就会撤销偏向锁;

实例:

package com.xxx.demo07_biased_lock;

import org.openjdk.jol.info.ClassLayout;

public class Demo01{
  public static void main(String[] args){
    MyThread mt = new MyThread();
    mt.start();

    MyThread mt2 = new MyThread();
    mt2.start();
  }
}

class MyThread extends Thread{
  static Object obj = new Object();

  @Override
  public void run(){
    for( int i = 0; i < 5; i++){
      synchronized(obj){
        // ...
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
      }
    }
  }
}
/**
假设在这当中存在有两个线程;
也就意味着有两个线程会去进行run()当中的synchronized;
那么两个线程来进行执行的时候就需要将该偏向锁给进行撤销;
*/

偏向锁撤销过程:

  1. 偏向锁的撤销动作必须等待全局安全点
  2. 暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态
  3. 撤销偏向锁,恢复到无锁(标志位为01)或轻量级锁(标志位为00)的状态;

TIPS: 全局安全点
假设当前需要进行统计超市在9点的时候超市当中有多少人;
那么应该如何统计?
即在9点的时候,让在超市当中的所有人都停下来;不要有人进入超市也不要有人出超市;那么这个时候才来进行统计;
那么这个线程安全点指的就是在这个点的时候所有的线程都会进行停下来;那么也就叫做全局安全点;那么只有到了全局安全点的时候才能来进行撤销偏向锁;

偏向锁 在 java1.6 之后是默认启用的;但是在应用程序启动几秒钟才激活,可以使用-XX:BiasedLockingStartupDelay=0参数关闭延迟,如果确定应用程序所有锁通常情况下处于竞争状态,可以通过-XX:-UseBiasedLocking=false参数关闭偏向锁。

4.10.4 偏向锁的好处

偏向锁适合使用在只有一个线程来获取锁的时候进行使用;即没有竞争情况;那么在这种情况下一个线程反复进入同步代码块退出同步代码块的效率是很高的;只要进行判断对象头当中的线程id即THREAD ID跟现在要获取锁的线程的THREAD ID是否相同即可;如果相同则进入同步代码块;退出同步代码块时也不需要做什么事情;所以性能高;

但是这个偏向锁不一定总是好的;如果存在有很多的线程来竞争锁;那么这个时候偏向锁就起不到什么作用了反而会影响效率;因为每次撤销一次偏向锁都必须要等待全局安全点所有线程都会停下来才能够进行撤销偏向锁,所以反而还会影响性能;比如说使用线程池来执行代码的时候,那么这个时候知道线程池当中肯定有多个线程反复去执行同样的任务,同样的代码即反复的去竞争同一把锁;那么在这个时候偏向锁就是多余的了;

偏向锁是在 只有一个线程执行同步块时 进一步提高性能,适用于一个线程反复获得同一把锁的情况。偏向锁可以提高带有同步但无竞争的程序性能。

4.11 synchronized优化-轻量级锁

4.11.1什么是轻量级锁

当偏向锁出现竞争的时候,会撤下偏向锁从而升级到轻量级锁;轻量级锁是JDK1.6当中为了优化synchronized而引入的一种新型锁机制;需要注意的是轻量级锁不是任何情况下其开销比较的小而是在特定的情况下才开销比较小;所以轻量级锁并不能够用来代替重量级锁;轻量级锁只是在一定的情况下来进行减少消耗;

轻量级锁是 JDK1.6之中加入的 新型锁机制,它名字中的“轻量级”是相对于使用monitor的传统锁而言的,因此传统的锁机制就称为“重量级”锁。首先需要强调的一点是,轻量级锁并不是用来代替重量级锁的;

引入轻量级锁的目的: 在多线程交替执行同步块的情况下,引入轻量级锁)尽量避免重量级锁引起的性能消耗,但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁的出现并非是要代替重量级锁。(也就是说在多线程交替执行同步块的时候轻量级的性能才是比较好的)

4.11.2轻量级锁原理

当关闭偏向锁功能 或者 多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,其获取锁步骤如下:

 1.判断当前对象 是否处于无锁状态(hashcode、0、01);

如果是,则JVM首先将在当前线程的栈帧中建立一个所记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word),将对象的Mark Word赋值到栈帧中的Lock Record中,将Lock Record的owner指向当前对象。

2.JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针;如果成功表示竞争到锁,则将锁标志位变成00,执行同步操作。

3.如果失败则判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象

4.11.3轻量级锁分析

java代码:
class MyThread extends Thread{
  static Object obj = new Object();

  @Override
  public void run(){
    for( int i = 0; i < 5; i++){
      synchronized(obj){
        // ...
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
      }
    }
  }
}
-----------------------------------------------------------
分析:
synchronized当中的obj即为锁对象(对象锁)
当前具有两个线程,分别是A线程以及B线程
(创建这两个线程并启动他们,那么他们都会将去执行run()方法当中的代码);
可以看到的是进入synchronized同步代码块之前需要一个obj对象锁;
假设线程A来先进行执行run()方法;
即,即将执行的run()方法运行就会进入到栈中去,那么进入到栈中执行的方法即一个栈帧,即此时运行的run()方法即为一个栈帧;
假设此时线程A要进入到同步代码块之中;那么这个时候就要升级为轻量级锁;因为存在有多个锁竞争;所以未使用偏向锁;
那么此时如何进行升级为轻量级锁?
首先在栈中运行执行run()方法的该栈帧当中会创建一个叫做Lock Record的锁记录空间(这块空间内存放displaced hdr以及owner);
那么接着就会将锁对象即obj升级为轻量级锁;
那么其当前obj锁对象的状态为无锁状态;
即会将对象当中的无锁状态当中的hashCode、分代年龄以及锁标记赋值到栈帧当中Lock record中创建的displaced hdr当中;
另外还会将栈帧当中的Lock record当中还会进行创建的owner指向obj,即也就是synchronized的锁对象(对象锁);
另外还需要做的事情有升级为轻量级锁的时候会将锁标志位的数值修改成“00”;
并且存在前25bit+31bit+1bit+4bit+1bit来进行保存栈帧中Lock Record的地址;
那么这些操作都是通过CAS来进行操作的;
这就是轻量级锁的原理;
笔记小结-轻量级锁的原理:
当关闭了偏向锁或者说偏向锁出现了竞争的情况那么都会导致偏向锁升级为轻量级锁;
轻量级锁步骤如下:
首先判断对象头当中的倒数两位即标志位是否为01,01即代表无锁状态;
如果确实无锁;那么JVM就会在当前的栈帧当中建立一个Lock Record这样一个空间;
这块空间用来存储对象头;即用来存储displaced hdr(displaced hdr即Displaced Mark Word)以及owner;
displaced hdr也就是指的Displaced Mark Word(会将分代年龄、锁标志、锁标志等放到displaced hdr当中去);
并且owner会执行当前的对象即synchronized当中的obj;
接着还会使用CAS操作把对象头当中的Mark Word进行保存栈帧中创建的Lock record的地址;
最后会将对象头当中的标志位改成00即代表的是轻量级锁;
如果在这个当中升级轻量级锁失败那么就会膨胀为重量级锁;

4.11.4 轻量级锁的释放

轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:

  1. 取出在获取轻量级锁 保存在Displaced Mark Word中的数据;
  2. 用CAS操作 将取出的数据 替换当前对象的Mark Word中,如果成功,则说明释放锁成功
  3. 如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要将轻量级锁需要膨胀升级为重量级锁。

对于轻量级锁,其性能提升的依据是“对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”,如果打破这个依据则除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢。

轻量级锁的释放即将栈帧当中的Lock Record当中的displaced hdr当中的hashCode重新放回到对象头原有的位置上即无锁状态上31bit处;以及displaced hdr当中的分代年龄以及锁标志位等都放回原位;撤销轻量级锁也是一个CAS操作;即如果将hashCode、分代年龄以及锁标志位都还原归位了那么也就说明轻量级锁已经被撤销了;

需要注意的是,对于轻量级锁而言:轻量级锁的性能之所以高,是因为在绝大部分情况下,这个同步代码块不存在有竞争的状况;线程之间交替执行;如果是多线程同时来进行竞争这个锁的话,那么这个轻量级锁的开销也就会更大;

4.11.5 轻量级锁好处

在多线程交替执行同步块的情况下,可以避免重量级锁引起的性能消耗。

4.12synchronized优化-自旋锁

当轻量级锁发生竞争的时候会膨胀升级为重量级锁;但是重量级锁相对于性能开销的消耗是比较大的;因此应该尽量避免升级为重量级锁;所以JVM在轻量级锁在升级为重量级锁的时候的这个过程当中还会再挣扎一下;尽量避免升级为重量级锁;这个挣扎的过程就是自旋锁;

重量级锁回顾:重量级锁是由monitor来进行实现的;当一个线程来进行竞争monitor锁如果没有竞争到那么线程就会进入阻塞状态;当其他线程将锁释放的时候会来进行唤醒那些处于阻塞状态的线程有机会去竞争锁;

自旋锁JDK 1.4.2中就已经引入,只不过默认是关闭的,自旋锁JDK 1.4.2中就已经引入,只不过默认是关闭的,可以使用-XX:+UseSpinning参数来开启,在JDK 6中就已经改为默认开启了。自旋等待不能代替阻塞,且先不说处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的;因此,如果锁被占用的时间很短,自旋等待的效果就会非常好;反之,如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费;因此,自旋等待的时间必须要有一定的限度.自旋次数的默认值是10次,用户可以使用参数-XX:PreBlockSpin来更改;

4.12.1适应性自旋锁

在JDK 6中引入了 自适应的自旋锁。自适应 意味着 自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机 就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100次循环。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序搜的状况预测就会越来越准确,虚拟机就会变得越来越“聪明”了。

好了,相信到这里大家对于synchronized的底层原理跟实现有了大体的了解,如果不熟悉大家再仔细学习本博客然后结合JVM源码来分析就大体的清晰了。

猜你喜欢

转载自blog.csdn.net/jokeMqc/article/details/115654540