java架构之路(多线程)synchronized详解以及锁的膨胀升级过程

  上几次博客,大发幸运飞艇大发幸运飞艇我 们 把volatile基本都说完了,剩下的还有大发幸运飞艇大发幸运飞艇我 们 的synchronized,还有大发幸运飞艇大发幸运飞艇我 们 的AQS,这次博客大发幸运飞艇我 来说一下synchronized的使用和原理。

  synchronized是jvm内部的一把隐式锁,一切的加锁和解锁过程是由jvm虚拟机来控制的,不需要大发幸运飞艇大发幸运飞艇我 们 认为的干预,大发幸运飞艇大发幸运飞艇我 们 大致从了解锁,到synchronized的使用,到锁的膨胀升级过程三个角度来说一下synchronized。

锁的分类

  java中大发幸运飞艇大发幸运飞艇我 们 听到很多的锁,什么显示锁,隐式锁,公平锁,重入锁等等,下面大发幸运飞艇我 来总结一张图来供大家学习使用。

 这次博客大发幸运飞艇大发幸运飞艇我 们 主要来说大发幸运飞艇大发幸运飞艇我 们 的隐示锁,就是大发幸运飞艇大发幸运飞艇我 们 的无锁到重量级锁。

synchronized的使用

  大发幸运飞艇大发幸运飞艇我 们 先来看一段简单的代码

public class SynchronizedTest {

    private static Object object = new Object();
    
    public static void main(String[] args) {
        synchronized (object){
            System.out.println("只有大发幸运飞艇我
拿到锁啦");
        }
    }
}

  就这样synchronized就可以使用了,这样是每次去拿全局对象的object去锁住后续的代码段。大发幸运飞艇大发幸运飞艇我 们 来看一下汇编指令码

 public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field object:Ljava/lang/Object;
       3: dup
       4: astore_1
       5: monitorenter
       6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       9: ldc           #4                  // String 只有大发幸运飞艇我
拿到锁啦
      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

  明显看到了两个很重要的大发幸运飞艇方法 monitorenter和monitorexit两个大发幸运飞艇方法 ,也就是说大发幸运飞艇大发幸运飞艇我 们 的synchronized大发幸运飞艇方法 加锁是基于monitorenter加锁和monitorexit解锁来操作的

  大发幸运飞艇大发幸运飞艇我 们 得知是由monitorenter来控制加锁和monitorexit解锁的,大发幸运飞艇大发幸运飞艇我 们 完全可以这样来操作。上次大发幸运飞艇大发幸运飞艇我 们 说过一个unsafe类。

public class SynchronizedTest {

    private static Object obj = new Object();

    public void lockMethod(){
        UnsafeInstance.reflectGetUnsafe().monitorEnter(obj);
    }
    
    public void unLockMethod(){
        UnsafeInstance.reflectGetUnsafe().monitorExit(obj);
    }
}

  就是大发幸运飞艇大发幸运飞艇我 们 上次说的unsafe那个类给大发幸运飞艇大发幸运飞艇我 们 提供了加锁和解锁的大发幸运飞艇方法 ,这样就是实现夸大发幸运飞艇方法 的加锁和解锁了,但是超级不建议这样的使用,后面的AQS回去说别的方式。越过虚拟机直接操作底层的,大发幸运飞艇大发幸运飞艇我 们 一般是不建议这样来做的。

  大发幸运飞艇大发幸运飞艇我 们 还可以将synchronized锁放置在大发幸运飞艇方法 上。例如

public class SynchronizedTest {

    private static Object object = new Object();

    public static synchronized void lockMethod() {
        System.out.println("只有大发幸运飞艇我
拿到锁啦");
    }
}

  这样加锁是加在了this当前类对象上的。如果不加static,锁是加在类对象上的,需要注意大发幸运飞艇大发幸运飞艇我 们 用的spring的bean作用域

