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

前言

本文继续【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 (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字节的整数倍。

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的批量重定向和批量撤销机制,如有不确切之处,欢迎继续拍砖。

原文地址:https://www.cnblogs.com/zzq6032010/p/11967177.html

时间: 2024-10-23 12:28:03

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

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

前言 近期研读路神之绝世武学,徜徉于浩瀚无垠知识之海洋,偶有攫取吉光片羽,惶恐未领略其精髓即隐入岁月深处,遂急忙记录一二,顺备来日吹cow之谈资.本小系列为并发之亲儿子-独臂狂侠synchronized专场. 一.使用场景 synchronized是java中的一个关键字,用于给对象加锁,保证在单机的并发条件下线程安全.原子性和可见性是它保证线程安全的基础功能.一定要注意它锁的是对象,而且它是一个排他的非公平可重入锁.本文先从使用的场景上来展现其作用. 1.用在普通方法中 常用的格式如下所示:

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

前言 本篇主要介绍一下synchronized的批量重偏向和批量撤销机制,属于深水区,大家提前备好氧气瓶. 说完synchronized锁的膨胀过程,下面我们再延伸一下synchronized锁的两种特殊处理,一种是锁的批量重偏向,一种是锁的批量撤销.JVM中有两个参数,BiasedLockingBulkRebiasThreshold和BiasedLockingBulkRevokeThreshold,前者默认阈值为20,控制批量重偏向:后者默认阈值为40,控制批量撤销.下面我们分别看一下它们是如

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

欢迎点赞阅读,一同学习交流,有疑问请留言 . GitHub上也有开源 JavaHouse,欢迎star 引用 当开发过程中,我们遇到并发问题.怎么解决? 一种解决方式,简单粗暴:上锁.将千军万马都给拦下来,只允许一个人过独木桥.书面意思就是将并行的程序变成串行的程序.现实的锁有门锁.挂锁和抽屉锁等等.在Java中,我们的锁就是synchronized关键字和Lock接口. synchronized关键字 synchronized也叫同步锁,是Java里面的关键字.我们可以猜测到synchroni

JAVA并发之synchronized关键字

简介 synchronizaed关键字是JAVA阻塞同步(互斥同步)中最常用的一种方式,使用时将此关键字加到所需同步的代码块儿前即可,比如 int i = 0; synchronized (this){ i++; } synchronizaed同步方式在JAVA中是重量级加锁方式,下面来介绍一下它的工作原理,首先写一段代码: public class Sync { synchronized void syncMethod(){} public void add(){ synchronized (

Java并发之CyclicBarria的使用(二)

Java并发之CyclicBarria的使用(二) 一.简介 之前借助于其他大神写过一篇关于CyclicBarria用法的博文,但是内心总是感觉丝丝的愧疚,因为笔者喜欢原创,而不喜欢去转载一些其他的文章,为此笔者自己原创了一个CyclicBarria的用法的示例Demo, 在此声明,该Demo没有实际的价值,仅仅只是演示CyclicBarria的用法,希望加深读者对"循环栅栏"的用法加深理解. 二.使用 需求假设:在D盘下有一个test文件夹,我们要使用两个线程将文件夹A, B, 拷贝

Java使用QRCode.jar生成与解析二维码

正题:Java使用QRCode.jar生成与解析二维码demo 欢迎新手共勉,大神监督指正 # 不知道QRCode的请移步wiki,自行了解,这里不多做解释 *******创建二维码之前的工作******** 去下面给出的地址下载QRCode.jar包,此jar包已经包括 生成与解析 . 官网下载到的jar包是没有解析的 https://files.cnblogs.com/files/bigroc/QRCode.zip ***创建好你的测试类导好jar包开始吧*** 第一部分:生成二维码 pac

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

巨人大哥谈Java中的Synchronized关键字用法 认识synchronized 对于写多线程程序的人来说,经常碰到的就是并发问题,对于容易出现并发问题的地方价格synchronized基本上就搞定 了,如果说不考虑性能问题的话,这一操绝对能应对百分之九十以上的情况,若对于性能方面有要求的话就需要额外的知识比如读写锁等等.本文目的先了解透彻synchronized的基本原理. Synchronized的基本使用 Synchronized的作用主要有三个: (1)确保线程互斥的访问同步代码 

Java多线程:synchronized关键字和Lock

一.synchronized synchronized关键字可以用于声明方法,也可以用来声明代码块,下面分别看一下具体的场景(摘抄自<大型网站系统与Java中间件实践>) 案例一:其中foo1和foo2是SynchronizedDemo1类的两个静态方法.在不同的线程中,这两个方法的调用是互斥的,不仅是它们之间,任何两个不同线程的调用也互斥. public class SynchronizedDemo1 { public synchronized static void foo1(){} pu

java并发之synchronized详解

前言 多个线程访问同一个类的synchronized方法时, 都是串行执行的 ! 就算有多个cpu也不例外 ! synchronized方法使用了类java的内置锁, 即锁住的是方法所属对象本身. 同一个锁某个时刻只能被一个执行线程所获取, 因此其他线程都得等待锁的释放. 因此就算你有多余的cpu可以执行, 但是你没有锁, 所以你还是不能进入synchronized方法执行, CPU因此而空闲. 如果某个线程长期持有一个竞争激烈的锁, 那么将导致其他线程都因等待所的释放而被挂起, 从而导致CPU