Java并发之synchronized关键字深度解析(二)

前言java

    本文继续【Java并发之synchronized关键字深度解析(一)】一文而来,着重介绍synchronized几种锁的特性。并发

1、对象头结构及锁状态标识jvm

    synchronized关键字是如何实现的给对象加锁?首先咱们要了解一下java中对象的组成。java中的对象由3部分组成,第一部分是对象头,第二部分是实例数据,第三部分是对齐填充。ide

    对齐填充:jvm规定对象的起始内存地址必须是8字节的整数倍,若是不够的话就用占位符来填充,此部分占位符就是对齐填充;测试

    实例数据:实例数据是对象存储的真正有效的信息-对象的成员变量信息(包括继承自父类的);spa

    对象头:对象头由两部分组成,第一部分是对象的运行时数据(Mark Word),包括哈希吗、锁偏向标识、锁类型、GC分代年龄、偏向线程id等;第二部分是对象的类型指针(Kclass Word),用于去堆中定位对象的实例数据和方法区中的类型数据。java对象的公共特性都在对象头中存放。操作系统

对象头存储内容以下所示(以64位操做系统为例):线程

|--------------------------------------------------------------------------------------------------------------|
|                                              Object Header (128 bits)                                        |
|--------------------------------------------------------------------------------------------------------------|
|                        Mark Word (64 bits)                                    |      Klass Word (64 bits)    |       
|--------------------------------------------------------------------------------------------------------------|
|  unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |     OOP to metadata object   |  无锁
|----------------------------------------------------------------------|--------|------------------------------|
|  thread:54 |         epoch:2      | unused:1 | age:4 | biased_lock:1 | lock:2 |     OOP to metadata object   | 偏向锁 |----------------------------------------------------------------------|--------|------------------------------| | ptr_to_lock_record:62 | lock:2 | OOP to metadata object | 轻量锁 |----------------------------------------------------------------------|--------|------------------------------| | ptr_to_heavyweight_monitor:62 | lock:2 | OOP to metadata object | 重量锁 |----------------------------------------------------------------------|--------|------------------------------| | | lock:2 | OOP to metadata object | GC |--------------------------------------------------------------------------------------------------------------|

其中lock:2表示有2bit控制锁类型,biased_lock:1表示1bit控制偏向锁状态,对应关系以下所示:设计

01:无锁(前面偏向锁状态为0时表示未锁定)指针

01:可偏向(前面偏向锁状态为1时表示可偏向)

00:轻量级锁

10:重量级锁

11:GC标记

    看到前两种状态时可能道友们会有些迷糊,先别着急,此处只要记住JVM的设计者们想用01状态来表示两种状况(无锁和可偏向),可是地球人都知道一个字符是没法作到标识两种状态的,因此他们就把前面一位暂时用不到的bit归入进来,用前一位的值是0仍是1来区分是无锁仍是可偏向。

2、锁的信息打印

    下面咱们先用代码验证一下这几种锁的存在(JVM默认开启偏向锁,默认的偏向锁启动时间为4-5秒后,因此先让主线程睡5秒再加锁能保证对象处于偏向锁的状态,此处也能够在VM Options中添加参数 【-XX:BiasedLockingStartupDelay=0】来让JVM取消延迟启动偏向锁(本文的示例均未设置此参数),其效果跟不改变VM Options只在main方法中让主线程先睡眠5秒是同样的)

 此外,要打印对象存储空间须要引入openjdk的jar包依赖

<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>

User对象代码:

1 public class User {
2     public String name; 3 public byte age; 4 }

万事具有,下面开始测试:

一、无锁状态

先不睡眠五秒,此时偏向锁未开启,因此对象都是无锁状态(未加synchronized的状况下),打印无锁状态的对象(锁标识001)

 1 package com.mybokeyuan.lockDemo;
 2 
 3 import org.openjdk.jol.info.ClassLayout; 4 5 public class LockClientTest { 6 public static void main(String[] args) { 7 User user = new User(); 8  System.out.println(ClassLayout.parseInstance(user).toPrintable()); 9  } 10 }

