通过内存布局带你掌握锁升级过程

Synchronized四种锁状态

在 Java 语言中,使用 Synchronized 是能够实现线程同步的,即加锁。并且实现的是悲观锁,在操作同步资源的时候直接先加锁。
加锁可以使一段代码在同一时间只有一个线程可以访问,在增加安全性的同时,牺牲掉的是程序的执行性能,所以为了在一定程度上减少获得锁和释放锁带来的性能消耗,在 jdk6 之后便引入了“偏向锁”和“轻量级锁”,所以总共有4种锁状态,级别由低到高依次为:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。这几个状态会随着竞争情况逐渐升级。

内存布局

要想清晰地了解锁升级的过程,首先需要我们掌握内存布局,很多公司会问到这样一个问题: Object o = new Object(); 对象o占多少字节?

这里我们首先给出对象的内存布局图。

在这里插入图片描述
可以看出内存布局有三个部分:对象头,实例数据,对齐。

对象头:

HotSpot虚拟机的对象头主要包括两部分信息:

第一部分:用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,这部分数据的长度在32位和64位的虚拟机(暂 不考虑开启压缩指针的场景)中分别为32个和64个Bits,官方称它为“Mark Word”

第二部分:类型指针,即对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

其他部分:如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。

在64位JVM上有一个压缩指针选项-XX:+UseCompressedOops,默认是开启的。开启之后Class Pointer部分就会压缩为4字节,此时对象头大小就会缩小到12字节。

实例数据
实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。

对齐
这里的对齐填充专指对象末尾的填充,如果对象填充完实例数据后的大小不满足"8N",则填充到8N,其实在上面运行结果中就已经有提示了。

内存布局实验

为了查看内存布局,需要下载第三方依赖JOL(Java Object Layout)

<dependencies>
        <!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.9</version>
        </dependency>
 </dependencies>

实验一:

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

输出:

java.lang.Object 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)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以直观的看到
对象头:

  • Mark Word 8字节
  • 类型指针 4字节

实例数据:0字节
对齐:4字节
因此总共占16字节
说明:对象头+实例数据 = 12字节不为8的倍数,需对齐补4个字节。

实验二:
People 对象新增两个int类型的变量(name,age)

@Data
public class People {
    int name;
    int age;
}

People o = new People();   
System.out.println(ClassLayout.parseInstance(o).toPrintable());

输出:

com.example.demo.People 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 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4    int People.name                               0
     16     4    int People.age                                0
     20     4        (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以看出多了两行

int People.name 
int People.age 

对象头:

  • Mark Word 8字节
  • 类型指针 4字节

实例数据:8字节(每个int类型各占4个字节)
对齐:4字节
总共占24个字节。

修改:如果把其中的一个int类型替换成long(8字节),结果还是占24个字节。

@Data
public class People {
    int name;
    long age;
}
 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 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4    int People.name                               0
     16     8   long People.age                                0
Instance size: 24 bytes

因为此时无需对齐。

锁状态与内存布局

大家需要掌握几种锁的概念,我曾在之前一篇文章形象的讲解了轻量级锁(cas),大家可以查看学习。

下图是这几种锁的比较。

锁状态优点缺点适用场景
偏向锁加锁解锁无额外消耗竞争线程多,会带来额外的锁撤销的消耗基本无锁竞争的同步场景
轻量级锁竞争线程不会阻塞,提高响应速度长时间自旋会造成cpu消耗少量锁竞争且线程持有锁时间不长
重量级锁不会导致cpu空转消耗资源线程阻塞,响应时间长大量线程竞争锁且锁持有时间长

上文说过锁相关信息都保存在对象头中的Mark Word中
重点
下图是Mark Word信息与锁状态对照表:
在这里插入图片描述
讲解:
第一行

  • 低三位:偏向锁位(iased_lock=0)代表锁为非偏向锁,它和偏向锁位(lock:01)组合表示锁状态为正常。
  • age:分代年龄
  • identity_hashcode(31位):hashcode

第二行

  • thread:线程信息

锁升级过程

锁升级的过程总结为:
new - 偏向锁 - 轻量级锁 (无锁, 自旋锁,自适应自旋)- 重量级锁
大家是不是一头雾水?没关系,我们通过下面的实验带你彻底掌握这张表以及锁升级的过程。

实验要进行以下三步:

  • new一个新对象,打印内存布局。
  • 进行hashCode操作,打印内存布局。
  • 上锁,打印内存布局。
public static void main(String[] args) {
    Object o = new Object();
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
    o.hashCode();
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
    synchronized (o){
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

输出:

java.lang.Object 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)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 22 7f 63 (00000001 00100010 01111111 01100011) (1669276161)
      4     4        (object header)                           07 00 00 00 (00000111 00000000 00000000 00000000) (7)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           b8 f5 4f 03 (10111000 11110101 01001111 00000011) (55571896)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

上面有三个输出,我们需要把要分析的对象头中的Mark Word(前两行)逆序展开,举个例子:

(00000001 00000000 00000000 00000000) (1)
(00000000 00000000 00000000 00000000) (0)

展开后为

00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

由于Mark Word中最低的三位代表锁状态 其中1位是偏向锁位 两位是普通锁位,此处锁信息为001

让我们看看每一步都发生了什么吧~

1. Object o = new Object()
此时打印的Mark Word值为

00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

锁信息为0 01
我们查找上图Mark Word对照表,iased_lock=0,lock=01
查找结果为无锁态 。
此时:

unused: 00000000 00000000 00000000 0
hashcode: 0000000 00000000 00000000 00000000
unued: 0
age: 0000
biased_lock: 0
lock: 01

2. o.hashCode()
此时打印的Mark Word值为

00000000 00000000 00000000 00000111 01100011 01111111 00100010 00000001

锁信息仍然为0 01,不过此时有了hashcode

unused: 00000000 00000000 00000000 0
hashcode: 0000111 01100011 01111111 00100010
unued: 0
age: 0000
biased_lock: 0
lock: 01

3. 默认synchronized(o)
此时打印的Mark Word值为

00000000 00000000 00000000 00000000 00000011 01001111 11110101 10111000

之前我们讲的锁升级顺序是先是偏向锁之后才会升级为轻量级锁
然而此时最低三位为 000(轻量级锁)
问号一:为什么不是偏向锁呢?
因为默认情况下偏向锁有个时延,默认是4秒,所以最先看到的是轻量级锁,如果要观察到偏向锁,应该设定启动参数

-XX:BiasedLockingStartupDelay=0

问号二:为什么要先为轻量级锁?
因为JVM虚拟机自己有一些默认启动的线程,里面有好多sync代码,这些sync代码启动时就知道肯定会有竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁撤销和锁升级的操作,效率较低。
不过有的JDK如JDK11,打开就是偏向锁,无需进行额外的参数设置。

如果你在Idea设置了启动参数,还是000(轻量级锁),是因为你已经计算过了对象的hashCode,则对象无法进入偏向状态!**

4. 如果设定上述参数成功
我们再来看下内存布局

00000000 00000000 00000000 00000000 00000011 01000111 01001000 00000101 

可以看到低3位为101(偏向锁)

5. 如果有线程上锁
上偏向锁,指的就是,把Mark Word的线程ID改为自己线程ID的过程
偏向锁不可重偏向 批量偏向 批量撤销

6. 一旦出现线程竞争
撤销偏向锁,升级轻量级锁
线程在自己的线程栈生成LockRecord ,用CAS操作将Mark Word设置为指向自己这个线程的LR的指针,设置成功者得到锁

7. 如果竞争加剧

有线程超过10次自旋或者自旋线程数超过CPU核数的一半,升级重量级锁:向操作系统申请资源,执行linux 互斥锁mutex ,线程挂起,进入等待队列,等待操作系统的调度,然后再映射回用户空间。

自旋次数可以在启动参数中进行调整

 -XX:PreBlockSpin

JDK1.6之后,加入自适应自旋 Adapative Self Spinning , JVM自己控制
自适应自旋锁意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

偏向锁由于有锁撤销的过程revoke,会消耗系统资源,所以,在锁争用特别激烈的时候,用偏向锁未必效率高。还不如直接使用轻量级锁。

其他概念

锁消除 lock eliminate
public void add(String str1,String str2){
         StringBuffer sb = new StringBuffer();
         sb.append(str1).append(str2);
}

我们都知道 StringBuffer 是线程安全的,因为它的关键方法都是被 synchronized 修饰过的,但我们看上面这段代码,我们会发现,sb 这个引用只会在 add 方法中使用,不可能被其它线程引用(因为是局部变量,栈私有),因此 sb 是不可能共享的资源,JVM 会自动消除 StringBuffer 对象内部的锁。

锁粗化 lock coarsening
public String test(String str){
       
       int i = 0;
       StringBuffer sb = new StringBuffer():
       while(i < 100){
           sb.append(str);
           i++;
       }
       return sb.toString():
}

JVM 会检测到这样一连串的操作都对同一个对象加锁(while 循环内 100 次执行 append,没有锁粗化的就要进行 100 次加锁/解锁),此时 JVM 就会将加锁的范围粗化到这一连串的操作的外部(比如 while 虚幻体外),使得这一连串操作只需要加一次锁即可。

             **更多技术文章,请扫码关注微信公众号“云计算平台技术”**

在这里插入图片描述

             **如果看完这篇文章后您还没有升职加薪,也可以扫码关注公众号骂我哦~**
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Windows系统中,内存布局可以分为以下几个部分: 1. 内核空间(Kernel Space):这是操作系统内核和驱动程序所使用的内存空间。它的大小通常为2GB或更高,这取决于Windows系统的版本和配置。在这个空间中,内核可以直接访问硬件和其他资源。 2. 用户空间(User Space):这是用户进程所使用的内存空间。它通常占据了进程地址空间的大部分,并由虚拟内存机制管理。在这个空间中,用户进程可以访问自己的代码和数据,以及共享库和系统资源等。 3. 内存映射文件(Mapped Files):这是一种特殊的内存映射,它将文件映射到进程的地址空间中。这使得进程可以像访问内存一样访问文件内容。在Windows系统中,内存映射文件通常用于实现共享内存区域和动态链接库等。 4. 堆(Heap):这是一个动态分配的内存区域,用于进程中的动态数据结构和变量。在Windows系统中,堆由Heap Manager管理,支持动态分配和释放内存,以及内存池和垃圾收集等。 5. 栈(Stack):这是用于函数调用和局部变量的一种内存区域。在Windows系统中,每个线程都有自己的栈,用于存储函数参数、返回地址和局部变量等。 6. 内存管理结构(Memory Management Structures):这是Windows系统用于管理内存数据结构和算法。包括虚拟内存管理机制、页面替换算法、内存分配器、进程内存管理和系统内存管理等。 在Windows系统中,内存布局的具体实现会根据系统版本、硬件配置和应用程序需求等因素而有所不同。但以上这些部分通常都是包含在内存布局中的。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值