Synchronized关键字------------ 对象头

Java对象头的组成

java对象实例:对象头+对象实际数据+对齐填充

java对象头分为两部分, mark word 和  klass pointer。

mark word 包括: 25unused,对象hashcode31bit,1bit无符号位,分代年龄4bit,偏向锁1bit,锁 类型2bit。

klass pointer存储对象的类型指针,该指针指向它的类元数据。

64位 JVM会默认使用选项 +UseCompressedOops 开启指针压缩,将指针压缩至32位。

以64位操作系统为例,对象头存储内容图例。

其中,lock:  锁状态标记位,该标记的值不同,整个mark word表示的含义不同。

biased_lock:偏向锁标记,为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。

age:Java GC标记位对象年龄。为什么默认是16岁回收,因为只有4bit,4bit 1111最大是15。

identity_hashcode:对象标识Hash码,采用延迟加载技术。当对象使用HashCode()计算后,并会将结果写到该对象头中。当对象被锁定时,该值会移动到线程Monitor中。

thread:持有偏向锁的线程ID和其他信息。这个线程ID并不是JVM分配的线程ID号,和Java Thread中的ID是两个概念。

epoch:偏向时间戳。

ptr_to_lock_record:指向栈中锁记录的指针。

ptr_to_heavyweight_monitor:指向线程Monitor的指针。

使用JOL工具类,打印对象头#

使用maven的方式,添加jol依赖

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

 

创建一个对象A

public class A {
    boolean flag = false;
}

 

使用jol工具类输出A对象的对象头

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

看看输出结果

输出的第一行内容和锁状态内容对应

unused:1 | age:4 | biased_lock:1 | lock:2

     0           0000             0                01     代表A对象正处于无锁状态

 

第三行中表示的是被指针压缩为32位的klass pointer

第四行则是我们创建的A对象属性信息 1字节的boolean值

第五行则代表了对象的对齐字段 为了凑齐64位的对象,对齐字段占用了3个字节,24bit

 

偏向锁#

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

输出结果

刚开始使用这段代码我是震惊的,为什么睡眠了5s中就把活生生的A对象由无锁状态改变成为偏向锁了呢?别急,容我慢慢道来!

JVM启动时会进行一系列的复杂活动,比如装载配置,系统类初始化等等。在这个过程中会使用大量synchronized关键字对对象加锁,且这些锁大多数都不是偏向锁。为了减少初始化时间,JVM默认延时加载偏向锁。这个延时的时间大概为4s左右,具体时间因机器而异。当然我们也可以设置JVM参数 -XX:BiasedLockingStartupDelay=0 来取消延时加载偏向锁。

可能你又要问了,我这也没使用synchronized关键字呀,那不也应该是无锁么?怎么会是偏向锁呢?

仔细看一下偏向锁的组成,对照输出结果红色划线位置,你会发现占用 thread 和 epoch 的 位置的均为0,说明当前偏向锁并没有偏向任何线程。此时这个偏向锁正处于可偏向状态,准备好进行偏向了!你也可以理解为此时的偏向锁是一个特殊状态的无锁

大家可以看下面这张图理解一下对象头的状态的创建过程

 

再来看看这段代码,使用了synchronized关键字

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

此时对象a,对象头内容有了明显的变化,当前偏向锁偏向主线程。

 

轻量级锁#

public static void main(String[] args) throws InterruptedException {
    Thread.sleep(5000);
    A a = new A();

    Thread thread1 = new Thread(){
        @Override
        public void run() {
            synchronized (a){
                System.out.println("thread1 locking");
                System.out.println(ClassLayout.parseInstance(a).toPrintable());
            }
            try {
                //thread1退出同步代码块,且没有死亡
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };

    Thread thread2 = new Thread(){
        @Override
        public void run() {
            synchronized (a){
                System.out.println("thread2 locking");
                System.out.println(ClassLayout.parseInstance(a).toPrintable());
            }
        }
    };
    thread1.start();

       //让thread1执行完同步代码块中方法。
    Thread.sleep(3000);
    thread2.start();
}

thread1中依旧输出偏向锁,thread2获取对象A时,thread1已经退出同步代码块,故此时thread2输出结果为轻量级锁。

 

重量级锁#

public static void main(String[] args) throws InterruptedException {
    Thread.sleep(5000);
    A a = new A();
    Thread thread1 = new Thread(){
        @Override
        public void run() {
            synchronized (a){
                System.out.println("thread1 locking");
                System.out.println(ClassLayout.parseInstance(a).toPrintable());
                try {
                    //让线程晚点儿死亡,造成锁的竞争
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    };
    Thread thread2 = new Thread(){
        @Override
        public void run() {
            synchronized (a){
                System.out.println("thread2 locking");
                System.out.println(ClassLayout.parseInstance(a).toPrintable());
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    };
    thread1.start();
    thread2.start();
}

thread1 和 thread2 同时竞争对象a,此时输出结果为重量级锁

偏向锁、轻量级锁、重量级锁的区别:

偏向锁,轻量级锁与重量级锁的区别以及如何膨胀

偏向锁

       Hotspot 的作者经过以往的研究发现大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得(偏向锁谁设计背景)为了让线程获得锁的代价更低而引入了偏向锁(偏向锁存在的意义,降低获取锁代价)。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁,而只需简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果测试成功,表示线程已经获得了锁,如果测试失败,则需要再测试下 Mark Word中偏向锁的标识是否设置成 1(表示当前是偏向锁),如果没有设置,则使用 CAS 竞争锁,如果设置了,则尝试使用 CAS 将对象头的偏向锁指向当前线程。

       偏向锁的撤销:偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态,如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,(当有其他线程尝试获得锁时,是根据遍历偏向线程的lock record来确定该线程是否还在执行同步块中的代码,如果lock record中obj字段指向锁对象,那么其他线程就认为该线程还在执行同步块代码。因此偏向锁的释放(解锁)很简单,仅仅将栈中的最近一条lock recordobj字段设置为null。需要注意的是,偏向锁的解锁步骤中并不会修改对象头中的thread id。栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。)

轻量级锁

       轻量级锁加锁:线程在执行同步块之前, JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用 CAS 将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,锁标志的状态值变为”10”,Mark Word中存储的就是指向重量级(互斥量)的指针。

       轻量级锁解锁:轻量级解锁时,会使用原子的 CAS 操作来将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

重量级锁

  重量级锁是 Java 虚拟机中最为基础的锁实现。在这种状态下,Java 虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程。

上面那句标红的话,真的是困扰了我好久,到底是两个线程,还是三个线程才开始膨胀呢!

下面是我个人自己的理解:

偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁。

        一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。

  一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。

  轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

参考自:https://www.cnblogs.com/sunweiye/p/11169048.html

synchronized关键字如何锁住对象的:

https://www.yunqishi.net/dnjc/dnzx/30175.html

synchronized锁代码块和方法的区别:

https://blog.csdn.net/yellowFARF/article/details/103755570

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值