  并且大发幸运飞艇大发幸运飞艇我 们 的synchronized是一个可重入锁,在jvm源码中有一个数值来记录加锁和解锁的次数,所以大发幸运飞艇大发幸运飞艇我 们 是可以多次套用synchronized的

public void lockMethod(){
    synchronized(obj){
        synchronized(obj){
            System.out.println("大发幸运飞艇我
没报错");
        }
    }
}

synchronized到底锁了什么

  还是拿上个每次加锁的时候会在对象头内记录大发幸运飞艇大发幸运飞艇我 们 的加锁信息,大发幸运飞艇大发幸运飞艇我 们 这里来说一下对象头里面都放置了什么吧。

以32位JVM内部存储结构为例

锁状态

25 bit

4bit

1bit

2bit

锁标志位

是否是偏向锁

23bit

2bit

GC标记

11

重量级锁

指向重量级锁Monitor的指针(依赖Mutex操作系统的互斥)

 

10

轻量级锁

指向线程栈中锁记录的指针

pointer to Lock Record

00

偏向锁

线程ID

Epoch

对象分代年龄

1

01

无锁

对象的hashCode

对象分代年龄

0

01

  由此看出对象一直是有一个位置来记录大发幸运飞艇大发幸运飞艇我 们 的锁信息的。说到这大发幸运飞艇大发幸运飞艇我 们 就可以来看一下大发幸运飞艇大发幸运飞艇我 们 锁的膨胀升级过程了。

锁的膨胀升级

  大发幸运飞艇大发幸运飞艇我 们 说过了对象头的内容,接下来可以说说大发幸运飞艇大发幸运飞艇我 们 的锁内部是如何升级上锁的了。从无锁到重量级锁的一个升级过程,大发幸运飞艇大发幸运飞艇我 们 来边画图,边详细看一下。

  无锁状态:

   开始时应该这样的,线程A和线程B要去争抢锁对象,但还未开始争抢,锁对象的对象头是无锁的状态也就是25bit位存的hashCode,4bit位存的对象的分代年龄,1bit位记录是否为偏向锁,2bit位记录状态,优先看最后2bit位,是01,所以说大发幸运飞艇大发幸运飞艇我 们 的对象可能无锁或者偏向锁状态的,继续前移一个位置,有1bit专门记录是否为偏向锁的,1代表是偏向锁,0代表无锁,刚刚开始的时候一定是一个无锁的状态,这个不需要多做解释,系统不同内部bit位存的东西可能有略微差异,但关键信息是一致的。

  偏向锁:

  这时线程开始占有锁对象,比如线程A得到了锁对象。 

 就会变成这样的,线程A拿到锁对象,将大发幸运飞艇大发幸运飞艇我 们 的偏向锁标志位改为1,并且将原有的hashCode的位置变为23bit位存放线程A的线程ID(用CAS算法得到的线程A的ID),2bit位存epoch,偏向锁是永远不会被释放的。

  接下来,线程B也开始运行,线程B也希望得到这把锁啊,于是线程B会检查23bit位存的是不是自己的线程ID,因为被线程A已经持有了,一定锁的23bit位一定不是线程B的线程ID了

   然后线程B也会不甘示弱啊,会尝试修改一次23bit位的对象头存储,如果说这时恰好线程A释放了锁,可以修改成功,然后线程B就可以持有该偏向锁了。如果修改失败,开始升级锁。自己无法修改,线程B只能找“大哥”了,线程B会通知虚拟机撤销偏向锁,然后虚拟机会撤销偏向锁,并告知线程A到达安全点进行等待。线程A到达了安全点,会再次判断线程是否已经退出了同步块,如果退出了,将23bit位置空,这时锁不需要升级,线程B可以直接进行使用了,还是将23bit的null改为线程B的线程ID就可以了。

   轻量级锁:如果线程B没有拿到锁,大发幸运飞艇大发幸运飞艇我 们 就会升级到轻量级锁,首先会在线程A和线程B都开辟一块LockRecord空间,然后把锁对象复制一份到自己的LockRecord空间下,并且开辟一块owner空间留作执行锁使用,并且锁对象的前30bit位合并,等待线程A和线程B来修改指向自己的线程,假如线程A修改成功,则锁对象头的前30bit位会存线程A的LockRecord的内存大发幸运飞艇地址 ,并且线程A的owner也会存一份锁对象的内存大发幸运飞艇地址 ,形成一个双向指向的形式。而线程B修改失败,则进入一个自旋状态,就是持续来修改锁对象。

   重量级锁:如果说线程B多次自旋以后还是迟迟没有拿到锁,他会继续上告,告知虚拟机,大发幸运飞艇我 多次自旋还是没有拿到锁,这时大发幸运飞艇大发幸运飞艇我 们 的线程B会由用户态切换到内核态,申请一个互斥量,并且将锁对象的前30bit指向大发幸运飞艇大发幸运飞艇我 们 的互斥量大发幸运飞艇地址 ,并且进入睡眠状态,然后大发幸运飞艇大发幸运飞艇我 们 的线程A继续运行知道完成时,当线程A想要释放锁资源时,发现原来锁的前30bit位并不是指向自己了,这时线程A释放锁,并且去唤醒那些处于睡眠状态的线程,锁升级到重量级锁。

逃逸分析

  很简单的一个问题,实例对象存在哪里?到底是堆还是栈?问题大发幸运飞艇我 先不回答,大发幸运飞艇大发幸运飞艇我 们 先看一段代码。

public class Test {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("开始");
        for (int i = 0; i < 500000; i++) {
            createCar();
        }
        System.out.println("结束");
        Thread.sleep(10000000);
    }


    private static void createCar() {
        Car car = new Car();
    }
}

就是大发幸运飞艇大发幸运飞艇我 们 运行一个创建对象的大发幸运飞艇方法 ,一次性创建50万个Car对象,然后大发幸运飞艇大发幸运飞艇我 们 让大发幸运飞艇大发幸运飞艇我 们 的线程进行深度的睡眠,两个打印是为了知道大发幸运飞艇大发幸运飞艇我 们 的对象已经开始创建了和已经创建完成了。大发幸运飞艇大发幸运飞艇我 们 来运行一下。

