Java锁synchronized关键字学习系列之CAS和对象头

Java锁synchronized关键字学习系列之CAS和对象头

关于java锁的问题,一直想找时间学习一下,但是去年一整年包括今年都太颓了感觉工作久之后还有经历了一些不开心的事情之后学习的热情就不是很足了。但是人不学习就会退步,所以还是坚持一下吧。不说这么多废话了…开始吧。

谈到java锁,肯定是学习java的每个人都绕不开的问题。一谈到并发编程很自然的就会想到锁。本文只是简单粗略的去了解和学习关于Synchronized关键字的一些简单应用,比如涉及到的对象头,锁升级之类的。并没有涉及到非常深入底层的实现。毕竟我个人目前能力有限,而且篇幅会比较长,所以这篇文章只能说是Synchronized的简单了解和学习。

Java锁还可以按照底层实现分为两种。一种是由JVM提供支持的Synchronized锁,本文主要探讨的就是Synchronized关键字。另一种是JDK提供的以AQS为实现基础的JUC工具,如ReentrantLock,ReadWriteLock,以及CountDownLatch,Semaphore,CyclicBarrier等。这些如果以后有机会当然也会深入的去学习和记录(又是一个坑)。

锁的分类和定义

  1. 自旋锁:是指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环(自旋锁其实是基于CAS的一种锁实现,通过不断循环的做CAS操作。可能有的小伙伴不知道啥是CAS,一会下文会简单的解释一下)
  2. 乐观锁:假定没有冲突,再修改数据时如果发现数据和之前获取的不一致,则读最新数据,修改后重试修改(乐观锁的核心操作就是CAS)
  3. 悲观锁:假定一定会发生并发冲突,同步所有对数据的相关操作,从读数据就开始上锁(Synchronized就是悲观锁
  4. 独享锁:给资源加上独享锁,该资源同一时刻只能被一个线程持有(如JUC中的写锁)
  5. 共享锁:给资源加上共享锁,该资源可同时被多个线程持有(如JUC中的读锁)
  6. 可重入锁:线程拿到某资源的锁后,可自由进入同一把锁同步的其他代码(即获得锁的线程,可多次进入持有的锁的代码中,如Synchronized就是可重入锁
  7. 不可重入锁:线程拿到某资源的锁后,不可进入同一把锁同步的其他代码
  8. 公平锁:争夺锁的顺序,获得锁的顺序是按照先来后到的(如ReentrantLock(true))
  9. 非公平锁:争夺锁的顺序,获得锁的顺序并非按照先来后到的(如Synchronized就是非公平锁)

参考:Java锁-Synchronized深层剖析

CAS

前面提到了CAS操作,那我们这里简单提一个概念CAS,全名就是CompareAndSwap,比较并替换,是一种实现并发算法时常用到的技术。这里涉及到的就是两个操作,比较然后再交换,跟乐观锁的操作是一样的,所以乐观锁的核心操作其实就是CAS。

CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B

比如你要操作一个变量,他的值为A,你希望将他修改为B,这期间不会进行加锁,当你在修改的时候,你发现值仍旧是A,然后将它修改为B,如果此时值被其他线程修改了,变成了C,那么将不会进行值B的写入操作,这就是CAS的核心理论,通过这样的操作可以实现逻辑上的一种“加锁”,避免了真正去加锁 。

当然CAS也存在一些问题比如ABA的问题,还有一些比较深入的知识,这里就不花太多篇幅去讲述了,以后有机会可能会专门去深入学习一下(又是一个坑)。如果有特别感兴趣的小伙伴可以参考一下其他资料。

参考:

锁开销优化以及 CAS 简单说明

原子操作CAS(Compare And Swap)

CAS原理分析及ABA问题详解

CAS原理分析

synchronized关键字

前面都是铺垫了,现在开始我们来研究一下这个Synchronized关键字吧,为啥用这么一个关键字我们就可以实现锁呢?

首先我们要知道synchronized是悲观锁,独享锁,可重入锁。这些我们在前面锁的分类和定义也多少了解了。

然后我们还知道Synchronized的使用方式有很多种,可以看下图。这里参考了:并发编程篇:synchronized的使用以及原理

在这里插入图片描述

那我们现在可以回答这两个问题了,synchronized关键字锁的是什么?锁的是代码还是锁的是对象?

答:锁的是对象

对象头

既然我们知道锁的是对象,那么我们就想知道锁是如何存储的?

我们知道一切皆对象。锁就是一个对象,那么这个对象里面的结构是怎么样的呢,锁对象里面都保存了哪些信息呢?

在Hotspot 虚拟机中,对象在内存中的存储布局,可以分为三个区域:

  1. 对象头(Header)
  2. 实例数据(Instance Data)
  3. 对齐填充(Padding)

那么锁的信息存在那个地方呢?我们直接给结论吧,锁的信息是存放在对象头里的。

那什么是对象头呢?我们可以引用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.

然后对象头里面包含了两个部分的信息,第一部分是mark word,第二部分是klass pointer

具体的解释我们也引用OpenJDK的专业术语的原文

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.

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

简单总结就是Mark Word,用于存储自身的运行时数据,如:HashCode,GC分代年龄,锁标记、偏向锁线程ID等
,所以我们说的锁信息就是记录在对象头中的Mark Word。而Klass Pointer是类型指针,即对象指向它的类元信息,虚拟机通过这个指针来确定这个对象是哪个类的实例。

参考:http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html

Mark Word

那接下来我们肯定是要研究一下Mark Word的结构了,在32位的虚拟机和64位的虚拟机中存储的结构不太一样。

32位虚拟机中:

在这里插入图片描述

64位虚拟机中:

在这里插入图片描述

|-----------------------------------------------------------------------------------------------------------------|
|                                             Object Header(128bits)                                              |
|-----------------------------------------------------------------------------------------------------------------|
|                                   Mark Word(64bits)               |  Klass Pointer(64bits) |      State         |
|-----------------------------------------------------------------------------------------------------------------|
| unused:25|identity_hashcode:31|unused:1|age:4|biase_lock:1|lock:2 | OOP to metadata object |      Nomal         |
|-----------------------------------------------------------------------------------------------------------------|
| thread:54|      epoch:2       |unused:1|age:4|biase_lock:1|lock:2 | OOP to metadata object |      Biased        |
|-----------------------------------------------------------------------------------------------------------------|
|                     ptr_to_lock_record:62                 |lock:2 | OOP to metadata object | Lightweight Locked |
|-----------------------------------------------------------------------------------------------------------------|
|                    ptr_to_heavyweight_monitor:62          |lock:2 | OOP to metadata object | Heavyweight Locked |
|-----------------------------------------------------------------------------------------------------------------|
|                                                           |lock:2 | OOP to metadata object |    Marked for GC   |
|-----------------------------------------------------------------------------------------------------------------|

通过上面的图我们可以知道64位的虚拟机中Mark Word占了64bits,也就是8个字节,Klass Pointer占了64bits也是8个字节。

现在我们普遍都是使用64位的虚拟机了,所以我们这里主要以64位虚拟机为主。

我们现在来看看一个对象的对象头是长怎样的吧?验证一下是不是我们上面看到的结果。

我们可以借助jol包来看看一个实例的对象头是怎样的。

首先我们来引入相关依赖

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

然后接下来我们就可以分析一个对象的实例了,我们首先分析第一个对象

class A {
    private boolean flag;
}
 public static void main(String[] args) {
        //===================开启指针压缩的结果
        //Klass是64bits(8个字节)但是这儿只有4个字节,是因为我们开启了指针压缩
        A a = new A();
        System.out.println(ClassLayout.parseInstance(a).toPrintable());//有对齐填充
        /*
        org.example.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     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
         */
}

在这里插入图片描述

我们可以设置虚拟机参数-XX:-UseCompressedOops来关闭指针压缩,然后再来看看结果。

在这里插入图片描述

通过上面我们可以知道一个对象中确实存在三部分:对象头(Header), 实例数据(Instance Data),对齐填充(Padding)。但是这三部分都是必须的吗?

我这里直接给出解答吧,对象头一定有,但是实例数据和对齐填充数据有可能没有。

我这里直接给出例子还有一些打印出来的对象头结果,看一看大家就知道了。

class A {
    private boolean flag;
}

class B {
    private int count;
}

class C {

}
public static void main(String[] args) {
        //===================开启指针压缩的结果
        //Klass是64bits(8个字节)但是这儿只有4个字节,是因为我们开启了指针压缩
        A a = new A();
        System.out.println(ClassLayout.parseInstance(a).toPrintable());//有对齐填充
        /*
        org.example.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     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
         */

        B b = new B();
        System.out.println(ClassLayout.parseInstance(b).toPrintable());//没有对齐填充
        /*
        org.example.B 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)                           e0 f2 0e 00 (11100000 11110010 00001110 00000000) (979680)
             12     4    int B.count                                   0
        Instance size: 16 bytes
        Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
         */


        C c = new C();
        System.out.println(ClassLayout.parseInstance(c).toPrintable());//没有实例数据
        /*
        org.example.C 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)                           e8 74 0e 00 (11101000 01110100 00001110 00000000) (947432)
             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
         */



        //============关闭指针压缩的结果 -XX:-UseCompressedOops

        /*
        org.example.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)                           90 5c f1 e9 (10010000 01011100 11110001 11101001) (-370058096)
             12     4           (object header)                           f6 01 00 00 (11110110 00000001 00000000 00000000) (502)
             16     1   boolean A.flag                                    false
             17     7           (loss due to the next object alignment)
        Instance size: 24 bytes
        Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

        org.example.B 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)                           a0 df 5a eb (10100000 11011111 01011010 11101011) (-346366048)
             12     4        (object header)                           f6 01 00 00 (11110110 00000001 00000000 00000000) (502)
             16     4    int B.count                                   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

        org.example.C 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)                           00 e4 5a eb (00000000 11100100 01011010 11101011) (-346364928)
             12     4        (object header)                           f6 01 00 00 (11110110 00000001 00000000 00000000) (502)
        Instance size: 16 bytes
        Space losses: 0 bytes internal + 0 bytes external = 0 bytes total


        Process finished with exit code 0


         */

    }

接下来我们就可以回答几个问题了。

  1. 锁的是对象的话,那么改变了对象中的什么?
    答:改变了对象的对象头。

  2. 对象的大小是固定的吗?
    答:不是固定的,如果是固定的话,就不会有内存溢出的问题了。

  3. 对象的组成部分?
    答:对象的实例数据,大小是不固定的;对象的对象头,大小的固定的。还有一个是对齐数据(可有可无,64位的JVM,对象的大小是8 byte的整数倍,不够会填充对齐的数据)

我们继续研究一下Mard word的部分,看那看那Mard word还能存放些什么?

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

结果:

java.lang.Object 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)                           00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

74ad1f1f
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 1f 1f ad (00000001 00011111 00011111 10101101) (-1390469375)
      4     4        (object header)                           74 00 00 00 (01110100 00000000 00000000 00000000) (116)
      8     4        (object header)                           00 10 00 00 (00000000 00010000 00000000 00000000) (4096)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

在这里插入图片描述

这里简单说明一下匿名偏向锁。

当JVM启用了偏向锁模式(JDK6以上默认开启),新创建对象的Mark Word中的Thread Id为0,说明此时处于可偏向但未偏向任何线程,也叫做匿名偏向状态(anonymously biased)。

我们可以通过虚拟机参数去关闭掉这个偏向锁模式, 运行时带上命令-XX:-UseBiasedLocking
然后我们来看看结果。

在这里插入图片描述

关于偏向锁和匿名偏向状态可以参考一下:

偏向锁

(二)偏向锁详解

之前我们介绍Mard word中会存放hashCode,但是当我们new一个Object的时候,并没有看到哪里存放了hashCode,那我们继续看看调用了o.hashCode()发生了什么,这个hashCode是存放在哪里的?

在这里插入图片描述

我们就不继续深入研究了,本人能力也有限哈哈,上面这些只是为了告诉大家,Mard word在不同的锁标志位下,他存放的数据不同,这样可以最大有效的利用。当然有错误的地方也希望大家能指出,这只是本人学习后的结果。

篇幅有限,本文主要简单入门和介绍一下CAS和对象头,《Java锁synchronized关键字学习系列》会继续更新,之后会学习一下关于锁升级。

源代码

https://gitee.com/cckevincyh/java_synchronized_learning

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值