前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >面试专题:Synchronized 锁的升级过程(锁/对象状态)及底层原理

面试专题:Synchronized 锁的升级过程(锁/对象状态)及底层原理

原创
作者头像
小明爱吃火锅
发布2024-02-18 15:43:38
4250
发布2024-02-18 15:43:38
举报
文章被收录于专栏:小明说Java小明说Java

前言

这个面试题其实涉及到的底层知识比较多,在Java中都知道synchronized,这是一个关键字,为什么使用了之后,可以结果多线程安全问题。里面内部流程是怎样的呢?加锁是加在哪里?金三银四越来越卷,面试官不再是,单纯的问如何解决线程安全,有没有使用过synchronized,而是想知道synchronized底层的知识点。本文就深入讲解synchronized底层原理,对象加锁是如果一步一步实现的。

synchronized加锁加在哪里?

synchronized可以使用两种方式进行加锁,一个同步代码块,另一种同步方法,其实都是针对对象进行加锁的。synchronized,是一个指令,解析成monitener,然后jvm去执行,实际改变对象头信息。所以要想知道synchronized原理需要先知道java对象头,改变的是对象头的什么信息。

对象头信息

JVM已经规定对象头的内容openjdk官网也说了,包括:

可以存放堆对象的布局、类型、gc状态、同步状态、标识的哈希值。有两个词组成

mark word:64bit(读取方式从反方向读)

klass pointer/class metadata address:类模块数据地址,指向元空间,32bit(有个是64bit,没有指针压缩)

c++源码中指出64位的jvm的mark word大小占64位,包括以下信息:

markword的结构,可以看下图:

可以看出:

hash:用31位来存储hash值,hash本身是不存在对象头的,只有调用hashcode时,调用native方法去计算生成,然后才放到对象头hash

age:GC分代年龄存了4位(新生区超过15次还没没回收就到老年区,设置只能1-15,原因是对象头gc状态,最大值记录在4四位的二进制上,也就是2的4次方,16。),biased_lock:偏向锁,1位

lock:锁状态,2位

biased_lock +?lock: 最后3位控制对象的5种状态

对象状态:无锁、偏向锁、轻量锁、重量锁、gc标记,只有锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。

