synchronized原理分析(一)

写在前面的话:大家好,我是small,初来乍到,请多多指教。相识是一种缘份,很高兴能在这里遇见你。希望这篇文章能给予你带来帮助,也希望在阅读之后能发表你的意见或给予纠正,感谢!

  • 本文概述:主要以synchronized为主,在此之前呢,希望能具备一个锁的概念,所以我提前写了一篇关于Java实现锁的demo,需要的可以去看看Java同步锁的实现
  • 本文难度:⭐ - ⭐⭐
  • 预计耗时 5 - 10 分钟
  • 运行环境
  1. idea
  2. jdk1.8(64位)
本文导读

通过本文你可以了解到Java对象的object header核心组成结构mark word、klass pointer,synchronized的变化也就处于mark word中,是这篇文章最重要的地方,没有之一。

synchronized的作用

synchronized是Java中解决高并发问题的一种常见方案,也是最简单的一种方法。作为synchronized,JVM为我们确保了以下三种特性:

  1. 原子性:确保代码在同一时间只有一个线程在执行(所谓先到先得)
  2. 可见性:确保共享变量的修改操作能够即使可见(就是拿锁前从主内存拿最新的,解锁前先刷回主内存)
  3. 有序性:避免指令重排序(一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作)

锁如何使用就不用多说了,想必大家都会了 😃( 无非就是锁对象、方法、代码块,对不对,快夸我),emm,接下来就不得不提对象头了…

对象头信息(JVM最喜欢的东西,没有之一)

为什么要提对象头信息?因为在我们自己的实现中,需要一个(Lock)类来控制代码的执行,也就是属于可见的(因为我能控制他)。

在synchronized中,用于控制实际执行的状态标志位,就位于对象头中,通常是不可见的,如下图,来自hotspot源码中注释(可能也看不清楚,┗|`O′|┛ 嗷~~)。

图来自hotspot源码中注释
看不清?问题不大,我翻译个你能看得懂的(图1)…

ud = unused=没有使用
bl=biased_lock=偏向标识
ep=epoch=偏向时间戳

对象头信息

上面的图,主要想表达Java对象有三种状态:

无锁状态、加锁状态、gc标记状态

其中最关心的也就是加锁状态,在加锁状态中可以看出大体分为三种状态,分别是:

偏向锁、轻量锁、重量锁

这三种锁的效率完全不同,只有在合理设计代码,才能合理的利用锁,那么这三种锁的原理是什么?

先喝口水,思考下…

想好了?(或许你就没想过,哈哈 😃 ),咳咳,我们继续…


对象的布局以及对象头

使用jol来分析java的对象布局,准备工作,先在maven(不一定非要maven)工程添加如下依赖:

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

然后,我们随便写一个类…

public class A {}//类里啥都没有...

再来个测试类…

import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;

public class testAJOL {
    static A a = new A();
    public static void main(String[] args) {
        System.out.println(VM.current().details());
     	System.out.println(ClassLayout.parseClass(A.class).toPrintable());
     	System.out.println(ClassLayout.parseInstance(a).toPrintable());
    }
}

稍等片刻…

输出结果如下

# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# WARNING | Compressed references base/shifts are guessed by the experiment!
# WARNING | Therefore, computed addresses are just guesses, and ARE NOT RELIABLE.
# WARNING | Make sure to attach Serviceability Agent to get the reliable addresses.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

com.small.pojo.A object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0    12        (object header)                           N/A
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

com.small.pojo.A 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)                           48 72 06 00 (01001000 01110010 00000110 00000000) (422472)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


Process finished with exit code 0

从上面的输出,可以发现header为12B,然后有4B为对其字节(因为JVM虚拟机上对象的大小必须是8的倍数),由于该类没有任何字段,所以实例数据为0B。因此,引出两个问题:

  1. 什么叫做对象的实例数据?
  2. 对象头中12B到底存的是什么呢?

问题1
我们先尝试往A类中添加一个基本数据,比如添加个int a;,int类型占用4个bit,然后我们再瞅瞅(ง •_•)ง

# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# WARNING | Compressed references base/shifts are guessed by the experiment!
# WARNING | Therefore, computed addresses are just guesses, and ARE NOT RELIABLE.
# WARNING | Make sure to attach Serviceability Agent to get the reliable addresses.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

com.small.pojo.A object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0    12        (object header)                           N/A
     12     4    int A.a                                       N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

com.small.pojo.A 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)                           48 72 06 00 (01001000 01110010 00000110 00000000) (422472)
     12     4    int A.a                                       0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total


Process finished with exit code 0

可以发现,添加了int a;之后,发现,字节没有发生变化,但是原来的对其字节没有了,而多出了 int A.a 初始值为0,这就是实例数据,占用4B(值得注意的是,如果再多添加一个int b;,那么对象头为12B,实例数据为8B,对其字节就为4B了)。

问题2

由于经过添加int a之后,发现12B并没有实际变化,通过这两次demo演示,可以发现,一个对象的布局基本由对象头、对象的实例数据、对其填充数据组成。

接下来,我们将目光放到这12B上面,在此之前呢,我先抛出个异常…咳咳…抛出个链接给你瞅瞅:HotSpot Glossary of Terms
我从里面贴出关键的部分,如下

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.

大致意思就是,在这个公共的结构中,任意一个受GC管理的且存在于堆上的对象(咳咳,这里不要钻牛角尖,虽然栈、TLAB上面也可以分配,这里主要讲的是堆,不要在意细节),在这个对象的开头公共部分,包含了这个对象在堆上的布局信息

  • type (这个对象的类型)
  • GC state (两个信息 分代年龄、是否被gc标记了)
  • synchronization state(是否被sync状态)
  • identity hash code(hashcode)

这些信息由两部分组成mark word 和 klass pointer(见图1),在上面两个demo输出结果中,可以看见header有两部分组成,长度为12B,mark word 8B 与klass pointer 4B(比例见图1)

 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)                           48 72 06 00 (01001000 01110010 00000110 00000000) (422472)

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”.

为当前这个对象指向堆(元空间)该对象本身类的class存在位置的首地址(也可以说是该对象的类的模板。
在header中,指针长度有可能是4(32bit),也有可能是8(64bit),主要看你有没有开启指针压缩

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中,主要包括了同步(sync)状态、hashcode以及gc状态,存储的内容,根据当前对象的状态有关。我们先来看未偏向(如下图)

ud = unused=没有使用
bl=biased_lock=偏向标识
ep=epoch=偏向时间戳

注意:未偏向时,有hash和无hash结构是一样的
在这里插入图片描述
意思就是说在长度为64bit的mark word中,前25个bit没有被用到,其次后面31个bit为hashcode,结合之前的demo来看,对吗???看起来好像不对啊,不要急,继续往下看

05 00 00 00 (00000101 00000000 00000000 00000000) (5)
00 00 00 00 (00000000 00000000 00000000 00000000)
48 72 06 00 (01001000 01110010 00000110 00000000) (422472)

这是在之前的输出结果,你会发现25bit + 31bit 长度为56bit,也就是说如果里面出现了hashcode,那就表示为hashcode,但是后面31bit都为0啊,该对象的hashcode总不能都为0吧?因此,得出了如下结论

当一个对象没有被调用hashcode()时,此时该对象是没有hashcode的,只有被调用了hashcode(),才会由jvm去计算该对象的hashcode,然后存储到该对象的header头中

是否真的是如此呢,我们来修改下之前的demo,修改为如下内容,然后再执行看看结果

public class TestJol {
    static A a = new A();
    public static void main(String[] args) {
	    //获取hashcode,同时转换成16进;
        System.out.println(Integer.toHexString(a.hashCode()));
        System.out.println(ClassLayout.parseInstance(a).toPrintable());
    }
}

结果输出

617c74e5
# WARNING: Unable to get Instrumentation. Dynamic Attach failed. You may add this JAR as -javaagent manually, or supply -Djdk.attach.allowAttachSelf
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
com.small.pojo.A object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 e5 74 7c (00000001 11100101 01110100 01111100) (2088035585)
      4     4        (object header)                           61 00 00 00 (01100001 00000000 00000000 00000000) (97)
      8     4        (object header)                           48 72 06 00 (01001000 01110010 00000110 00000000) (422472)
     12     4    int A.a                                       0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total


Process finished with exit code 0

是不是发现hashcode不为0了?(鼓掌,👏👏👏)

抛出个问题:你能从打印的hashcode在jvm输出的地址中找到规律吗?

hashcode : 617c74e5
jvm内存地址: 01 e5 74 7c 61 00 00 00 48 72 06 00

思考30秒…

我先喝口水…稍等…

找到了吗?找到了吗?如果你找到了,恭喜你,中奖啦(入坑成功),点击【大小端模式】领奖吧!(嘻嘻 (●’◡’●))

没找到的没关系,回头再找找…

推荐一篇 手动计算hashcode ,计算出来的值与直接调用hashcode()方法的值是一样的。


接下来,根据小端模式计算原则,可以推算出,剩下的8bit代表的含义
0 0000 0 01 分为四个段

  1. 0 没有被使用
  2. 0000 分代年龄
    年龄如何计算,在下一篇讲到
  3. 0 偏向标识
    当前对象是否可偏向,0不可偏向,1可偏向
  4. 01 锁状态
  1. 01 偏向锁(性能最佳)
  2. 00 轻量锁(性能一般)
  3. 10 重量锁(性能最差)
  4. 11 GC

在以上demo中,对象a锁状态是一个偏向锁,但是实际上并没有被偏向,因为在前56bit存储的是hashcode,所以a对象,目前并没有线程偏向他,所以是没有加锁的状态,那可不可以被偏向呢,由偏向标识来确定 ,在最开始的demo中有体现出来,注释掉调用hashcode这行代码(见问题1的demo),就可以发现后八位为00000101,此处的偏向标识为1,代表可以偏向。

如果你的执行结果依然是00000001,那么可以考虑关闭偏向延迟来解决这个问题。

为什么计算了hashcode就不可被偏向?

拿图1来举例
偏向与未偏向
从结构上来看,只有在前56位存在不同,后面均相同,也就是说,在一个对象计算了hashcode之后,那么前56位存储的是hashcode,也就无法再存放线程id,也就不可达到偏向的目的,只有在一个对象没有被计算hashcode时,才可以被偏向。
错误的案例:

	static A a = new A();
    public static void main(String[] args) {
        a.hashCode();
        synchronized (a){
            ...
        }
    }

接下来演示一个对象从无锁可偏向无hash到偏向锁的过程

 	static A a = new A();
 	
 	public static void main(String[] args) {
        System.out.println(ClassLayout.parseInstance(a).toPrintable());
        synchronized (a){
            System.out.println(ClassLayout.parseInstance(a).toPrintable());
        }
    }

输出结果

com.small.pojo.A 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)                           48 72 06 00 (01001000 01110010 00000110 00000000) (422472)
     12     4    int A.a                                       0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

com.small.pojo.A object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 98 00 cd (00000101 10011000 00000000 11001101) (-855599099)
      4     4        (object header)                           ee 7f 00 00 (11101110 01111111 00000000 00000000) (32750)
      8     4        (object header)                           48 72 06 00 (01001000 01110010 00000110 00000000) (422472)
     12     4    int A.a                                       0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total


Process finished with exit code 0

在上面的案例中,原本存储hashcode的地方,现存储的是当前偏向的线程id

思考 : 如果在获取锁之前,调用了hashcode()呢?


我们下期再会

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值