输出结果:

com.mybokeyuan.lockDemo.User object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387) 12 1 byte User.age 0 13 3 (alignment/padding gap) 16 4 java.lang.String User.name null 20 4 (loss due to the next object alignment) Instance size: 24 bytes Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

下面咱们来解读一下这个打印结果。

经过TYPE DESCRIPTION能够知道,前三行打印的是对象头(object header),那么后面四行就是对象的实例数据和对其填充了。

先看第一行,VALUE中,标红的001表示当前对象是无锁状态,前面的0对应咱们上面讲的可偏向锁状态为非偏向锁(若是是1表示偏向锁)。第三行存放的是对象指针。

第四行和第六行存放的是对象的两个成员变量,第五行空间用于填充age变量;第七行就是咱们所说的对齐填充,使对象内存空间凑齐8字节的整数倍。

二、偏向锁状态

加上睡眠5秒

 1 package com.mybokeyuan.lockDemo;
 2 
 3 import org.openjdk.jol.info.ClassLayout; 4 5 public class LockClientTest { 6 public static void main(String[] args) { 7 // 先睡眠5秒,保证开启偏向锁 8 try { 9 Thread.sleep(5000); 10 } catch (InterruptedException e) { // -XX:-UseBiasedLocking 11 e.printStackTrace(); // -XX:BiasedLockingStartupDelay=0 12  } 13 User user = new User(); 14  System.out.println(ClassLayout.parseInstance(user).toPrintable()); 15  } 16 }

看看打印结果:

com.mybokeyuan.lockDemo.User object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387) 12 1 byte User.age 0 13 3 (alignment/padding gap) 16 4 java.lang.String User.name null 20 4 (loss due to the next object alignment) Instance size: 24 bytes Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

能够看到,锁状态为101可偏向锁状态了,只是因为未用synchronized加锁,因此线程id是空的。其他数据跟上述无锁状态同样。

偏向锁带线程id状况,代码以下:

 1 package com.mybokeyuan.lockDemo;
 2 
 3 import org.openjdk.jol.info.ClassLayout; 4 5 public class LockClientTest { 6 public static void main(String[] args) { 7 // 先睡眠5秒,保证开启偏向锁 8 try { 9 Thread.sleep(5000); 10 } catch (InterruptedException e) { // -XX:-UseBiasedLocking 11 e.printStackTrace(); // -XX:BiasedLockingStartupDelay=0 12  } 13 User user = new User(); 14 synchronized (user) { 15  System.out.println(ClassLayout.parseInstance(user).toPrintable()); 16  } 17  } 18 }

输出结果:

com.mybokeyuan.lockDemo.User object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           05 e8 d0 02 (00000101 11101000 11010000 00000010) (47245317) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387) 12 1 byte User.age 0 13 3 (alignment/padding gap) 16 4 java.lang.String User.name null 20 4 (loss due to the next object alignment) Instance size: 24 bytes Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

可见第一行中后面再也不是0了,有了线程id的值。

三、轻量级锁状态

再看看轻量锁,不睡眠5秒,直接用synchronized给对象加锁,此时触发的就是轻量锁。代码以下:

 1 package com.mybokeyuan.lockDemo;
 2 
 3 import org.openjdk.jol.info.ClassLayout; 4 5 public class LockClientTest { 6 public static void main(String[] args) { 7 User user = new User(); 8 synchronized (user) { 9  System.out.println(ClassLayout.parseInstance(user).toPrintable()); 10  } 11  } 12 }

打印结果:

com.mybokeyuan.lockDemo.User object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           e8 f2 47 03 (11101000 11110010 01000111 00000011) (55046888) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387) 12 1 byte User.age 0 13 3 (alignment/padding gap) 16 4 java.lang.String User.name null 20 4 (loss due to the next object alignment) Instance size: 24 bytes Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

能够看到锁的标识位为000,轻量级锁

四、重量级锁状态

最后看一下重量级锁,只有在锁竞争的时候才会变为重量级锁,代码以下:

 1 package com.mybokeyuan.lockDemo;
 2 
 3 import org.openjdk.jol.info.ClassLayout; 4 5 public class LockClientTest { 6 public static void main(String[] args) { 7 User user = new User(); 8  System.out.println(ClassLayout.parseInstance(user).toPrintable()); 9 Thread t1 = new Thread(() -> { 10 synchronized (user) { 11 try { 12 Thread.sleep(5000);// 睡眠,创造竞争条件 13 } catch (InterruptedException e) { 14  e.printStackTrace(); 15  } 16  } 17  }); 18  t1.start(); 19 Thread t2 = new Thread(() -> { 20 synchronized (user) { 21 try { 22 Thread.sleep(1000); 23 } catch (InterruptedException e) { 24  e.printStackTrace(); 25  } 26  } 27  }); 28  t2.start(); 29  System.out.println(ClassLayout.parseInstance(user).toPrintable()); 30  } 31 }

输出结果为:

com.mybokeyuan.lockDemo.User object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           8a e4 ba 02 (10001010 11100100 10111010 00000010) (45802634) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387) 12 1 byte User.age 0 13 3 (alignment/padding gap) 16 4 java.lang.String User.name null 20 4 (loss due to the next object alignment) Instance size: 24 bytes Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

能够看到锁状态为010,重量级锁。

五、调用hashCode会取消偏向

此外,若是经过Object对象的本地hashCode方法来获取对象的hashCode值,会使对象取消偏向锁状态

 1 public class LockClientTest {
 2     public static void main(String[] args) { 3 // 先睡眠5秒,保证开启偏向锁 4 try { 5 Thread.sleep(5000); 6 } catch (InterruptedException e) { // -XX:-UseBiasedLocking 7 e.printStackTrace(); // -XX:BiasedLockingStartupDelay=0 8  } 9 User user = new User(); 10  System.out.println(ClassLayout.parseInstance(user).toPrintable()); 11  System.out.println(user.hashCode()); 12  System.out.println(ClassLayout.parseInstance(user).toPrintable()); 13 14  } 15 }

打印结果:

com.mybokeyuan.lockDemo.User object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 44 c1 00 20 (01000100 11000001 00000000 00100000) (536920388) 12 1 byte User.age 0 13 3 (alignment/padding gap) 16 4 java.lang.String User.name null 20 4 (loss due to the next object alignment) Instance size: 24 bytes Space losses: 3 bytes internal + 4 bytes external = 7 bytes total 460332449 com.mybokeyuan.lockDemo.User object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 a1 1d 70 (00000001 10100001 00011101 01110000) (1880989953) 4 4 (object header) 1b 00 00 00 (00011011 00000000 00000000 00000000) (27) 8 4 (object header) 44 c1 00 20 (01000100 11000001 00000000 00100000) (536920388) 12 1 byte User.age 0 13 3 (alignment/padding gap) 16 4 java.lang.String User.name null 20 4 (loss due to the next object alignment) Instance size: 24 bytes Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

能够看到,计算完对象的hashCode以后,该对象当即从偏向锁状态变为了无锁状态,即便后续给对象加锁,该对象也只会进入轻量级或者重量级锁状态,不会再进入偏向状态了。由于该对象一旦进行Object的hashCode计算,那么对象头中会保存这个hashCode,此时再也没法存放偏向线程的id了(由于对象头的长度没法同时存放hashCode和偏向线程id),因此此后该对象没法再进入偏向锁状态。

 3、锁膨胀过程

到这里,咱们一块儿看完了synchronized给对象加的各类锁状态以及触发场景,下面咱们梳理一下它们之间的关系。

 JVM启动后会默认开启偏向锁(默认4-5秒后开启),开启后,全部新建对象的对象头中都标识为101可偏向状态,且偏向线程id为0,表示处于初始化的偏向锁状态。此后一旦有线程对该对象使用了synchronized加锁,那么就会进入偏向锁状态,偏向线程id记录当前线程id;若是走完同步块以后,有另外一个线程对该对象加锁,那么膨胀为轻量级锁,若是未走完同步块就有另外一个线程试图给该对象加锁,那么会直接膨胀为(中间会有一个自旋锁的过程,此处略去)重量级锁。

一、开启偏向锁

 

                                                                                                             开启偏向的锁膨胀草图

下面演示一下对象从偏向锁膨胀为轻量级锁的过程:

package com.mybokeyuan.lockDemo;

import org.openjdk.jol.info.ClassLayout; public class LockClientTest { public static void main(String[] args) throws Exception { // 先睡眠5秒,保证开启偏向锁 try { Thread.sleep(5000); } catch (InterruptedException e) { // -XX:-UseBiasedLocking e.printStackTrace(); // -XX:BiasedLockingStartupDelay=0  } User user = new User(); Thread t1 = new Thread(() -> { synchronized (user) { System.out.println(ClassLayout.parseInstance(user).toPrintable()); } }); t1.start(); t1.join(); // 确保t1执行完了再执行当前主线程 synchronized (user) { System.out.println(ClassLayout.parseInstance(user).toPrintable()); } System.out.println(ClassLayout.parseInstance(user).toPrintable()); } }

打印结果以下,能够看到user对象先是偏向锁,而后变为轻量级锁,最后走完同步块释放锁变为无锁状态。

com.mybokeyuan.lockDemo.User object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           05 48 6c 1a (00000101 01001000 01101100 00011010) (443303941) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 44 c1 00 20 (01000100 11000001 00000000 00100000) (536920388) 12 1 byte User.age 0 13 3 (alignment/padding gap) 16 4 java.lang.String User.name null 20 4 (loss due to the next object alignment) Instance size: 24 bytes Space losses: 3 bytes internal + 4 bytes external = 7 bytes total com.mybokeyuan.lockDemo.User object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) c0 f0 1f 02 (11000000 11110000 00011111 00000010) (35647680) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 44 c1 00 20 (01000100 11000001 00000000 00100000) (536920388) 12 1 byte User.age 0 13 3 (alignment/padding gap) 16 4 java.lang.String User.name null 20 4 (loss due to the next object alignment) Instance size: 24 bytes Space losses: 3 bytes internal + 4 bytes external = 7 bytes total com.mybokeyuan.lockDemo.User object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 44 c1 00 20 (01000100 11000001 00000000 00100000) (536920388) 12 1 byte User.age 0 13 3 (alignment/padding gap) 16 4 java.lang.String User.name null 20 4 (loss due to the next object alignment) Instance size: 24 bytes Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

 二、关闭偏向锁

若是经过参数设置JVM不开启偏向锁,那么新建立的对象是001无锁状态,遇到synchronized同步块会变为轻量级锁,遇到锁竞争变为重量级锁。

 

                                                                                      关闭偏向的锁膨胀草图

4、重量级锁原理

    Java中synchronized的重量级锁,是基于进入和退出Monitor对象实现的。在编译时会将同步块的开始位置插入monitorenter指令,在结束位置插入monitorexit指令。当线程执行到monitorenter指令时,会尝试获取对象所对应的Monitor全部权,若是获取到了,即获取到了锁,会在Monitor的owner中存放当前线程的id,这样它将处于锁定状态,除非退出同步块,不然其余线程没法获取到这个Monitor。

后记

下一篇将是本小系列的最后一篇,着重介绍synchronized的批量重定向和批量撤销机制,若有不确切之处,欢迎继续拍砖。

相关文章
相关标签/搜索