从synchronized关键字谈起的一系列问题

若一个方法存在并发安全问题,用synchronized关键字解决最简单的同步问题:
public class Test {
    public static void lockTest(){
        synchronized (Test.class) {
            System.out.println("hello");
        }
    }
}
synchronized中参数可以是 对象或者,上面是静态方法,因此参数只能是字节码,通常我们也可以额外增加一个类当作锁。
那么synchronized锁的是什么?锁代码块?锁参数?
答案是 锁参数,依照上面代码就是对Test.class这个字节码对象上锁。
为什么是对参数上锁?可以看下面代码:
public class Test {
   static ReentrantLock reentrantLock=new ReentrantLock();
    public static void lockTest(){
        reentrantLock.lock();
            System.out.println("hello");
        reentrantLock.unlock();
    }
}
ReentrantLock(重入锁)是JUC并发包下提供的锁,synchronized是内置锁,上面代码ReentrantLock同样实现了对代码块的同步,它的含义是对reentrantLock这个对象进行上锁,其源码是通过对该类的一个state属性进行操作的:
/**
 * The synchronization state.
 */
private volatile int state;
调用lock将state修改为1,则上锁成功;
调用unlock将state修改为0,则解锁成功;
因此我们分析,如果我们给对象加锁,是不是就意味着这个对象内部必须要有一个 标识上锁成功与否的属性?可是我们在最开始用synchronized进行加锁,Test.class里并没有声明一个标识呀!
于是问题出现了,用synchronized进行加锁改变了锁对象的什么?
答案是改变对象的对象头
那么什么叫对象头?
由于Java面向对象的思想,在JVM中需要大量存储对象,存储时为了实现一些额外的功能,需要在对象中添加一些标记字段用于增强对象功能,这些标记字段组成了对象头。
因此还要学习Java的对象布局,也即Java对象的组成。
我们都知道对象存储在堆上,那么对象在堆上到底要分配多少内存呢?
public class Test {
  boolean flag=false;//占 1byte
}
我们实例化上面的对象,JVM会为它分配多少内存呢?1Byte吗?
首先需要明确一个知识点:64位的虚拟机规定对象大小是8字节的整数倍
因此我们猜想,一个对象分配的内存绝不仅限于定义的属性大小,还要填充写些我们难以看到的东西。
一个对象可以由以下部分组成:
  • 对象的实例数据(定义的属性,如boolean,int等,的不固定的)
  • 对象头(大小固定)
  • 填充数据
