前言

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

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

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

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

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

对象头:对象头由两部分组成,第一部分是对象的运行时数据(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来区分是无锁还是可偏向。

二、锁的信息打印

下面我们先用代码验证一下这几种锁的存在(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 }

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

1、无锁状态

先不睡眠五秒,此时偏向锁未开启,所以对象都是无锁状态(未加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 (00000 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字节的整数倍。

2、偏向锁状态

加上睡眠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的值。

3、轻量级锁状态

再看看轻量锁,不睡眠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,轻量级锁

4、重量级锁状态

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

 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,重量级锁。

5、调用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),所以此后该对象无法再进入偏向锁状态。

 三、锁膨胀过程

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

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

1、开启偏向锁

开启偏向的锁膨胀草图

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

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

2、关闭偏向锁

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

关闭偏向的锁膨胀草图

四、重量级锁原理

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

后记

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

Java并发之synchronized关键字深度解析(二)的更多相关文章

  1. Java并发之synchronized关键字深度解析(一)

    前言 近期研读路神之绝世武学,徜徉于浩瀚无垠知识之海洋,偶有攫取吉光片羽,惶恐未领略其精髓即隐入岁月深处,遂急忙记录一二,顺备来日吹cow之谈资.本小系列为并发之亲儿子-独臂狂侠synchronize ...

  2. Java并发之synchronized关键字深度解析(三)

    前言 本篇主要介绍一下synchronized的批量重偏向和批量撤销机制,属于深水区,大家提前备好氧气瓶. 上一篇说完synchronized锁的膨胀过程,下面我们再延伸一下synchronized锁 ...

  3. Java并发之synchronized关键字

         上篇文章我们主要介绍了并发的基本思想以及线程的基本知识,通过多线程我们可以实现对计算机资源的充分利用,但是在最后我们也说明了多线程给程序带来的两种典型的问题,针对它们,synchronize ...

  4. Java并发之synchronized关键字和Lock接口

    欢迎点赞阅读,一同学习交流,有疑问请留言 . GitHub上也有开源 JavaHouse,欢迎star 引用 当开发过程中,我们遇到并发问题.怎么解决? 一种解决方式,简单粗暴:上锁.将千军万马都给拦 ...

  5. 深入理解Java并发之synchronized实现原理

    深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoader) 深入 ...

  6. 并发之synchronized关键字的应用

    并发之synchronized关键字的应用 synchronized关键字理论基础 前两章我们学习了下java内存模型的相关知识, 现在我们来讲讲逢并发必出现的synchronized关键字. 作用 ...

  7. Java并发之synchronized

    Java多线程同步关键词是常用的多线程同步手段.它可以修饰静态类方法,实例方法,或代码块.修饰static静态方法时是对整个类加锁. 一.实现原理 在JVM中对象内存分三块区域,对象头.实例数据.对齐 ...

  8. 巨人大哥谈Java中的Synchronized关键字用法

    巨人大哥谈Java中的Synchronized关键字用法 认识synchronized 对于写多线程程序的人来说,经常碰到的就是并发问题,对于容易出现并发问题的地方价格synchronized基本上就 ...

  9. Java进阶1. Synchronized 关键字

    Java进阶1. Synchronized 关键字 20131025 1.关于synchronized的简介: Synchronized 关键字代表对这个方法加锁,相当于不管那一个线程,运行到这个方法 ...

随机推荐

  1. HTTP基础及telnet简单命令

    一.HTTP概况 20世纪90年代初期,一个主要的新兴应用即万维网(World Wide Web)登上了舞台.Web是一个引起公众注意的因特网应用.Web的应用层协议是超文本传输协议(HTTP),它是 ...

  2. Linux 7开机自启项查看并设置

      在Linux6中查看及设置开机自启信息是使用chkconfig命令,Linux7中此命令已经被替代,接下来我们就来研究下Linux7中的区别所在. chkconfig --list Note: T ...

  3. ip地址计算

    1.多少个子网? 2x个,其中x为被遮盖(取值为1)的位数.例如,在11000000(这个值是子网掩码的最后几位,例如,mask=18)中,取值为1的位数为2,因此子网数位22=4个: 2.每个子网包 ...

  4. ##* %%* linux变量处理

    链接来自他们分享,,,, 如有侵权,请联系本人删除,本人将立即删除.停止分享. https://blog.csdn.net/fengzijinliang/article/details/4252021 ...

  5. 程序员的进阶课-架构师之路(9)-平衡二叉树(AVL树)

    版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/m0_37609579/article/de ...

  6. 【数据结构】之串(C语言描述)

    串(字符串)是编程中最常用的结构,但 C语言 中没有“字符串”这种变量,只能通过字符数组的形式表示字符串. C语言 为我们提供了一个 string.h 的头文件,通过这个头文件,我们可以实现对字符串的 ...

  7. java程序员面试答题技巧

    答题时,先答是什么,再答有什么作用和要注意什么(这部分最重要,展现自己的心得) 答案的段落分别,层次分明,条理清晰都非常重要,从这些表面的东西也可以看出一个人的 习惯.办事风格.条理等. 要讲你做出答 ...

  8. DevOps on DevCloud|如何采用流水线践行CI/CD理念【华为云技术分享】

    [摘要] 持续集成/持续交付(CI/CD,Continuous Integration/Continuous Deployment)在DevOps CMALS理念中具有支柱性地位,因而CI/CD流水线 ...

  9. FF.PyAdmin 接口服务/后台管理微框架 (Flask+LayUI)

    源码(有兴趣的朋友请Star一下) github: https://github.com/fufuok/FF.PyAdmin gitee: https://gitee.com/fufuok/FF.Py ...

  10. 基于VMware Workstation下Windows server的搭建

    网络安全学习内容 一.VMware安装Windows系统   1.1安装配置虚拟机 需要提前准备的东西: 配置网络实验室的IP: 为了满足实验中一些需要用到网络的需求,学校为我们提供了每个人学号密码的 ...