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

2024-02-18 15:43:38 浏览数 (1)

前言

这个面试题其实涉及到的底层知识比较多,在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 底层原理,希望大家在金三银四找到好坑位。

0 人点赞