读到这里就有个疑问.你怎么知道对象布局就是这三部分呢?
首先最开始我是读《深入理解JVM虚拟机》了解到这个概念的,接下来我会利用JOL工具去证明一下这个概念的正确性,其原理就是去查看一个实例对象在堆内存中的数据分布
1.引入工具依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol‐core</artifactId>
<version>0.9</version>
</dependency>
2.目标类
public class A {
    //没有任何数据
}
3.解析类
public class Test {
    public static void main(String[] args) {
        A a=new A();
        //解析对象a在JVM中的内存分配
        System.out.println(ClassLayout.parseInstance(a).toPrintable());
    }
}
4.结果
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)                           13 d3 d9 ef (00010011 11010011 11011001 11101111) (-270937325)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
分析我们暂且不谈,试着为目标类添一个属性后看看有什么不一样的结果:
public class A {
    boolean flag=false;//大小占1Byte
}
 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)                           8e 65 da ef (10001110 01100101 11011010 11101111) (-270899826)
     12     1   boolean A.flag                                    false
     13     3           (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total
结果很直观,两次结果都为这个实例对象分配了一个大小固定12字节的对象头(object header),第一次结果含有大小为4Byte的填充数据,而第二次结果因为有一个大小为1Byte的实例数据,为了满足虚拟机的对象大小分配规定,因此还要填充3Byte的数据。
思考一个问题,如果对象数据中只有一个int型的变量,还需要填充数据吗?
答案是不需要的,int类型数据占4字节,此时对象头+实例数据=16字节,已经是8的整数倍便不再进行数据填充,因此数据填充只是为了去满足JVM的对象内存分配规定
根据OFFSET偏移字段我们可以得到对象组成的一个位置布局:

到此我们已经证明且确定Java的对象布局的组成部分,可以有理有据和面试官吹牛B了 …
于是第二个问题接踵而来,既然对象头固定大小占12Byte,这可不小,那么它里面包含了什么内容?它又有什么作用呢?(不同位数的JVM对象头的长度不一样,这里指的是64bit的Jvm)
对象头是实现synchronized锁的重要部分,我们重点分析它的组成:
  • Mark Word
  • Klass Pointer
我又开始哔哔了,哎呀呀你怎么知道对象头就是这两个部分,证明一下呗!
首先明确两个概念:
1.当前使用的虚拟机是什么?
打开命令行,输入java -version,得到我当前使用的虚拟机是HotSpot。
2.什么是JVM?什么是HotSpot?什么是OpenJDK?它们有什么关联?
JVM是一种规范标准,HotSpot是根据这种规范标准实现的产品,OpenJDK是HotSpot中部分开源的源码。
因此我们只需要在JVM中寻找对象头的定义规范,即可知道它的组成部分。
引用OpenJDK文档当中对对象头的解释:
Object Header

Common structure at the beginning of every GC-managed heap object. (Every oop points to an object header.) Includes fundamental information about the heap object’s layout, type, GC state, synchronization state, and identity hash code. Consists of two words. In arrays it is immediately followed by a length field. Note that both Java objects and VM- internal objects have a common object header format

上述引用中提到一个java对象头包含了2个word(数组对象除外,数组对象的对象头还包含一个数组长度),并且好包含了堆对象的布局类型GC状态同步状态标识哈希码,具体怎么包含的呢?又是哪两个word呢?
1.Mark Word

The first word of every object header. Usually a set of bitfields including synchronization state and identity hash code. May also be a pointer (with characteristic low bit encoding) to synchronization related information. During GC, may contain GC state bits.

mark word为第一个word,根据文档可以知他里面包含了锁的信息hashcodegc信息等等,拿GC举例,我们知道可见部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是最大15),最终如果还是存活,就存入到老年代,那么为什么是15次呢?
因为在mark word中分配4bit存放这个GC的复制次数信息,4bit的数据从0000到1111最多表示0~15个复制次数状态,此外诸如锁信息和hashcode等也会分配一定的bit去保存,在后面会进行探讨。
2.Klass Pointer

The second word of every object header. Points to another object (a metaobject) which describes the layout and behavior of the original object. For Java objects, the “klass” contains a C++ style “vtable”.

klass word为对象头的第二个word,主要指向对象的元数据(即对象属于哪个类)。klass word为对象头的第二个word,主要指向对象的元数据(即对象属于哪个类)
于是我们可以得出对象头的组成结构:

假设我们理解一个对象头主要上图两部分组成,在之前我们利用JOL打印的对象头信息知道一个对象头是12Byte,那这两部分的大小分别是多少呢?
//  64 bits:
//  --------
//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
//  size:64 ----------------------------------------------------->| (CMS free block)
我们从上面HotSpot的源码注释中得知一个Mark Word大小是64bit即8Byte,因此可得Klass Word的大小是4Byte
(有些博客或者教材会指明Klass Word的大小是8Byte,其实也没错,JVM默认开启指针压缩,未压缩前的Klass Word的大小就是8Byte)
接下来会对对象头这两部分包含的详细内容进行剖析,首先了解一下对象头中为对象的状态分配的五种类型:
  • 无锁
  • 偏向锁
  • 轻量级锁
  • 重量级锁
  • GC标记
为什么是这五种状态呢?怎么去表示这些状态?祭出下面这张未开启指针压缩时的对象头结构天命图:

难以看懂?没关系,我们先只需关注state(对象状态)lock(锁标志)biased_locak(偏向锁标志)这三个字段,在对象头中正是用着三个字段来表示对象的五种状态,简化后得到下图:

