一、Monitor

在介绍Mark Word的时候,提到过Monitor,可以称为监视器或管程,下面我们看下它到底是个什么东西。

每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针。不加 synchronized 的对象不会关联Monitor。

首先通过下图展示管程的组成,以及和java对象的关系:

Monitor管程 (2).png

  • 有一个java对象,上了一把synchronized(重量级锁),当有线程获取到锁时,其对象头中的Mark Word变成了指向Monitor的指针。(原本mark word当中的内容会存储到Monitor当中,释放时会取出这些内容再次放到mark word。)

  • thread3 来竞争这把锁,此时只有它自己,那么thread3将会被设置为Monitor的Owner,有且只能有一个Owner。

  • 如果thread3持有锁的过程中,如果thread4和thread5也来竞争锁,就会添加到EntryList当中,此时线程将被阻塞(BLOCKED)。

  • 当thread执行完同步代码块当中的内容,会唤醒EntryList当中的线程来竞争锁,此竞争是非公平的。

  • 另外,在WaitSet当中的thread1和thread2,其状态是WAITING,表示他们之前获得过锁,但是条件不满足,此处不讲解,后面在分析wait,notify时讲解。

二、synchronized原理分析

2.1 synchronized字节码分析

在上面了解Monitor后,进入java当中的重点,synchronized的学习。

有如下代码:

   static final Object object = new Object();
static int i = 0;

public static void main(String[] args) {
synchronized (object) {
i++;
}
}

2.2 轻量级锁

如开篇展示的对象Mark Word,共有5种锁的状态, 我们本小节讲解其中之一,轻量级锁相关的内容。

轻量级锁是指在满足一定的条件内,使用CAS(自旋)来尝试获取对象锁的一种机制,如果超过以下条件,则会膨胀为重量级锁:

1)在jdk1.6前,默认10次,可通过-XX:PreBlockSpin来修改,或者自旋线程数超过CPU核数的一半。

2)jdk1.6之后,引入了自适应自旋锁,次数并非一成不变。根据获取锁的成功率来决定是否能有更长的等待时间。

轻量级锁仍然是使用synchronized,用户其实是无感知的。

2.2.1 轻量级锁的上锁和释放锁

假设当前有一把对象锁lock,两个方法使用同一把锁,且add当中会调用sub()方法,如下所示:

   static final Object lock = new Object();

public void add(){
synchronized (lock){
sub();
}
}

public void sub(){
synchronized (lock){

}
}
  • 当线程尝试获取这把锁的时候,会创建锁记录(Lock Record),每个线程的栈帧(线程执行到的方法,都会生成对应的栈帧),都会包含一个锁记录的结构,可以用来存储对象的Mark Word,如下所示:

轻量级锁 (2).png

  • 让锁记录中 Object Reference 指向锁对象,并尝试用 CAS 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录(如图上的1),并将锁记录的地址存入Object的Mark Word(如图上的2),如下所示:

轻量级锁 (1).png

  • 如果CAS成功,对象的Mark Word将会存储Lock Record 地址 和 锁状态 00,如下图所示:

轻量级锁 (3).png

  • 如果CAS失败,此时会有两种情况:

    • 如果是其他线程已经持有了该轻量级锁,则表示发生竞争,此时进入锁膨胀。

    • 如果是自己执行了 synchronized 锁重入(就像我们示例代码中的add()方法),那么再添加一条 Lock Record 作为重入的计数,只不过新添加的Lock Record中没有Object的Mark word内容,为null。如下所示:

轻量级锁 (4).png

  • 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一,将null的Lock Record删除。

  • 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 CAS 将 Mark Word 的值恢复给对象头。

    • 成功,则解锁成功。

    • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。

当线程使用CAS尝试对对象加锁,有两种失败情况,一种情况是自己执行了synchronized锁重入;另外一种,就是本文需要学习的重点内容:锁膨胀

一、何为锁膨胀?

假设当前Object对象,已经被Thread1所持有,当Thread2前来竞争这把锁,满足上述条件后,会发生锁膨胀,如下图所示:

锁膨胀 (3).png

如上所示,此时Thread2来获取轻量级锁肯定失败的,所以会进入锁膨胀的流程:

1)为Object对象申请Monitor锁,Object的Mark Word指向Monitor地址;Monitor的Owner指向Thread1的锁记录。

2)Thread2进入Monitor的EntryList当中,状态变成BLOCKED。

锁膨胀 (2).png

当Thread1执行完代码块的内容后,开始释放锁,使用CAS去重置Object的Mark Word,此时会失败。因为当前对象头存储的是Monitor的地址。

所示此时会进入重量级锁的解锁过程。将Monitor的Owner设置为null,同时唤醒EntryList中的Thread-2。

二、自适应自旋锁

jdk1.6之后,引入了自适应自旋锁,在重量级锁当中,也进行了一些优化。

前面提到当发生锁膨胀后,没持有锁的线程会进入Monitor的EntryList当中进行阻塞,实际情况是,会通过自适应自旋锁进行一定次数的自旋,如果获取到锁了,就避免进入阻塞状态(会进行上下文切换)。如果没获取到锁,此时在进入阻塞状态。

  • 自旋是占用CPU的,只有在多核CPU中才能发挥优势。

  • 自适应自旋锁会动态调整自旋次数,获取锁成功的次数多,就会多自旋几次;如果一次都没有成功,则会可能会直接进行阻塞。

  • java7后不能控制是否开启自旋锁。

举报/反馈

高级互联网专家

8.5万获赞 2.8万粉丝
互联网软件“卓越技术顾问”,顶级软件架构开发者(承接各类软件项目开发,欢迎合作)<网站、管理后台、app、小程序、服务系统、技术支持等>!
优质科技领域创作者,活力创作者
关注
0
0
收藏
分享