奈何没文化,捋捋JVM锁状态一丝皮毛

锁是什么?

线程获取的锁到底是什么东西呢?每个对象都有的锁其实是一个用c++写的Monitor对象,每个Java对象有且只有一个Monitor对象。Monitor的数据结构如下:

 ObjectMonitor() {
    ......
    // 用来锁的进入次数,为0说明该锁还没有被获取
    _count        = 0;
     // 锁的重入次数
    _recursions   = 0;
    // 指向持有ObjectMonitor对象的线程
    _owner        = NULL;
    // 存放处于wait状态的线程队列
    _WaitSet      = NULL;
    // 存放处于等待锁block状态的线程队列
    _EntryList    = NULL ;
    ......
  }

当线程获得了锁,即将_Owner属性修改为自己,把_cuont属性+1;释放锁即把_Owner属性改为null,把_count属性-1(CAS保证该操作原子性);

其他线程当然也能访问这个Monitor对象,但是当发现_count不为0的时候,就说明这把锁已经被占有了,就阻塞。

锁状态类型

  • 无锁状态
  • 偏向锁
  • 轻量级锁
  • 重量级锁

对象头

普通对象的对象头由两个字宽(在32位系统中一个字宽为4个字节,即32bit,64为则为64bit)组成,一个字宽为Mark Word(重点),一个字宽为Class Metedata Area(存储指向对象类型数据的指针),如果是数组,还会多一个字宽,用于记录数组的长度。

下面是32位系统的Mark Word:

img

可以看到,不同的锁状态对应对象头Mark Word的不同状态,所以锁状态应该是对象私有的。

Synchronized底层

每个对象都天生可以成为一个锁,synchronized加在不同位置,充当锁的对象就不同,有三种情况:

  • 加在静态方法上,此时充当锁的对象是这个方法所属类的Class对象
  • 加载非静态方法上,此时充当锁的对象就是调用这个方法的实例对象
  • 同步块,此时充当锁的对象就是括号里的对象

如下:

class Main{
    
    private Object lock = new Object();
    
    static synchronized void A(){
        System.out.println("这里作为锁的对象是Main的Class对象");
    }
    
    synchronized void B(){
        /*
        例如Main m = new Main();
        m.B();所以这里作为锁的对象是m这个实例对象
         */
        System.out.println("这里作为锁的对象是调用B()的实例对象");
        synchronized (lock){
            System.out.println("这里作为锁的对象就是这个Object对象lock");
        }
    }
}

JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,具体细节不太一样。代码块同步是使用monitorenter和monitorexit指令实现的,方法同步使用ACC_SYNCHRONIZED来实现,但基本原理都差不多。

monitorenter指令是在编译后插入到同步代码块开始的位置,而monitorexit是插入到代码块结束和异常处,JVM保证每个monitorenter和monitorexit相匹配。任何对象都有一个monitor与之关联,当一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时将会尝试获取对象对应的monitor的所有权,即尝试获取对象的锁(将_count设为1且将_Owner设为自己)。

注意,只有重量级锁状态下才需要进入Monitor对象(即JVM才会使用monitorenter指令和monitorexit指令)。

锁升级与对比

虽然synchronized是重量级锁,但并不是进入同步方法就直接将锁状态变为重量级锁,而是从轻量级锁开始一直膨胀升级。这样是为了避免大量的CAS加锁解锁造成的系统开销。

锁状态个人理解是一种机制,并不是具体的锁类型,即开启了相应的锁状态JVM就会按照相应的机制去调度线程获取锁的过程,当然也可以关闭JVM的一些锁状态,比如关闭偏向锁状态,那么锁升级过程就会变成:无锁状态-轻量级锁-重量级锁。锁状态是从低到高升级的,不可降级。

由无锁状态-重量级锁状态的升级过程如下。

偏向锁(JDK1.6之后JVM默认开启)

单线程情况下对象头处于无锁状态,而一旦有线程进入了synchronized修饰的同步方法或者同步块,就会使用偏向锁机制。因为很多情况下不会出现锁竞争的情况,即一直都是同一个线程去获取锁。这种情况下执行同步块里面的代码就没必要做同步了。

偏向锁就是基于这种情况考虑,默认认为不会出现锁竞争的情况,就采取很少的控制手段来保证程序的效率。下面介绍偏向锁的获取锁和释放锁的过程。

偏向锁的获取

每个进入同步方法或者同步代码块的线程,都会在自己的虚拟机栈中创建一个栈帧,栈帧中会开辟出一个空间叫做锁记录(只有同步方法或者包含同步代码块的方法的栈帧中才会创建)。

当线程去尝试获取偏向锁成功后,就在对象头锁记录中存储自己的线程ID(CAS),以后该线程进出同步块就可以不用CAS去加锁解锁,只要测试一下对象头里的偏向锁ID是否是自己就行了。

偏向锁的撤销

只有当出现锁竞争的时候,偏向锁才释放锁。当有其他线程用CAS尝试去获取锁,当前线程就知道出现了锁竞争,就等到JVM的全局安全点(此时无正在执行的字节码),然后暂停线程,将锁记录中的线程ID设为null,然后修改对象头的锁状态为轻量级锁(即锁升级),然后再恢复线程。

轻量级锁

轻量级锁加锁

在轻量级锁机制下,线程获取锁时会将对象头的Mark Word复制一份到锁记录中,称为Displaced Mark Word。然后线程尝试使用CAS去将对象头里面的Mark Word修改为指向锁记录的指针,如果成功就代表获取到了锁,如果失败就说明有其它线程竞争锁,就通过自旋来尝试获取锁。如果自旋获取锁失败就会导致锁升级为重量级锁,并且该线程阻塞。

轻量级锁解锁

线程会尝试使用CAS将锁记录中的Displaced Mark Word去替换对象头中的Mark Word,如果成功就代表释放了锁;如果失败就说明出现了锁竞争(可能是其他线程在自旋获取锁),锁就膨胀为重量级锁,等到竞争锁的线程因自旋获取锁失败阻塞后,轻量级锁就可以完成解锁,然后会唤醒阻塞等待的线程。

重量级锁加锁解锁即上面介绍过的synchronized底层

引申出自旋锁

可以看到轻量级锁中,在修改对象头的Mark Word时如果失败,线程会自旋(即做死循环去尝试获取锁,获取到锁或者达到相应的自旋次数才退出循环),这个时候就是自旋锁,即自旋锁是一种状态,用于描述线程不断自旋去尝试获取锁的状态。

为什么自旋?

因为当线程获取不到锁时需要阻塞,锁释放时线程又会恢复运行继续去竞争锁,这里线程就被挂起和恢复了一次,需要不小的系统开销。而大多数情况下锁被占用的时间很短,所以就可以让锁自旋一段时间来等待锁的释放,如果在自旋期间获取到了锁,那就省去了挂起恢复的开销。直到自旋达到一定次数还无法获取到锁才将线程真正挂起。

自适应自旋锁
自旋锁到底自旋多少次合适呢?因为自旋也是要占用CPU时间的,自旋太长时间不好,太短又等不到锁,由此出现了自适应自旋锁。

自适应自旋锁会根据这个线程之前的自旋情况来自动调整自旋次数。如果之前这个线程自旋有很高的概率获得锁,那么下一次线程会允许多自旋几次;反之就减少线程的自旋次数,甚至可能直接阻塞挂起。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值