不难看出,无锁和偏向锁的锁标志位是相同的,因此引入了偏向锁标志位进行区分,其余三种状态只需要锁标志位区分即可。
学到这里便可以对开始的问题进行解答,使用synchronized对对象进行加锁,实际上是改变了对象头中锁的标志位信息
在天命图中我们看到,不同对象状态下的Mark Word中的结构是不同的,下面简单的对第一种无锁状态进行分析:
最开始我们使用JOL工具获得了一个空属性目标类的对象数据如下:
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)                           13 d3 d9 ef (00010011 11010011 11011001 11101111) (-270937325)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
是不是很奇怪?
(1)此时的这个对象刚刚实例化,没有被加锁或被GC标记,处于无锁状态,按照天命图其前25个bit应该是unused(未使用),但是其对象头的前25位如下,并不是全为0,显然被使用了?
00000001 00000000 00000000 00000000)
(00000000 00000000 00000000 00000000)
(00010011 11010011 11011001 11101111)
解答这个问题需要明确一个知识点,我们使用的CPU大都是英特尔的架构,采用的是小端存储,直白点也就是说这些数据和天命图的对应是倒过来的,unused(未使用)的25bit应该是倒数前25个,如下:
(00000001 00000000 00000000 00000000)
(00000000 00000000 00000000 00000000
(00010011 11010011 11011001 11101111)
显然全为0没有被使用,按照这个逻辑可以取出锁标志位为01偏向锁标志位为0,对应的对象状态即为无锁。
(2)此外,按照天命图使用了如下的31个bit来存储对象的hashcode,但是我们会发现对应的二进制位全为0,难道它没有hashcode?
(00000001 00000000 00000000 00000000
00000000 00000000 00000000 00000000)
(00010011 11010011 11011001 11101111)
首先明确对象自身并不会去主动计算对象的hashcode,需要我们在代码中显性或隐性的计算出hashcode,才会自动保存在对象头中,将测试类修改为如下:
public class Test {
    public static void main(String[] args) {
        A a=new A();
        //显性计算对象hashcode,并转为十六进制
        System.out.println(Integer.toHexString(a.hashCode()));
        //解析对象a在JVM中的内存分配
        System.out.println(ClassLayout.parseInstance(a).toPrintable());
    }
}
6422b8ff
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 ff b8 22 (00000001 11111111 10111000 00100010) (582549249)
      4     4        (object header)                           64 00 00 00 (01100100 00000000 00000000 00000000) (100)
      8     4        (object header)                           75 11 da ef (01110101 00010001 11011010 11101111) (-270921355)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
(00000001 11111111 10111000 00100010)
(01100100 00000000 00000000 00000000)
(01110101 00010001 11011010 11101111)
可以看到,存储hashcode的31位已经发生了变化,转化为十六进制后很直观的看出与我们的hashcode的值是相同的。

转载请注明原出处,欢迎到我的个人博客查看更多内容。

http://www.coolblog.online

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
synchronized关键字Java中用于实现线程同步的关键字。它可以用来修饰方法或代码块,以确保在多线程环境下的安全性。 synchronized关键字的使用有以下几点需要注意: 1. synchronized是一种重量级的操作,会影响性能。因此,在使用synchronized时应尽可能减小同步块的范围,避免锁的竞争。 2. synchronized锁的范围应尽量小,只保护必要的代码块,避免对整个方法或对象进行锁定。这样可以提高程序的并发性能。 3. synchronized锁定的对象不应该被修改,否则可能会导致死锁的发生。因此,在使用synchronized时需要谨慎处理锁定的对象。 4. 在使用synchronized时,需要考虑线程间的协调和通信,以避免死锁和活锁的发生。这可以通过合理设计程序逻辑和使用其他同步机制来实现。 总的来说,synchronized关键字是一种常用的线程同步机制,可以确保在多线程环境下的数据安全性和一致性。但是在使用时需要注意性能问题和锁的范围,以及避免死锁和活锁的发生。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [Synchronized 关键字详解](https://blog.csdn.net/swadian2008/article/details/99328700)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [举例讲解Javasynchronized关键字的用法](https://download.csdn.net/download/weixin_38724611/12798175)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mushroom-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值