初学Synchronized

在了解synchronize之前,我们需要了解

  • 用户态和内核态
    用户态:用户应用程序的操作
    内核态:操作系统执行的操作

  • CAS
    CAS是compare and swap的简称,中文翻译是比较并交换。使用CAS可以实现不加锁进行单线程读写操作。
    比如:现在需要对一个数字进行操作,但是程序不想加synchronize关键字,也就是说不想加锁,这时候可以使用CAS进行操作。CAS将需要操作的数字读到自己的线程中进行操作,进行操作完(比如+1)之后,再把数据返回,而在返回的过程中读取原数据是否还是之前的数据,如果是则说明没有线程修改过,可以把数据返回;如果被修改过则不断进行这个过程,直到操作成功为止。
    但是在CAS中会存在一个问题:ABA问题。有点类似于狸猫换太子的感觉。。。。。。。
    在这里插入图片描述
    什么是ABA问题呢:我们在进行CAS的过程中,原数据被其他线程拿走用完之后再返回回来,此时CAS操作完之后在读取原数据是否一致时,CAS觉得是一样的,但是,数据却被用过了。
    这时,为了避免这种狸猫换太子的事情发生,我们可以加上版本号,解决这个问题。

OK,讲完这两个知识点,现在开始上干货
在这里插入图片描述

解析synchronize

我们通常会给一个类或者方法加上synchronize关键字,可是java中是怎么识别一个类或方法是否被上锁了的呢?
我们需要先了解一下java对象的组成
我这里用的是springboot,属于maven项目,在依赖中加入

<!-- 引入jol依赖 -->
        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.10</version>
        </dependency>

现在最新的版本是0.10,,在代码中:

import org.openjdk.jol.info.ClassLayout;

public class test {
    public static void main(String[] args) {
        Demo demo = new Demo();

        System.out.println(ClassLayout.parseInstance(demo).toPrintable());
    }
}
class Demo{
    private int a;
    private String b;
}

这样可以很简单的输出对象的组成:

 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)                           4b 28 17 00 (01001011 00101000 00010111 00000000) (1517643)
     12     4                int Demo.a                                    0
     16     4   java.lang.String Demo.b                                    null
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

依照这个可以很清楚的看到,对象是由对象头、对象的实例数据、数据对齐(最后的loss due to the next object alignment)这三部分组成。JVM 64位虚拟机规定,对象大小必须为8的倍数,如果对象头加实例数据是8的倍数,则没有对齐数据。在这里,对象头加实例数据大小为12+8=20,所以补足4位为24,是8的倍数。
在对象头、对象的实例数据、数据对齐三部分中,实例数据和数据对齐没啥好研究的,要研究的是对象头。

对象头解析
说什么都不如官方文档来的实在:
OpenJDK hotspot官方文档
官方文档中规定:

对象头
每个GC管理的堆对象开头的通用结构。(每个oop都指向一个对象标头。)包括有关堆对象的布局,类型,GC状态,同步状态和标识哈希码的基本信息。由两个词组成。在数组中,紧随其后的是长度字段。请注意,Java对象和VM内部对象都具有通用的对象标头格式。
Mark Word:
每个对象标头的第一个单词。通常,一组位域包括同步状态和标识哈希码。也可以是指向与同步相关的信息的指针(具有特征性的低位编码)。在GC期间,可能包含GC状态位。
klass pointer:
每个对象标头的第二个字。指向另一个对象(元对象),该对象描述原始对象的布局和行为。对于Java对象,“容器”包含C ++样式“ vtable”。

对象头是由Mark Word +klass pointer组成,上面的输出代码可以很明显的看出,对象头的长度为12byte,也就是96bit。其中Mark Word占8byte,klass pointer占4byte。这里借用马士兵网课的资料图来解析对象头中的Mark Word组成:
在这里插入图片描述
这张表详细的介绍了对象头Mark Word的组成。有点混乱的捋一捋,这里没有包括klass pointer,只是对象头Mark Word的组成,是64bit也就是8byte长度的数据。

 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)                           4b 28 17 00 (01001011 00101000 00010111 00000000) (1517643)
     12     4                int Demo.a                                    0
     16     4   java.lang.String Demo.b                                    null
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

