java锁的膨胀过程-synchronized(2)

引言(从入门到放弃)

如果大家有看过我写的第一篇博客,那么就请继续追随着我的狂风绝息。

实例-轻量锁

我抛出几个问题,大家先思考。

  1. 轻量锁如何证明他的存在?
  2. 偏向锁跟轻量锁有关系吗?
  3. 如果有关系,过程又是怎样的?

先来一波代码:
在这里插入图片描述

public class Rocket {
    public static void main(String[] args) {
        James james = new James();
        //我已经关闭偏向锁延迟,所以是可偏向状态,上一博客提到过
        out.println(ClassLayout.parseInstance(james).toPrintable());
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                sync(james);
            }, "davis" + i).start();
            //确保第一个线程已经释放锁资源
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private static void sync(James james) {
        synchronized (james) {
            out.println(Thread.currentThread().getName());
            out.println(ClassLayout.parseInstance(james).toPrintable());
        }
    }
}

代码我解释一下,开启两个线程,分别调用sync()方法,加了10s睡眠,保证两线程顺序执行,主要是保证第一个线程在第二线程获取锁之前释放锁资源,main线程打印毋庸置疑,大家考虑一下两个线程分别在同步块中打印什么?给你们两只小白兔的时间。
在这里插入图片描述

两个线程同一对象mark word有明显的区别,大家心里有好多疑问,好似也说不出什么678,但是都能看到偏向状态和对象状态 000,是不是出现另样了,继无锁和偏向锁又一状态,咱也别管他是轻量级还是重量级,跟我往下走。
如果我在上述main方法最后加入一行代码:

out.println(ClassLayout.parseInstance(james).toPrintable());

再次执行,我们只看最后一行代码打印结果:
在这里插入图片描述
怎么样,是不是又回到了一开始我们研究的无锁状态,突然想起一首歌
“又回到最初的起点
记忆中你青涩的脸
我们终于 来到了这一天
桌垫下的老照片”
ok,大家肯定对无锁状态已经了如指掌,做一个总结:首先大家要明白一点,就上头代码来说,无论打印多少次,是不是都只共用了一个对象,说明什么问题,对象头中无锁偏向状态变化成为了偏向锁状态,偏向锁状态可以变化成为 00 状态, 00 状态又可以变化成为无锁状态,重点来了,无锁状态还能不能变化成为偏向锁状态,给大家思考三只奶兔顺序执行的时间。

大家还记不记得上一篇说到,我们可以通过改变jvm参数关闭偏向延迟,如果不加同步块对象状态是无锁可偏向,如果加了同步块就是偏向锁,那么问题来了,上段代码运行jvm参数已经修改过了,就现在而言,如何让无锁状态变化成为无锁可偏向状态或者偏向状态,大家考虑,偏向状态是不是不可逆的,现在不做结论。问题:如果不改变jvm运行参数,打开偏向延迟,不加同步块必然会是无锁状态,但是加了同步块,之前的代码执行结果会是怎样的?

告警:我这提所提及的状态可变化是为了方便大家理解,当太极练成之时就是忘记这里之日,变化的行为涉及锁的膨胀,以及锁的撤销,是存在许多复杂的过程。

public class Clippers {
    public static void main(String[] args) {
        James james = new James();
        out.println("if lock");
        synchronized (james) {
            out.println(ClassLayout.parseInstance(james).toPrintable());
        }
    }
}

看执行结果:
在这里插入图片描述
在这里插入图片描述

针对 000 我们通过主图(上一篇博客)可知,轻量锁不存在偏向状态,mark word中bit域为 62bit(ptr_to_lock_record)+00,ptr_to_lock_record代表指向栈中锁记录的指针。

ptr_to_lock_record: 每个线程都有一个独立的栈,栈存放着我们的栈帧,在栈帧当中,当我们锁要升级为轻量锁,会创建一个lock_record,lock_record存在两部分,一部分是mark word,在执行同步的时候,如果判断是一个偏向锁,那么会首先置为无锁,并将markword复制到lock_redord当中,然后CAS成功为轻量锁,另一部分为OBJECT指针,指向了当前实例对象。如果执行同步发现为无锁状态,会直接将mark word复制到lock_record,所以当前实例对象的对象头中62bit(ptr_to_lock_record)指向lock_record当中的markword中首地址

这里我先啰嗦一部分,有助于小伙伴们往下看。大家有没有赶脚,在一个main线程下,偏向锁和轻量锁没什么区别,只不过修改了一下jvm运行参数,关闭或者开启偏向锁延迟,然后状态码就会不一样。大家请带着这种感觉,迎接现实的蹂躏!

在单线程情况下,我们写一段简单代码,就偏向和轻量对比一下,代码不绝对,大家可以自己脑洞。

//指定pojo
public class Curry {
    
    private int num;

    public synchronized void calculate() {
        num++;
    }
}

Jazz关闭了偏向锁延迟,所以代表偏向锁。

public class Jazz {
    public static void main(String[] args) {
        Curry curry = new Curry();
        long start = System.currentTimeMillis();
        for (long i = 0; i < 1000000000l; i++) {
            curry.calculate();
        }

        long end = System.currentTimeMillis();
        System.out.println(String.format("%sms",end-start));
    }
}

结果:3922ms
就Jazz代码,如果我开启偏向延迟,也就是轻量锁状态下运行。
结果:34599ms
总结:一台亚索飞起四年的笔记本,一套偏向锁代码执行完毕仅需要两个狂风绝息斩的时间,而一套轻量锁却需要一段狂风绝息斩的冷却时间,性能孰高孰低,大家自有分辨了吧。

CAS(CompareAndSwap)

无论是偏向锁,轻量锁,还是重量锁,都有各自的应用场景。
大家可以回看,我举的大部分例子都是基于单线程环境下(跟main线程有关),或者多线程交替执行(两种情况:1. 第一个线程死亡。2. 第一个线程没有死亡),也就是不存在资源争夺(互斥)的情况下,我们继续解决问题,那么请看以下代码:

public class Clippers {
    public static void main(String[] args) {
        /**
         * 已经关闭偏向锁延迟
         */
        James james = new James();
        out.println("if lock");
        new Thread(() -> {
            synchronized (james) {
                //偏向锁
                out.println(ClassLayout.parseInstance(james).toPrintable());
            }
        }, "irving").start();

        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //偏向锁
        out.println(ClassLayout.parseInstance(james).toPrintable());
        
        synchronized (james) {
            //轻量锁
            out.println(ClassLayout.parseInstance(james).toPrintable());
        }
        out.println(ClassLayout.parseInstance(james).toPrintable());//无锁
    }
}

这里有四次打印,我们顺序执行。
①当我们关闭偏向延迟,首先对象(也就是james)起初会有一个无锁可偏向状态,所以,irving线程执行开始进入同步块,判断是偏向状态且不存在线程ID,并将当前线程ID写入mark word。
在这里插入图片描述
②休眠10s确保irving线程释放锁,第二次打印。大家有什么想法,没有同步,没有资源争夺,猜测mark word并没有做什么改动,是不是跟第一次打印信息相同呢?
在这里插入图片描述
大家自行对比,一模一样的对象头信息。

③第三次的时候,进入同步块,此时是不是由main线程执行,ok,大家考虑一下,此时对象头信息很明显线程ID对应irving,作为main线程,内部应该怎么做?

Just do it:所谓CAS就是底层CPU一套原子操作指令。举个例子,个人理解(不准确欢迎指出),i=1,作为CPU执行可能就是一个赋值操作。但是i++,需要取i,参与+1运算,然后赋值给i。
CAS操作可大致分为三种状态,Old,Expactation(预期),New。
CAS过程:拿对象mark word值也就是Old与Expactation(预期值)作比较,所谓预期值就是无锁状态mark word,并且判断是否相同,不同无法写入线程ID,CAS失败,开始偏向锁撤销,并置为无锁状态,再进行CAS,成功,进而膨胀成为轻量锁。所以偏向锁升级为轻量锁,必然会存在一个无锁中间状态,否则无法CAS成功。

在这里插入图片描述
简单总结:james(对象)被main线程所持有,但是对象中的mark word是偏向irving线程ID,所以无法进行加锁,只好撤销偏向锁,置为一种无锁状态,重新写入main线程mark word,升级为轻量锁。
④第四次打印正好可以验证了一点,偏向锁是偏向某一线程,该线程退出同步仍然为偏向锁,当锁在被另一个线程所持有之后,就不存在偏向状态,所以第四次打印是无锁。
在这里插入图片描述

重量锁

对于重量锁,我们先不去了解他的概念,上一段代码看看香不香:

public class Blazers {

