看看我给面试官是如何娓娓道来synchronized锁升级过程的

简介: 看看我给面试官是如何娓娓道来synchronized锁升级过程的

备战2022春招或暑期实习,祝大家每天进步亿点点!Java并发编程Day8

本篇总结的是 如何在Java中避免创建不必要的对象,后续会每日更新~

关于《我们一起学Redis》、《我们一起学HarmonyOS》等知识点可以查看我的往期博客

相信自己,越活越坚强,活着就该逢山开路,遇水架桥!生活,你给我压力,我还你奇迹!image.png目录


1、简介


2、锁升级


2.1 无锁状态


2.2 偏向锁


2.3 轻量级锁


2.4 重量级锁


1、简介

synchronized是Java并发领域元老级人物,synchronized很多程序员都会用,它有三种表现形式。


普通同步方法 -> synchronized锁住的是当前对象

image.pngsynchronized在很多人眼里都是性能低的并发实现方式,早期Java中synchronized确实是一把重量级锁,在JDK1.6之后对synchronized做了全面的优化,优化主要思路是:同步代码块大多数场景下并不存在多线程竞争的情况,通俗点说就是大部分情况下这把锁其实不需要。因此JDK1.6引入了“偏向锁”和“轻量级锁”,从此以后synchronized中锁一共有4个状态,分别是无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。锁的四个状态是一个升级打怪的过程,只能不断升级而不能降级,当锁已经成为重量级锁了,就无法回到轻量级锁。


2、锁升级

2.1 无锁状态

在32位虚拟机中,无锁状态的对象头中的Mark Word组成如下所示(对象头不了解可以查看本专栏中的Monitor文章)image.png一个对象初始状态都是无锁状态,我们主要关注最后三位:


biased_lock占一位,表示是否是偏向锁,初始值为0,表示不是偏向锁

lock_state占两位,表示锁标志位或锁状态,初始值为10,表示无锁状态

默认情况下开启偏向锁,因此如果不关闭偏向锁,上述biased_lock值应该为1。


2.2 偏向锁

为什么会设计偏向锁这个东西呢?


这是因为大多数情况下,同步代码压根就没有竞争的情况发生,也就是一把锁一直是同一个线程在加锁、执行同步代码、解锁,这种情况是不是可以优化呢?当然可以啦!那怎么优化呢?这就是偏向锁干的事情。

比如如下代码,在未使用偏向锁的情况下需要两次加锁解锁操作,而使用偏向锁则免去了这些操作。

image.png什么是偏向锁呢?


从字面上就能理解,偏向锁就是偏向某个线程的锁,将这个锁对象想办法标记为当前线程就可以了,线程怎么区分呢?就用线程ID做标记嘛,把线程ID搞到锁对象里面就可以了嘛!


具体怎么实现的呢?


当某个线程访问同步代码需要获取锁时,不再直接去关联一个monitor对象,而是使用CAS将线程ID设置到对象头中的Mark Word中,并且线程栈帧中的锁记录中也会存储锁偏向的线程ID,这样只要锁不发生竞争,同一个线程多次尝试获取同一把锁的时候,只需要比较锁对象头的Mark Word中是不是偏向当前线程即可。

在32位虚拟机中,处于偏向锁的对象头的Mark Word组成如下所示:

前23位被设置成偏向线程的ID,biased_lock被设置成1,表示当前锁对象处于偏向锁状态,指的注意的是偏向锁的锁标志位和无锁标志位是一样都是10image.pngimage.pngimage.pngimage.png共64位,高位请脑海里补0

初始Mark Word为0x0000000000000005 -> 二进制 100000000000000000000000000101

偏向线程t1之后为0x000000002022b805 -> 二进制 100000001000101011100000000101

可以看到此时Mark Word中标记了线程ID (注意这个线程id是操作系统分配的线程id,不是虚拟机中java给定的线程id,不信你试试看),重复获取同一把锁t1线程只需要比较线程id即可。

image.pngimage.pngimage.png可以看到Mark Word的变化(共64位,高位请脑海里补0):


初始默认开启偏向锁0x0000000000000005 -> 10000000000000000000000000000000101


调用hashcode()方法0x000000063e31ee01 -> 11000111110001100011110111000000001


线程id0x063e31ee -> 110001111100011000111101110


可以看到调用hashCode()方法之后,MarkWord的最低3位由101转换为001了,偏向锁被取消了,此时Mark Word的组成就由下图所示

image.png因此可以得出结论,当我们调用某个锁对象的hashCode()方法时,默认的偏向锁机制将会被取消。

偏向锁升级为轻量级锁

