作者:京东批发 刘跃明

Monitor概念

Java对象的内存布局

对象除了咱们自定义的一些属性外,还有其它数据,在内存中能够分为三个区域:对象头、实例数据、对齐填充,这三个区域组成起来才是一个残缺的对象。

对象头:在JVM中须要大量存储对象,存储时为了实现一些额定的性能,须要在对象中增加一些标记字段用于加强对象性能,这些标记字段组成了对象头。

实例数据:寄存类的属性数据信息,包含父类的属性信息。

对齐填充:因为虚拟机要求对象其实地址必须是8字节的整数倍,须要存在填充区域以满足8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐。

图1

Java对象头

JVM中对象头的形式有以下两种(以32位虚拟机为例):

一般对象

Object Header (64 bits)
Mark Word (32 bits)Klass Word (32 bits)

数组对象

Object Header (96 bits)
Mark Word(32bits)Klass Word(32bits)array length(32bits)

Mark Word

这部分次要用来存储对象本身的运行数据,如hashcode、gc分带年龄等,Mark Word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark Word为32位,64位JVM为64位。为了让一个字大小存储更多的信息,JVM将字的最低两个位设置为标记位,不同标记位下的Mark Word示意如下:

Mark Word (32 bits)State
identity_hashcode:25age:4biased_lock:1lock:2Normal
thread:23epoch:2age:4biased_lock:1lock:2Biased
ptr_to_lock_record:30lock:2LightweightLocked
ptr_to_heavyweight_monitor:30lock:2HeavyweightLocked
lock:2Marked for GC

其中各局部的含意如下:

lock: 2位的锁状态标记位,该标记的值不同,整个Mark Word示意的含意不同。

biased_locklock状态
001无锁
101偏差锁
000轻量级锁
010重量级锁
011GC标记

biased_lock: 对象是否启用偏差锁标记,只占1个二进制位,为1时示意对象启用偏差锁,为0时示意对象没有偏差锁。

age: 4位的Java对象年龄,在GC中,如果对象再Survivor区复制一次,年龄减少1,当对象达到设定的阈值时,将会降职到老年代,默认状况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6,因为age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的起因。

identity_hashcode: 25位的对象示意Hash码,采纳提早加载技术,调用办法System.idenHashcode()计算,并会将后果写到该对象头中,当对象被锁定时,该值会挪动到管程Monitor中。

thread: 持有偏差锁的线程ID。

epoch: 偏差工夫戳。

ptr_to_lock_record: 指向栈中锁记录的指针。

ptr_to_heavyweight_monitor: 指向管程Monitor的指针。

Klass Word

这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例,该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。

array length

如果对象是一个数组,那么对象头还须要有额定的空间用于存储数组的长度,这部分数据的长度也随着JVM架构的不同而不同:32位的JVM长度为32位,64位JVM则为64位。

Monitor原理

Monitor被翻译为监视器管程

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

Monitor构造如下:

图2

•刚开始Monitor中Owner为null

•当Thread-2执行synchronized(obj)就会将Monitor的所有者Owner置为Thread-2,Monitor中只能有一个Owner

•在Thread-2上锁的过程中,如果Thread-3、Thread-4、Thread-5也来执行synchronized(obj),就会进入EntryList BLOCKED

•Thread-2执行完同步代码块的内容,而后唤醒EntryList中期待的线程来竞争锁,竞争是非偏心的,也就是先进并非先获取锁

•图2中WaitSet中的Thread-0、Thread-1是之前取得过锁,但条件不满足进入WAITING状态的线程,前面讲wait-notify时会剖析

留神:

•synchronized必须是进入同一个对象的Monitor才有上述的成果

•不加synchronized的对象不会关联监视器,不听从以上规定

synchronized原理

static final Object lock = new Object();static int counter = 0;public static void main(String[] args) {    synchronized (lock) {        counter++;    }}

对应的字节码为:

public static main([Ljava/lang/String;)V

TRYCATCHBLOCK L0 L1 L2 null

TRYCATCHBLOCK L2 L3 L2 null

L4

LINENUMBER 6 L4

GETSTATIC MyClass03.lock : Ljava/lang/Object;

DUP

ASTORE 1

MONITORENTER //正文1

L0

LINENUMBER 7 L0

GETSTATIC MyClass03.counter : I

ICONST_1

IADD

PUTSTATIC MyClass03.counter : I

L5

LINENUMBER 8 L5

ALOAD 1

MONITOREXIT //正文2

L1

GOTO L6

L2

FRAME FULL [[Ljava/lang/String; java/lang/Object] [java/lang/Throwable]

ASTORE 2

ALOAD 1

MONITOREXIT //正文3

L3

ALOAD 2

ATHROW

L6

LINENUMBER 9 L6

FRAME CHOP 1

RETURN

L7

LOCALVARIABLE args [Ljava/lang/String; L4 L7 0

MAXSTACK = 2

MAXLOCALS = 3

正文1

MONITORENTER的意思为:每个对象都有一个监督锁(Monitor),当Monitor被占用时就会处于锁定状态,线程执行MONITORENTER指令时尝试获取Monitor的所有权,过程如下:

•如果Monitor的进入数为0,则该线程进入Monitor,并将进入数设置为1,该线程即为Monitor的所有者(Owner)

•如果该线程曾经占用Monitor,只是从新进入Monitor,则进入Monitor的进入数加1

•如果其它线程曾经占用Monitor,则该线程进入阻塞状态,直到Monitor进入数为0,再从新尝试获取Monitor的所有权

正文2

MONITOREXIT的意思为:执行指令时,Monitor的进入数减1,如果减1后进入数为0,该线程退出Monitor,不再是这个Monitor的所有者,其它被Monitor阻塞的线程从新尝试获取Monitor的所有权。

总结

通过正文1和正文2可知,synchronized的实现原理,底层是通过Monitor的对象来实现,其实wait和notify等办法也依赖Monitor,这就是为什么wait和notify办法必须要在同步办法内调用,否则会抛出java.lang.IllegalMonitorStateException的起因。

如果程序失常执行则按上述形容即可实现,如果程序在同步办法内产生异样,代码则会走正文3,在正文3能够看到MONITOREXIT指令,也就是synchronized曾经解决异常情况下的退出。

注:办法级别的synchronized不会在字节码指令中有所体现,而是在常量池中减少了ACC_SYNCHRONIZED标识符,JVM就是通过该标识符来实现同步的,办法调用时,JVM会判断办法的ACC_SYNCHRONIZED是否被设置,如果被设置,线程执行办法前会先获取Monitor所有权,执行完办法后再开释Monitor所有权,实质是一样的。

synchronized原理进阶

轻量级锁

轻量级锁的应用场景:如果一个对象尽管有多线程要加锁,但加锁的工夫是错开的(也就是没有竞争),那么能够应用轻量级锁来优化。

轻量级锁对使用者是通明的,即语法依然是synchronized

假如有两个办法同步块,利用同一个对象加锁

static final Object obj = new Object();public static void method1() {    synchronized (obj) { // 同步块 A        method2();    }}public static void method2() {    synchronized (obj) { // 同步块 B    }}

创立锁记录(Lock Record)对象,每个线程的栈帧都会蕴含一个锁记录的构造,外部能够存储锁定对象的Mark Word

图3

让锁记录中Object reference指向锁对象,并尝试用cas替换Object的Mark Word,将Mark Word的值存入锁记录

图4

如果cas替换胜利,对象头中存储了锁记录地址和状态00,示意由该线程给对象加锁,这是图示如下

图5

如果cas失败,有两种状况

•如果是其它线程曾经持有了该Object的轻量级锁,这是表明有竞争,进入锁收缩过程

•如果是本人线程执行了synchronized锁重入,那么再增加一条Lock Record作为重入的技术

图6

当退出synchronized代码块(解锁时),如果有取值为null的锁记录,示意由重入,这是重置锁记录,示意重入技术减一

图7

当退出synchronized代码块(解锁时),锁记录的值不为null,这时应用cas将Mark Word的值回复给对象头

•胜利,则解锁胜利

•失败,阐明轻量级锁进行了锁收缩或曾经降级为重量级锁,进入重量级锁解锁流程

锁收缩

如果在尝试加轻量级锁的过程中,CAS操作无奈胜利,这是一种状况就是有其它线程为此对象加上了轻量级锁(有竞争),这是须要进行锁收缩,将轻量级锁变为重量级锁。

当Thread-1进行轻量级加锁时,Thread-0曾经对该对象加了轻量级锁

图8

这是Thread-1加轻量级锁失败,进入锁收缩流程

•即为Object对象申请Monitor锁,让Object指向重量级锁地址

•而后本人进入Monitor的EntryList BLOCKED

图9

当Thread-0退出同步块解锁时,应用cas将Mark Word的值复原给对象头,失败,这是会进入重量级解锁流程,即依照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList 中BLOCKED线程

自旋优化

重量级锁竞争的时候,还能够应用自旋来进行优化,如果以后线程自旋胜利(即这时候持锁线程曾经退出了同步,开释了锁),这是以后线程就能够防止阻塞。

自旋重试胜利的状况

线程1(core 1上)对象Mark线程2(core 2 上)
-10(分量锁)-
拜访同步块,获取Monitor10(分量锁)分量锁指针-
胜利(加锁)10(分量锁)分量锁指针-
执行同步块10(分量锁)分量锁指针-
执行同步块10(分量锁)分量锁指针拜访同步块,获取Monitor
执行同步块10(分量锁)分量锁指针自旋重试
执行结束10(分量锁)分量锁指针自旋重试
胜利(解锁)01(无锁)自旋重试
-10(分量锁)分量锁指针胜利(加锁)
-10(分量锁)分量锁指针执行同步块
-

自旋重试失败的状况

线程1(core 1上)对象Mark线程2(core 2 上)
-10(分量锁)-
拜访同步块,获取Monitor10(分量锁)分量锁指针-
胜利(加锁)10(分量锁)分量锁指针-
执行同步块10(分量锁)分量锁指针-
执行同步块10(分量锁)分量锁指针拜访同步块,获取Monitor
执行同步块10(分量锁)分量锁指针自旋重试
执行同步块10(分量锁)分量锁指针自旋重试
执行同步块10(分量锁)分量锁指针自旋重试
执行同步块10(分量锁)分量锁指针阻塞
-

•自旋会占用CPU工夫,单核CPU自旋就是节约,多核CPU自旋能力发挥优势。

•在Java 6之后自旋锁是自适应的,比方对象刚刚的一次自旋操作胜利过,那么认为这次自旋胜利的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比拟智能。

•Java 7之后不能管制是否开启自旋性能。

偏差锁

轻量级锁在没有竞争时(就本人这个线程),每次重入依然须要执行CAS操作。

Java 6中引入了偏差锁做进一步优化:只有第一次应用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是本人的就示意没有竞争,不必从新CAS,当前只有不产生竞争,这个对象就归该线程所有。

注:

Java 15之后废除偏差锁,默认是敞开,如果想应用偏差锁,配置-XX:+UseBiasedLocking启动参数。

启动偏差锁之后,偏差锁有一个提早失效的机制,这是因为JVM启动时会进行一系列的简单流动,比方装载配置,零碎类初始化等等。在这个过程中会应用大量synchronized关键字对对象加锁,且这些锁大多数都不是偏差锁。为了缩小初始化工夫,JVM默认延时加载偏差锁。这个延时的工夫大略为4s左右,具体工夫因机器而异。当然咱们也能够设置JVM参数 -XX:BiasedLockingStartupDelay=0 来勾销延时加载偏差锁。

例如:

static final Object obj = new Object();public static void m1() {    synchronized (obj) { // 同步块 A        m2();    }}public static void m2() {    synchronized (obj) { // 同步块 B        m3();    }}public static void m3() {    synchronized (obj) {    }}

如果敞开偏差锁,应用轻量锁状况:

图10

开启偏差锁,应用偏差锁状况:

图11

偏差状态

回顾一下对象头格局

Mark Word (32 bits)State
identity_hashcode:25age:4biased_lock:1lock:2Normal
thread:23epoch:2age:4biased_lock:1lock:2Biased
ptr_to_lock_record:30lock:2LightweightLocked
ptr_to_heavyweight_monitor:30lock:2HeavyweightLocked
lock:2Marked for GC

一个对象创立时:

•如果开启了偏差锁(默认开启),那么对象创立后,Mark Word值为0x05,也就是最初是3位为101,这是它的thread、epoch、age都为0

•如果没有开启偏差锁,那么对象创立后,Mark Word值为0x01,也就是最初3位为001,这时它的hashcode、age都为0,第一次用到hashcode时才会赋值

咱们来验证下,应用jol第三方工具,以及对工具打印对象头做了一个解决,让对象头开起来更简便:

测试代码

public synchronized static void main(String[] args){    log.info("{}", toSimplePrintable(object));}

开启偏差锁的状况下

打印的数据如下(因为Java15之后偏差锁废除,因而关上偏差锁打印会正告)

17:15:17 [main] c.MyClass03 - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101

最初为101,其余都为0,验证了上述第一条。

可能你又要问了,我这也没应用synchronized关键字呀,那不也应该是无锁么?怎么会是偏差锁呢?

认真看一下偏差锁的组成,对照输入后果红色划线地位,你会发现占用 thread 和 epoch 的 地位的均为0,阐明以后偏差锁并没有偏差任何线程。此时这个偏差锁正处于可偏差状态,筹备好进行偏差了!你也能够了解为此时的偏差锁是一个非凡状态的无锁

敞开偏差锁的状况下

打印的数据如下

17:18:32 [main] c.MyClass03 - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

最初为001,其它都是0,验证了上述第二条。

接下来验证加锁的状况,代码如下:

private static Object object = new Object();public synchronized static void main(String[] args){    new Thread(()->{        log.info("{}", "synchronized前");        log.info("{}", toSimplePrintable(object));        synchronized (object){            log.info("{}", "synchronized中");            log.info("{}", toSimplePrintable(object));        }        log.info("{}", "synchronized后");        log.info("{}", toSimplePrintable(object));    },"t1").start();}

开启偏差锁的状况,打印数据如下

17:24:05 [t1] c.MyClass03 - synchronized前

17:24:05 [t1] c.MyClass03 - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101

17:24:05 [t1] c.MyClass03 - synchronized中

17:24:05 [t1] c.MyClass03 - 00000000 00000000 00000000 00000001 00001110 00000111 01001000 00000101

17:24:05 [t1] c.MyClass03 - synchronized后

17:24:05 [t1] c.MyClass03 - 00000000 00000000 00000000 00000001 00001110 00000111 01001000 00000101

应用了偏差锁,并记录了线程的值(101后面的一串数字),然而处于偏差锁的对象解锁后,线程id仍存储于对象头中。

敞开偏差锁的状况,打印数据如下

17:28:24 [t1] c.MyClass03 - synchronized前

17:28:24 [t1] c.MyClass03 - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

17:28:24 [t1] c.MyClass03 - synchronized中

17:28:24 [t1] c.MyClass03 - 00000000 00000000 00000000 00000001 01110000 00100100 10101001 01100000

17:28:24 [t1] c.MyClass03 - synchronized后

17:28:24 [t1] c.MyClass03 - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

应用轻量锁(最初为000),并且记录了占中存储的锁信息地址(000后面一串数字),同步块完结后复原到原先状态(因为没有应用hashcode,所以hashcode值为0)。

偏差锁撤销

在真正解说偏差撤销之前,须要和大家明确一个概念——偏差锁撤销和偏差锁开释是两码事。

•撤销:抽象的说就是多个线程竞争导致不能再应用偏差模式的时候,次要是告知这个锁对象不能再用偏差模式

•开释:和你的惯例了解一样,对应的就是 synchronized 办法的退出或 synchronized 块的完结

何为偏差撤销?

从偏差状态撤回原有的状态,也就是将 MarkWord 的第 3 位(是否偏差撤销)的值,从 1 变回 0

如果只是一个线程获取锁,再加上「偏心」的机制,是没有理由撤销偏差的,所以偏差的撤销只能产生在有竞争的状况下

撤销-hashcode调用

调用了对象的hashcode会导致偏差锁被撤销:

•轻量级锁会在锁记录中记录hashcode

•重量级锁会在Monitor中记录hashcode

测试代码如下

private static Object object = new Object();public synchronized static void main(String[] args){    object.hashCode();//调用hashcode    new Thread(()->{        log.info("{}", "synchronized前");        log.info("{}", toSimplePrintable(object));        synchronized (object){            log.info("{}", "synchronized中");            log.info("{}", toSimplePrintable(object));        }        log.info("{}", "synchronized后");        log.info("{}", toSimplePrintable(object));    },"t1").start();}

打印如下:

17:36:05 [t1] c.MyClass03 - synchronized前

17:36:06 [t1] c.MyClass03 - 00000000 00000000 00000000 01011111 00100001 00001000 10110101 00000001

17:36:06 [t1] c.MyClass03 - synchronized中

17:36:06 [t1] c.MyClass03 - 00000000 00000000 00000000 00000001 01101110 00010011 11101001 01100000

17:36:06 [t1] c.MyClass03 - synchronized后

17:36:06 [t1] c.MyClass03 - 00000000 00000000 00000000 01011111 00100001 00001000 10110101 00000001

撤销-其它线程应用对象

当有其它线程应用偏差锁对象时,会将偏差锁降级为轻量级锁。

测试代码如下

private static void test2() {    Thread t1 = new Thread(() -> {        synchronized (object) {            log.info("{}", toSimplePrintable(object));        }        synchronized (MyClass03.class) {            MyClass03.class.notify();//t1执行完之后才告诉t2执行        }    }, "t1");    t1.start();    Thread t2 = new Thread(() -> {        synchronized (MyClass03.class) {            try {                MyClass03.class.wait();            } catch (InterruptedException e) {                e.printStackTrace();            }        }        log.info("{}", toSimplePrintable(object));        synchronized (object) {            log.info("{}", toSimplePrintable(object));        }        log.info("{}", toSimplePrintable(object));    }, "t2");    t2.start();}

打印数据如下

17:51:38 [t1] c.MyClass03 - 00000000 00000000 00000000 00000001 01000111 00000000 11101000 00000101

17:51:38 [t2] c.MyClass03 - 00000000 00000000 00000000 00000001 01000111 00000000 11101000 00000101

17:51:38 [t2] c.MyClass03 - 00000000 00000000 00000000 00000001 01111000 00100000 01101001 01010000

17:51:38 [t2] c.MyClass03 - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

能够看到线程t1是应用偏差锁,线程t2应用锁之前是一样的,然而一旦应用了锁,便降级为轻量级锁,执行完同步代码之后,复原成撤销偏差锁的状态。

撤销-调用wait/notify

代码如下

private static void test3(){    Thread t1 = new Thread(() -> {        log.info("{}", toSimplePrintable(object));        synchronized (object) {            log.info("{}", toSimplePrintable(object));            try {                object.wait();            } catch (InterruptedException e) {                e.printStackTrace();            }            log.info("{}", toSimplePrintable(object));        }    }, "t1");    t1.start();    new Thread(() -> {        try {            Thread.sleep(6000);        } catch (InterruptedException e) {            e.printStackTrace();        }        synchronized (object) {            log.debug("notify");            object.notify();        }    }, "t2").start();}

打印数据如下

17:57:57 [t1] c.MyClass03 - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101

17:57:57 [t1] c.MyClass03 - 00000000 00000000 00000000 00000001 00001111 00001100 11010000 00000101

17:58:02 [t2] c.MyClass03 - notify

17:58:02 [t1] c.MyClass03 - 00000000 00000000 01100000 00000000 00000011 11000001 10000010 01110010

调用wait和notify得是用Monitor,所以会从偏差锁降级为重量级锁。

批量重偏差

如果对象尽管被多个线程拜访,但没有竞争,这是偏差了线程t1的对象依然有机会从新偏差t2,重偏差会重置对象的Thread ID。

当撤销偏差锁阈值超过20次后,JVM会这样感觉,我是不是偏差错了呢,于是会在给这些对象加锁时从新偏差至加锁线程。

代码如下

public static class Dog{}private static void test4() {    Vector<Dog> list = new Vector<>();    Thread t1 = new Thread(() -> {        for (int i = 0; i < 30; i++) {            Dog d = new Dog();            list.add(d);            synchronized (d) {                log.info("{}", i+"\t"+toSimplePrintable(d));            }        }        synchronized (list) {            list.notify();        }    }, "t1");    t1.start();    Thread t2 = new Thread(() -> {        synchronized (list) {            try {                list.wait();            } catch (InterruptedException e) {                e.printStackTrace();            }        }        log.debug("===============> ");        for (int i = 0; i < 30; i++) {            Dog d = list.get(i);            log.info("{}", i+"\t"+toSimplePrintable(d));            synchronized (d) {                log.info("{}", i+"\t"+toSimplePrintable(d));            }            log.info("{}", i+"\t"+toSimplePrintable(d));        }    }, "t2");    t2.start();}

打印如下

图12

另外我在测试的是否发现一个线程,当对象是一般类(如Dog)时,重偏差的阈值就是20,也就是第21次开启了偏差锁,然而如果把一般类替换成Object时,重偏差的阈值就是9,也就是第10次开启了偏差锁并重偏差(如图13),这是怎么回事儿,有理解的同学能够评论交换下。

图13

批量撤销

当撤销偏差锁阈值超过40次后,JVM会这样感觉,本人的确偏差错了,基本不该偏差,于是整个类的所有对象都会变为不可偏差的,新建的对象也是不可偏差的。

代码如下

static Thread t1, t2, t3;private static void test6() throws InterruptedException {    Vector<Dog> list = new Vector<>();    int loopNumber = 40;    t1 = new Thread(() -> {        for (int i = 0; i < loopNumber; i++) {            Dog d = new Dog();            list.add(d);            synchronized (d) {                log.info("{}", i + "\t" + toSimplePrintable(d));            }        }        LockSupport.unpark(t2);    }, "t1");    t1.start();    t2 = new Thread(() -> {        LockSupport.park();        log.debug("===============> ");        for (int i = 0; i < loopNumber; i++) {            Dog d = list.get(i);            log.info("{}", i + "\t" + toSimplePrintable(d));            synchronized (d) {                log.info("{}", i + "\t" + toSimplePrintable(d));            }            log.info("{}", i + "\t" + toSimplePrintable(d));        }        LockSupport.unpark(t3);    }, "t2");    t2.start();    t3 = new Thread(() -> {        LockSupport.park();        log.debug("===============> ");        for (int i = 0; i < loopNumber; i++) {            Dog d = list.get(i);            log.info("{}", i + "\t" + toSimplePrintable(d));            synchronized (d) {                log.info("{}", i + "\t" + toSimplePrintable(d));            }            log.info("{}", i + "\t" + toSimplePrintable(d));        }    }, "t3");    t3.start();    t3.join();    log.info("{}", toSimplePrintable(new Dog()));}

打印如下

图14