JVM synchronized关键字锁的基本原理(hotspot)

首先祭出上图,上图是线程获取锁和锁升级的基本流程

但是其中还有几个点没有列举清楚,旁边标着有数字标号

问题如下:

1.CAS操作怎么失败的?

考虑这样一种情景,我们假设线程1是第一个获得此锁对象的线程,一开始线程1进入同步代码块,检查到是无锁态,因此Compare null,Set 他自己的threadID,成功设置,当然可能也设置不成功,可能有其他线程在竞争锁,但是CAS操作一定会有一个成功,而其他将会失败。我们现在就假设线程1成功了,它获取锁就乐呵乐呵的执行同步代码块的代码了,然后线程2来了,检查锁状态,发现是偏向锁,看看是不是自己的thread ID,发现不是,CAS一下(注意这里的CAS操作底层是一个原子操作,是比较耗性能的),此时Compare的是null(一个指定的关键字,并不一定是java中的null,可能是无锁态的mark word,也可能是偏向锁的mark word,但其thread ID为空),Set的也是线程2自己的threadID,但是此时mark word的thread ID是线程1的,因此不管是线程2还是线程3等等,都将失败,而线程1在进入将会成功。这是必须这样设置,保证让已经获得偏向锁的线程,再次进入一定成功,其他线程总是失败

2.CAS失败了,线程1所持有的偏向锁要升级,那此时线程2在干什么,JVM又是怎么判断线程1已经退出同步代码块了?

此时线程2将再次检查锁状态,发现是偏向锁,然后失败,然后再失败,再检查锁状态,循环往复。注意,在这失败了是重新检查锁状态,就好像再重新进一边同步代码块一样,而不是直接CAS,这样做的目的是,在线程2做这种循环失败的过程中,可能还会有线程3,线程4等等其他线程激烈竞争,极有导致锁连续升级,最终膨胀为重量级锁,而线程2是无法感知到这一切的,他只能不断的检查锁状态,并作出相应的动作,比如线程1升级完了,此时线程2检查到所状态是轻量级锁了,将CAS对应轻量级锁的内容,并且失败了将会自旋等等

JVM怎么判断线程1退出同步代码块的呢,我们知道,对于偏向锁,线程不会主动释放,因此只凭mark word是无法得知的,必须借助其他内容,但又必须保证能在潜逃进入同步代码块的情况下一样使用,因此必须借助其他的值进行判断,且该值必须线程共享,比如类似进入同步代码块值+1,退出同步代码块-1,判断该值是否为0等,这取决于具体虚拟机实现了,没翻过hotspot源码,乱猜一下,但是JVM一定能够判断某线程是否退出了同步代码块,且判断方式是与持有什么锁无关的。此时JVM检测到线程1已死或者已退出同步代码块,将修改对象头的mark word,将其修改至适合1中需要的样子(要么恢复到无锁态,要么将偏向锁状态的thread ID置为null)

3 这个啥子操作

这个操作只是后续可能的步骤哦,总会有个时间段要执行原来的线程1的代码

扫描二维码关注公众号,回复: 4651562 查看本文章

4什么是安全点 safe-point

call baidu

5.这个位置为什么会CAS失败,线程都被堵在获取锁位置了,为什么释放锁还会失败

这个问题需要先回答轻量级锁是怎么升级为重量级锁的,依然是线程1获得锁,线程2想要获得锁,线程2不断自旋,CAS失败后,膨胀为重量级锁,此时线程2再竞争,将会调用操作系统调用,进入等待队列里面。(注:等待队列,阻塞队列是相对于重量级锁而言的)。此时对线程1而言,不会有任何的变化,此时它内部栈帧仍然持有的是轻量级锁的锁记录,它嵌套进入同步代码块总能成功,因为他坚信自己已经获得了锁,不会重新走一遍获得锁的过程,因此他不需要改变自己的栈帧的锁记录,直到他退出同步代码块(哪怕是嵌套的方法调用,也一定会有锁的退出,此时他将意识到,原来的锁对象的对象头的mark word已将改变成了重量级锁对应的对象头的mark word),他还以为是轻量级锁呢,因此CAS会失败,此时他将会释放原来的轻量级锁(这一步和CAS成功是一样的)。注意,此分析对嵌套调用同样适用。

另外 关于对象头的mark word更换之后,如何获取对象的hashcode等问题,请移步

https://know.baidu.com/wenda/question/info?qid=94d9adf70f6c9f5cb94904291bd4a76c2ef76ea  

垃圾回收的处理和上面大致的思路

ps:在加锁之后,如何处理一个锁对象 锁不同的代码块,但是各个不同的同步代码块之间是可以非同步的,多个线程访问同样的同步代码块需要同步;

对于此类问题,按理说应该是多线程理解的不是很透彻,当sync代码块或者方法时,被该锁对象监视的所有代码块,同步方法,都将进入同步,如:

public class Main{
    public Main(){ }

    public static void main(String[] args) throws InterruptedException {
        Main main=new Main();
        Main mutex=new Main();
        Thread thread1=new Thread(){
            @Override
            public void run(){
                synchronized (mutex){
                    main.sleep();
                }
            }
        };
        Thread thread2=new Thread(){
            @Override
            public void run(){
                synchronized (mutex){
                    main.play();
                }
            }
        };
        thread1.start();
        Thread.sleep(100);
        thread2.start();
    }
    public  void sleep()  {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public  void play(){
        System.out.println("play");
    }
}

play必须等待sleep执行完成后才能执行(个中道理,请考虑当对象创建完成后存储在堆中,对象大小就确定了,不会在变了,因此不会出现再向对象头追加mark word的情况)没办法,锁实现就是这样。当然也看到了,想要实现上述多个线程进入同一个代码块需要同步,而进入不同的代码块不需要同步的问题,请用不同的锁对象处理代码块,而对于同步方法,无法解决。或者将同步方法修改为同步代码块或者撤销同步方法,需要同步时使用信号量完成。

对象头信息:

请注意 无锁态和偏向锁的锁标志位是一样的,一定程度上可以认为偏向锁不是真正意义上的锁。

辛勤成果,多多鼓励。

猜你喜欢

转载自blog.csdn.net/WK_SDU/article/details/84310856
今日推荐