偏向锁在什么时候会升级为轻量级锁呢?

可以看如下代码,t1对lock对象加锁之后,t2对lock对象加锁。

static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
    log.info("初始状态...");
    log.info(ClassLayout.parseInstance(lock).toPrintable());
    Thread t1 = new Thread(() -> {
        log.info(String.valueOf(Thread.currentThread().getId()));
        synchronized (lock) {
            log.info("线程t1第一次持有锁时...");
            log.info(ClassLayout.parseInstance(lock).toPrintable());
        }
    }, "Thread-1");
    t1.start();
    t1.join();
    log.info("线程t1加锁之后...");
    log.info(ClassLayout.parseInstance(lock).toPrintable());
    Thread t2 = new Thread(() -> {
        synchronized (lock) {
            log.info("线程t2持有锁时...");
            log.info(ClassLayout.parseInstance(lock).toPrintable());
        }
    }, "Thread-2");
    t2.start();
    t2.join();
    log.info("线程t2释放锁之后...");
    log.info(ClassLayout.parseInstance(lock).toPrintable());
}

可以看到t2线程在t1线程之后持有锁,此时锁由偏向锁升级为轻量级锁了,在t2释放锁之后,锁并没有变化偏向锁,而是回到了轻量级锁状态,这也说明了锁的升级过程是不可逆的。image.pngimage.png当时是不是在想,这个锁记录是用来干啥的呢?

其实这个锁记录就是用来解决轻量级锁的。


当锁处于轻量级锁状态时,线程在执行同步代码之前,JVM会现在当前线程的栈帧中创建用于存储锁记录的空间——这个就是锁记录,并且将锁对象头中的Mark Word复制到锁记录中,然后通过CAS尝试将对象头中的Mark Word替换为指向锁记录的指针,如果替换成功则当前线程获得锁,如果失败则尝试自旋获取锁,自旋获取锁又有成功和失败两种情况,如果自旋获取锁失败了则锁膨胀为重量级锁。


在32位虚拟机中,处于轻量级锁的对象头的Mark Word组成如下所示:image.pngimage.png具体实现方式


上面说了轻量级锁时,JVM会尝试将栈帧的锁记录地址设值到锁对象头的Mark Word中,那具体是怎么替换呢?

首先来看锁记录的内部构造:


锁记录有一个Lock Record地址,Lock Record地址用于记录锁记录的内存地址,当前线程通过CAS替换锁对象的Mark Word,如果成功则会将Mark Word的前62位信息记录在Lock Record地址中,Lock Record地址中的锁记录地址写入Mark Word中。

Object Reference用于记录锁对象的内存地址,当获取锁成功时Object Reference替换为锁对象内存地址image.png

package com.test;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
/**
 * @Author: Liziba
 * @Date: 2021/12/4 22:50
 */
@Slf4j
public class ThinLockingDemo {
    static final Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        log.info("初始状态...");
        log.info(ClassLayout.parseInstance(lock).toPrintable());
        Thread t1 = new Thread(() -> {
            method1();
        }, "Thread-1");
        t1.start();
        t1.join();
        log.info("线程t1释放锁之后...");
        log.info(ClassLayout.parseInstance(lock).toPrintable());
    }
    private static void method2() {
        synchronized (lock) {
            log.info("线程t1第二次持有锁时...");
            log.info(ClassLayout.parseInstance(lock).toPrintable());
        }
    }
    private static void method1() {
        log.info(String.valueOf(Thread.currentThread().getId()));
        synchronized (lock) {
            log.info("线程t1第一次持有锁时...");
            log.info(ClassLayout.parseInstance(lock).toPrintable());
        }
        method2();
    }
}


image.png

image.pngimage.pngimage.pngimage.png当t1线程运行结束尝试通过CAS替换锁对象Lock的Mark Word时,此时替换会失败,因为锁对象Lock的Mark Word已经被t2线程修改成Monitor的地址了,那怎么办呢?这个时候就得按照重量级锁那一套流程走了,t1线程会根据Mark Word中的Monitor内存地址找到Monitor对象,将Owner设置为null,并且将阻塞在EntryList中的线程唤醒,此时t2就可以重新参与锁对象Lock的竞争了,此时锁对象已经膨胀为重量级锁了。


最后我们来通过代码看下锁膨胀过程:

package com.test;
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;
import java.util.concurrent.TimeUnit;
/**
 * @Author: Liziba
 * @Date: 2021/12/5 00:21
 */