synchronized锁的升级过程(锁/对象状态

通过上述对象头介绍,应该清楚了,synchronized加锁主要改变的是对象头的信息,改变的是64位对象头,最后的三位。

无锁 001:无锁就是没有对任何资源进行锁定,所有线程都能访问并修改资源,

偏向锁 101:在锁对象的对象头中记录一下当前获取到该锁的线程ID,该线程下次如果又来获取该锁就可以直接获取到了,也就是支持锁重入。偏向锁作用,主要是解决可重入问题,当线程重复获取锁的时候,就判断该锁是否有线程ID

轻量级锁 000:当两个或者以上线程交替获取锁,当没有在对象上并发的获取锁时,偏向锁升级为轻量级锁。在此阶段,线程采取CAS的自旋锁方式尝试获取锁,避免阻塞线程造成的CPU在用户态和内核态间转换的消耗。

重量级锁 010:两个或者以上线程并发的在一个对象上进行同步时,为了避免无用自旋锁cpu,轻量级锁就会升级成重量级锁。

代码演示synchronized锁的升级过程

synchronized加锁,一把锁,在没有竞争的情况下,被同一个对象多次获取,所以没必要一直加锁操作,以此来减少CPU资源,所以就会导致加了锁,最后三位数还是000,接下来通过代码展示synchronized加锁这四种状态的对象头的改变情况。

首先,64位对象头查看需要依赖对应的插件,官网 https://mvnrepository.com/artifact/org.openjdk.jol/jol-core 对象头查看,只要用10.0的版本进行查看,就可以查看到二进制,在pom文件中加入对应依赖。

代码语言:xml
复制

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

无锁状态 001

即没有使用关键字synchronized,对象创建的时候。

代码语言:java
复制
 class ZPF {
    boolean flag = false;
}

public class Main{
    public static void main(String[] args) throws Exception {
        ZPF a = new ZPF();
        System.out.println("befor hash");
        //没有计算HASHCODE之前的对象头
        System.out.println(ClassLayout.parseInstance(a).toPrintable());
        //JVM 计算的hashcode
        System.out.println("jvm------------0x"+Integer.toHexString(a.hashCode()));
        //当计算完hashcode之后,我们可以查看对象头的信息变化
        System.out.println("after hash");
        System.out.println(ClassLayout.parseInstance(a).toPrintable());

    }

}

可以看到对象头object header 有12byte也就是96bit,64bit是mark word,32bit是类模块数据地址

由于对象头读取方式逆序的,所以最终结果是跟官网指出的一样,最后三位是 001

00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

偏向锁101

无并发线程竞争,加锁中的对象状态,就是偏向锁,如下代码:

代码语言:java
复制
// 偏向锁
public static void main(String[] args) throws InterruptedException {
    Thread.sleep(5000);//虚拟机启动时对偏向锁有延时
    ZPF a = new ZPF();
    synchronized (a){
        System.out.println("lock ing");
        System.out.println(ClassLayout.parseInstance(a).toPrintable());
    }

}

在这个例子中,加了Thread.sleep(5000)时间之后,对象锁变成了偏向锁,不加的话就是轻量级锁。这是因为在没有加Thread.sleep(5000)时,程序执行非常快,锁的状态会在偏向锁、轻量级锁和重量级锁之间快速切换。而加了Thread.sleep(5000)后,程序执行时间足够长,使得锁的状态可以稳定在偏向锁上,最终结果如下

根据读取规则,最终64位对象头是,最后三位是 101

00000000 00000000 00000000 00000000 00000011 01101011 01000000 00000101

轻量级锁 000

主要演示无并发线程竞争是,对象锁的变化,这里主要区别是跟上述对象锁的对比,少了延时,synchronized加锁的时候,对象头锁状态就会变成轻量级锁。

代码语言:java
复制
static ZPF a;
public static void main(String[] args) throws Exception {
    a = new ZPF();
    System.out.println("befre lock");
    System.out.println(ClassLayout.parseInstance(a).toPrintable());
    sync();
    System.out.println("after lock");
    System.out.println(ClassLayout.parseInstance(a).toPrintable());
}

public  static  void sync() throws InterruptedException {
    synchronized (a){
        System.out.println("lock ing");
        System.out.println(ClassLayout.parseInstance(a).toPrintable());
    }
}

查看结果,加锁中,锁的对象状态是轻量级的

根据读取规则,最终64位对象头是,最后三位是 000

00000000 00000000 00000000 00000000 00000010 11000100 11110011 01000000

重量级锁 010

由于jdk优化之后synchronized加锁,不会立即升级成重量级锁,只有在多个线程并发,加锁,对象状态才会升级重量级锁。接下来模拟,多个线程抢占对象锁。

代码语言:java
复制
static ZPF a;
public static void main(String[] args) throws Exception {

    a = new ZPF();
    System.out.println("befre lock");
    System.out.println(ClassLayout.parseInstance(a).toPrintable());//无锁
    // 线程1,先占有锁,如果有其他线程过来抢占,就会升级重量级锁,没有其他线程抢占,就是轻量级锁
    Thread t1 = new Thread(() -> {
        synchronized (a) {
            try {
                Thread.sleep(1000);
                System.out.println(ClassLayout.parseInstance(a).toPrintable());//重量锁
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t1.start();

    // 开启线程2,此时锁给线程1获取中
   Thread t2 = new Thread(() -> {
        synchronized (a) {
            System.out.println(ClassLayout.parseInstance(a).toPrintable());//重量锁
        }
    });
    t2.start();
}

查看结果,可以看到,对象头最后三位是010,已经变成了重量级锁了。

00000000 00000000 00000000 00000000 00011000 01011001 00011111 01111010

总结

对于java八股文,已经不局限于表层使用方法了,面试官更想知道的是,底层的知识,本文主要通过介绍同步锁synchronized 的原理,进而通过实际案例代码,将抽象的java对象信息展示出来,让面试者更加深入了解synchronized 底层原理,希望大家在金三银四找到好坑位。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • synchronized加锁加在哪里?
  • 对象头信息
  • synchronized锁的升级过程(锁/对象状态)
  • 代码演示synchronized锁的升级过程
    • 无锁状态 001
      • 偏向锁101
        • 轻量级锁 000
          • 重量级锁 010
          • 总结
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
          http://www.vxiaotou.com