33

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

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=Mzg3MjA4MTExMw%3D%3D&%3Bmid=2247492336&%3Bidx=1&%3Bsn=1a65c789557c0929b7520b2992746f94
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

来自: 博客园(作者:小菜技术)

原文链接 (底部链接可直达):

https://www.cnblogs.com/cxiaocai/p/12189848.html

前言

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

锁的分类

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

640?wx_fmt=png

这次博客我们主要来说我们的隐示锁,就是我们的无锁到重量级锁。

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解锁来操作的

640?wx_fmt=png

我们得知是由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内部存储结构为例

640?wx_fmt=png

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

锁的膨胀升级

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

无锁状态:

640?wx_fmt=png

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

偏向锁:

这时线程开始占有锁对象,比如线程A得到了锁对象。 640?wx_fmt=png

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

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

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

640?wx_fmt=png

轻量级锁:

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

640?wx_fmt=png

重量级锁:

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

640?wx_fmt=png

逃逸分析

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

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对象,然后我们让我们的线程进行深度的睡眠,两个打印是为了知道我们的对象已经开始创建了和已经创建完成了。我们来运行一下。

640?wx_fmt=png

然后运行jmap -histo命令来查看我们的线程

640?wx_fmt=png

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

640?wx_fmt=png

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

感谢大家的阅读,不正确的地方,还希望大家来斧正,我们一起探讨技术问题,觉得写得好的,给个推荐,给个赞,点个关注吧,鞠躬,谢谢:pray:。

喜欢我可以给我设为星标哦

640?wx_fmt=jpeg

640?wx_fmt=jpeg

好文章,我 “在看”


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK