关于锁的四种状态分析

背景

  1. 在使用多线程场景下,大部分同学都知道使用synchronized关键词来加锁实现多线程下的数据一致性的保证,但是在jdk6以前,synchronized锁是一个十分重量级的锁,之所以说是十分重量级是因为通过synchronized加锁jdk会向操作系统申请锁,锁是操作系统中一种十分核心的资源,可以理解为一个操作系统的锁数量是有限的,当我们每次都去申请操作系统的锁是对锁资源的一种巨大的浪费,另外一方面,大家都知道操作系统分为用户态和内核态,用户态是应用程序所在的一种状态,包括jdk和我们的java程序其实都是运行在用户态的应用程序,而操作系统的锁是需要在内核态才可以使用,这时候就涉及到了用户态升级内核态的转变,这种转变是十分消耗性能和资源的,所以对我们的应用程序性能也是巨大的拖累
  2. 幸运的是,在jdk6的时候,jdk内部对synchronized做了大量的优化,引入了偏向锁、自旋锁等新锁的加入,在开始的时候锁只会存在偏向锁、自旋锁较为轻量级的锁,如果在轻量级的锁无法搞定问题的情况下才会升级为重量级锁,重量级锁就是操作系统的锁,在大部分情况下偏向锁和自旋锁即可解决大部分问题,避免了操作系统锁资源的浪费,也为我们的应用程序性能带来了巨大的提升

工具准备

在分析锁的时候我们需要JOL工具

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

准备工作

在开始分析锁的四种状态前,我们需要先了解对象在内存中的布局,我的操作系统为64位,以64为操作系统为例,我们先分析对象的布局
image.png
以上为对象在内存中的布局分布,分为对象头、实例数据和对齐填充

  • 对象头:包含mark word和class pointer,mark word可以理解为对对象的一些信息的标记,后续我们会分析mark word中包含哪些信息
  • class pointer:对象指针,此指针指向为当前对象所属的class是哪个,在64位系统中,一般class pointer占用8个字节,但是由于jvm默认开启-XX:+UseCompressedClassPointers,此参数表示jvm开启class pointer压缩,class pointer长度会从原有8字节压缩为4字节
  • 数组长度:此项只有数组对象才存在
  • 实例数据:对象各种属性及其值
  • 对齐填充:为了保证内存的对齐,每个对象会占用8个字节的倍数,如果占用不够8的倍数,则会使用空数据填充至8字节的倍数,例如一个Id对象,其中包含了Integer类型的Id属性和Integer类型的age属性,我们可以分析出,mark word占用8字节,class pointer占用4字节,即对象头共占12字节,实例数据中,id占用4字节,age占用4字节,即实例数据共占用8字节,目前一共占用8+12=20字节,但是由于需要保证内存对齐,故需要填充4个空白字节,所以User对象一共占用了24个字节
public static void main(String[] args){
        Object o = new Object();

        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }

以上我们创建了一个空的Object对象,我们使用jol工具中的ClassLayout打印出内存布局

# 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
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

通过输出的日志我们可以看到,前三个为对象头(object header),每个header占用4个字节,共12个字节,12位置开始有4个字节描述为:loss due to the next object alignment,此4个字节为上述提到了对齐填充,所以Object对象占用了16个字节

public static void main(String[] args){
        Object o = new Object();

        System.out.println(ClassLayout.parseInstance(o).toPrintable());

        synchronized (o) {
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }

以上代码中,我们增加了synchronized锁,在增加锁之后重新输出内存布局

# 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
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)                           c0 09 f7 06 (11000000 00001001 11110111 00000110) (116853184)
      4     4        (object header)                           00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
      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


Process finished with exit code 0

可以看到第一个object header发生了变化,即mark word发生了变化,所以我们可以确认,锁标记记录在对象的mark word中

【注】:其实在mark word中包括以下信息:锁的状态、identity hashcode、GC信息

锁的四种状态

简单的来说,锁一共分为四个状态:new、偏向锁、轻量级锁(又称无锁、自旋锁、自旋适应锁)、重量级锁
在这里插入图片描述

【注】:hasCode仅会在调用hashCode()方法时才会生成,默认为空,可以调整jvm参数设置初始生成
new:初始创建对象状态
偏向锁:jdk6后使用synchronized加锁后默认是偏向锁,其中54位用于存储当前线程的指针
轻量级锁(无锁、自旋锁、自旋适应锁):当有多个线程开始竞争此资源的时候,偏向锁自动升级为轻量级锁,所谓轻量级锁其实就是CAS,通俗的解释,轻量级锁相当于给门上锁,至于在门里面的过程并不关心,通过循环不停的判断本线程能否上锁,如果可以上锁则加锁成功,不能上锁则接着循环,每次循环数量+1,当抢夺锁失败次数达到一定的数量后,轻量级锁升级为重量级锁【注:当有多个线程抢夺资源时,每个线程会生成一个Lock Record,轻量级锁中保存的是指向加锁成功的线程的Lock Record指针,相当于给门加锁贴标签】
重量级锁:重量级锁是操作系统级别锁,需要从用户态升级到内核态来加锁,锁的资源也是有限的,对性能会造成大幅度影响,当升级为重量级锁后,会有一个线程对资源加锁成功,其余线程进行队列等待,当加锁线程释放资源后通知队列中其他线程抢夺资源【注:重量级锁默认实现的是非公平锁,公平锁是FIFO,即队首线程优先获取锁,非公平锁是队列中所有线程同时抢夺,以实际抢夺到的线程为准】

锁降级

锁降级可以认为在特殊情况下发生,但是锁降级是没有任何意义的,因为锁降级说明没有任何线程抢夺此资源,则此资源会发生GC回收,既然已经开始执行GC回收资源了,那么此时降级锁也就没有任何意义了

锁粗化

锁粗化就是告诉我们任何事情都有个度,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗,对于一些极端情况,粗化锁,来提升加锁解锁开销,提升程序性能

一种场景:

public void doSomethingMethod(){
    synchronized(lock){
        //do some thing
    }
    //这是还有一些代码,做其它不需要同步的工作,但能很快执行完毕
    synchronized(lock){
        //do other thing
    }
}

粗化后的结果:

public void doSomethingMethod(){
    //进行锁粗化:整合成一次锁请求、同步、释放
    synchronized(lock){
        //do some thing
        //做其它不需要同步但能很快执行完的工作
        //do other thing
    }
}

另一种场景:

for(int i=0;i<size;i++){
    synchronized(lock){
    }
}

粗化后的结果:

synchronized(lock){
    for(int i=0;i<size;i++){
    }
}

【注】:锁粗化的前提是中间不需要同步的代码能够很快速地完成,如果不需要同步的代码需要花很长时间,就会导致同步块的执行需要花费很长的时间,这样做也就不合理了

锁消除

我们都知道字符串拼接中,StringBuffer是线程安全的,StringBuilder是非线程安全的,是因为StringBuffer中的方法使用了synchronized修饰,但是在某些情况下加锁其实是没有必要的,jdk会自动执行优化

public void test(){
    StringBuffer sb = new StringBuffer();
    sb.append("a").append("b").append("c");
}

上述方法中,每次调用append方法的时候都需要加锁-解锁,极大的消耗资源,在java中,如果检测到StringBuffer对象仅在此方法中使用,则会消除append中的锁,从而提升效率

小短腿的个人公众号上线啦,后续技术分享将优先在公众号进行哈,大家可以关注公众号,小短腿有什么新的分享会及时同步哈
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值