输出数据可以对照着上面的解析表来看就非常的清晰明了。注意这里是倒序查看,因为是什么小端存储的关系,我没有深究。
因为倒序的关系,所以第一条数据00000101指的是 1bit unused+4bit 分代年龄+1bit 偏向位+2 bit锁标志位,所以最后3位101指的就是锁状态(当锁为自旋锁或者重量级锁或者被GC标记时只需要最后两位即可识别)。
仔细对照输出和马士兵的资料图,发现刚刚new出来的对象demo竟然是偏向锁?不是应该无锁001的吗?
在这里插入图片描述
其实我刚开始看到也是有点懵的。但是我就是要搞懂,查了很多其他文章之后:

在JVM刚启动时,会有很多容器加载或者初始化的操作,需要抢占资源,在这个过程中会使用大量synchronized关键字对对象加锁,且这些锁大多数都不是偏向锁。为了减少初始化时间,JVM默认延时加载偏向锁。这个延时的时间大概为4s左右

所以说,这个初始化的偏向锁是一种特殊状态的无锁,对照马士兵资料图我们可以看到输出的偏向锁中的54位当前线程指针和Epoch都是0(倒序查看,最前面的54位就是object header第二行最后往前数的54位都是0)。为啥是object header第二行往前数?因为对象头中的Mark Word是8byte,object header第三行是klass point,别搞混了

 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)                           4b 28 17 00 (01001011 00101000 00010111 00000000) (1517643)
     12     4                int Demo.a                                    0
     16     4   java.lang.String Demo.b                                    null
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

当前偏向锁并没有偏向任何线程,但是准备偏向了,这时我们加上synchronize关键词看看效果:

import org.openjdk.jol.info.ClassLayout;

public class test {
    public static void main(String[] args) {
        Demo demo = new Demo();
        synchronized (demo) {

        }
        System.out.println(ClassLayout.parseInstance(demo).toPrintable());
    }
}
class Demo{
    private int a;
    private String b;
}

输出:

com.onion.tcpclient.webControll.Demo object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           05 40 d3 0b (00000101 01000000 11010011 00001011) (198393861)
      4     4                    (object header)                           45 01 00 00 (01000101 00000001 00000000 00000000) (325)
      8     4                    (object header)                           4b 28 17 00 (01001011 00101000 00010111 00000000) (1517643)
     12     4                int Demo.a                                    0
     16     4   java.lang.String Demo.b                                    null
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以明显的看到54位当前线程指针和Epoch都不为0了,说明已经产生了偏向锁。

偏向锁的输出看到了,那还有自旋锁和重量级锁的呢?
在这里插入图片描述
这就要讲讲

锁升级

马士兵资料图
一个类new出来可能是普通对象或者是匿名偏向,我们之前new出来的demo就是匿名偏向,加上synchronize关键字之后升级为偏向锁。
偏向锁在发生轻度竞争时升级为自旋锁(轻量级锁),竞争再加剧(如耗时过长、wait等等)升级为重量级锁。
偏向锁和自旋锁是属于用户态的锁,而重量级锁需要向内核申请。
**竞争过程:**偏向锁把当前线程指针放到MarkWord里,当发生轻度竞争时,撤销偏向锁,升级为轻量级锁,每个线程都在自己的线程栈内部生成一个LR:lock record,使用自旋(自我旋转)的方式抢夺,失败的线程进行CAS操作,不断重复,如果竞争加剧向内核申请重量级锁。

ok,这篇文章到这里就结束了,如果你问我自旋锁和重量级锁的输出,你可以自己尝试多搞几个Treed模拟抢占的情况。
在这里插入图片描述
搞几个小知识点吧:
锁重入:
synchronize是可重入锁:比如 类N继承类M,M是synchronize,N也是,这时N调用super.m(),就是两重synchronize. 重入次数必须记录,因为要解锁几次必须要对应.
偏向锁、轻量级锁->记录在线程栈里->每重入一次LR+1(再增加一个LR)

自旋锁什么时候升级为重量级锁:
有线程超过十次自旋,或者自旋线程数超过CPU核数的一半,1.6之后,加入自适应自旋,jvm自动控制

为什么有自旋锁还需要重量级锁?
自旋是消耗cpu资源的(你一直旋转不累吗?),如果锁的时间长或者自旋线程多,cpu会被大量消耗
重量级锁有等待队列, 所有拿不到锁的进入等待队列, 不需要消耗cpu资源

偏向锁是否一定比自旋锁效率高?
不一定,在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及锁撤销,这时候直接使用自旋锁

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值