@Slf4j
public class ThinLockingDemo {
    static final Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        log.info("初始状态...");
        log.info(ClassLayout.parseInstance(lock).toPrintable());
        Thread t1 = new Thread(() -> {
            method1();
        }, "Thread-1");
        Thread t2 = new Thread(() -> {
            method2();
        }, "Thread-2");
        t1.start();
        // 运行t1先获取锁
        TimeUnit.SECONDS.sleep(1);
        t2.start();
        t1.join();
        t2.join();
        log.info("线程t1释放锁之后...");
        log.info(ClassLayout.parseInstance(lock).toPrintable());
    }
    private static void method1() {
        synchronized (lock) {
            log.info("线程t1持有锁时...");
            log.info(ClassLayout.parseInstance(lock).toPrintable());
            try {
                // t1睡眠5秒,允许t2竞争锁
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    private static void method2() {
        synchronized (lock) {
            log.info("线程t2持有锁时...");
            log.info(ClassLayout.parseInstance(lock).toPrintable());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

image.png

2.4 重量级锁

重量级锁看我的Monitor文章即可!

目录
相关文章
|
23天前
|
NoSQL Java API
美团面试:Redis锁如何续期?Redis锁超时,任务没完怎么办?
在40岁老架构师尼恩的读者交流群中,近期有小伙伴在面试一线互联网企业时遇到了关于Redis分布式锁过期及自动续期的问题。尼恩对此进行了系统化的梳理,介绍了两种核心解决方案:一是通过增加版本号实现乐观锁,二是利用watch dog自动续期机制。后者通过后台线程定期检查锁的状态并在必要时延长锁的过期时间,确保锁不会因超时而意外释放。尼恩还分享了详细的代码实现和原理分析,帮助读者深入理解并掌握这些技术点,以便在面试中自信应对相关问题。更多技术细节和面试准备资料可在尼恩的技术文章和《尼恩Java面试宝典》中获取。
美团面试:Redis锁如何续期?Redis锁超时,任务没完怎么办?
|
2月前
|
消息中间件 安全 前端开发
面试官:单核服务器可以不加锁吗?
面试官:单核服务器可以不加锁吗?
47 4
面试官:单核服务器可以不加锁吗?
|
2月前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)
|
23天前
|
存储 Kubernetes 架构师
阿里面试:JVM 锁内存 是怎么变化的? JVM 锁的膨胀过程 ?
尼恩,一位经验丰富的40岁老架构师,通过其读者交流群分享了一系列关于JVM锁的深度解析,包括偏向锁、轻量级锁、自旋锁和重量级锁的概念、内存结构变化及锁膨胀流程。这些内容不仅帮助群内的小伙伴们顺利通过了多家一线互联网企业的面试,还整理成了《尼恩Java面试宝典》等技术资料,助力更多开发者提升技术水平,实现职业逆袭。尼恩强调,掌握这些核心知识点不仅能提高面试成功率,还能在实际工作中更好地应对高并发场景下的性能优化问题。
|
27天前
|
存储 算法 安全
HashMap常见面试题(超全面):实现原理、扩容机制、链表何时升级为红黑树、死循环
HashMap常见面试题:红黑树、散列表,HashMap实现原理、扩容机制,HashMap的jd1.7与jdk1.8有什么区别,寻址算法、链表何时升级为红黑树、死循环
|
28天前
|
存储 安全 Java
面试题:再谈Synchronized实现原理!
面试题:再谈Synchronized实现原理!
|
3月前
|
缓存 Java
【多线程面试题二十三】、 说说你对读写锁的了解volatile关键字有什么用?
这篇文章讨论了Java中的`volatile`关键字,解释了它如何保证变量的可见性和禁止指令重排,以及它不能保证复合操作的原子性。
|
3月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
2天前
|
存储 缓存 Java
大厂面试必看!Java基本数据类型和包装类的那些坑
本文介绍了Java中的基本数据类型和包装类,包括整数类型、浮点数类型、字符类型和布尔类型。详细讲解了每种类型的特性和应用场景,并探讨了包装类的引入原因、装箱与拆箱机制以及缓存机制。最后总结了面试中常见的相关考点,帮助读者更好地理解和应对面试中的问题。
15 4
|
1月前
|
算法 Java 数据中心
探讨面试常见问题雪花算法、时钟回拨问题,java中优雅的实现方式
【10月更文挑战第2天】在大数据量系统中,分布式ID生成是一个关键问题。为了保证在分布式环境下生成的ID唯一、有序且高效,业界提出了多种解决方案,其中雪花算法(Snowflake Algorithm)是一种广泛应用的分布式ID生成算法。本文将详细介绍雪花算法的原理、实现及其处理时钟回拨问题的方法,并提供Java代码示例。
59 2