 然后运行jmap -histo命令来查看大发幸运飞艇大发幸运飞艇我 们 的线程

   大发幸运飞艇大发幸运飞艇我 们 可以看到,car对象并没有产生50万个,别说会被GC掉对象,在运行之前大发幸运飞艇我 已经加了GC日志的参数-XX:+PrintGCDetails,控制台没有打印任何GC日志的。那么为什么会这样呢?大发幸运飞艇大发幸运飞艇我 们 来看一下大发幸运飞艇大发幸运飞艇我 们 的代码,由createCar代码创建了car对象,但car对象并没有被其它的大发幸运飞艇方法 或者线程去调用,虚拟机会认为大发幸运飞艇你 这对象可能只是一个实例化,并没有进行使用,这时虚拟机会给予大发幸运飞艇你 一个大发幸运飞艇优化 ,就是对于可能没有使用的对象进行一次逃逸,也就是大发幸运飞艇大发幸运飞艇我 们 说到的逃逸分析。大发幸运飞艇大发幸运飞艇我 们 加入 -XX:­DoEscapeAnalysis参数再看一次。

   这也就是关闭了大发幸运飞艇大发幸运飞艇我 们 的逃逸分析,虚拟机就会真的为大发幸运飞艇大发幸运飞艇我 们 创建了50万个对象。也就是说开启了逃逸分析有一部分对象只是创建了线程栈上,当线程栈结束,对象也被销毁,上面的问题也就有答案了,实例对象可能存在堆上,也可能存在栈上。

posted @ 2020-01-14 15:10  小菜大发幸运飞艇技术   阅读(690)  评论(4编辑  收藏