    public static void main(String[] args) {
        James james = new James();
        out.println("whether to compete for lock");
        new Thread(()->{
            synchronized (james){
                out.println(ClassLayout.parseInstance(james).toPrintable());
            }
        }, "love").start();

        synchronized (james){
            out.println(ClassLayout.parseInstance(james).toPrintable());
        }
    }
}

大家请看打印结果:
在这里插入图片描述
两次打印对象状态都是 10,也就是重量锁,并且对象头也相同,为什么,他又是如何膨胀为重量锁的?
在这里插入图片描述
在这里我将两线程执行画了一幅图,用执行流程来代替晦涩难懂的语言效果会好一点,并不权威,我也希望未来能成为权威,还望大家能批评指正。
在这里插入图片描述
就重量级锁,我给大家证明一个问题:

public static void main(String[] args) throws InterruptedException {
        James james = new James();
        out.println("before lock");
        //无锁
        out.println("1 "+ClassLayout.parseInstance(james).toPrintable());

        Thread t1=new Thread(()->{
            synchronized (james) {
                out.println("before wait");
                //轻量
                out.println("2 "+ClassLayout.parseInstance(james).toPrintable());
                try {
                    james.wait();
                     out.println("after wait");
                    //重量
                    out.println("3 "+ClassLayout.parseInstance(james).toPrintable());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            
        },"anthony");
        t1.start();

        TimeUnit.SECONDS.sleep(5);
        //重量
        out.println("4 "+ClassLayout.parseInstance(james).toPrintable());

        synchronized (james) {
            out.println("notifyAll");
            //重量
            out.println("5 "+ClassLayout.parseInstance(james).toPrintable());
            james.notifyAll();
        }
        TimeUnit.SECONDS.sleep(5);
        //无锁
        out.println("6 "+ClassLayout.parseInstance(james).toPrintable());
    }

解释一下:打开偏向延迟,打印语句我已经标了序号,第一次睡眠5s确保线程t1已经进入wait等待状态,第二次睡眠确保t1线程释放锁资源,大家可以先考虑一下:
按照上代码序号,我把打印结果截取出来:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

清楚地看到,在未进入同步之前,对象状态是无锁状态(编号1),毋庸置疑,当t1线程首先进入同步代码当中就会升级为轻量锁(编号2),然后进入等待状态,主线程睡眠5s确保t1线程进入等待后,打印对象头信息,为重量锁(编号4),为什么,主线程进入同步块,也是重量锁(编号5),唤醒等待线程,t1又获取到锁资源开始执行,打印为重量锁(编号3)。
小结论:对象调用wait方法之后,可以直接升级为重量锁。原因是什么?
在这里插入图片描述
理论:在openjdk源码当中,所有争夺这把锁的线程都会进入WaitSet当中,并没有权利去争夺CPU资源,当调用notify的时候,会从WaitSet取出一个线程放入EntryList当中,EntryList等待CPU分配资源,才可执行。
notifyAll和notify的区别:调用notifyAll,所谓唤醒所有,就是将WaitSet中所有线程取出放入EntryList,具体执行哪一个线程,不知道,由CPU分配调度。

针对重量锁的性能我们可以跟轻量锁对比一下,上代码(代码不唯一):

public class Curry {

    private int threePoint;

    public synchronized void downtown() {
        threePoint++;
        GoldenState.countDownLatch.countDown();
    }
}
public class GoldenState {
    //同时只能允许一个线程操作
    static CountDownLatch countDownLatch = new CountDownLatch(1000000000);
    public static void main(String[] args) throws InterruptedException {
        final Curry curry = new Curry();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 2; i++) {

            new Thread() {
                @Override
                public void run() {
                    while (countDownLatch.getCount() > 0) {
                        curry.downtown();
                    }
                }
            }.start();
        }
        //等待完活
        countDownLatch.await();
        long end = System.currentTimeMillis();
        System.out.println(String.format("%sms", end - start));
    }
}

代码解析:两个线程互相争夺锁资源,所以一直重量锁状态执行。
结果:52135ms
结论:比较轻量级锁的性能对比代码,虽然代码有出入,但是结果也足以证明重量锁的效率是是这三种锁效率最低的。

synchronized,在JDK1.6之前只有一种语义,就是重量锁,在1.6之后做了相当多的优化,偏向和轻量的概念只是一部分,还包括批量重偏向,批量撤销等。

多线程东西很多,需要证明和解决的问题也很多,知识就像浩瀚的海洋,让你心向往却总是望不到边际,所以,希望就是此时此刻,扬帆起航。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值