IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 看看我给面试官是如何娓娓道来synchronized锁升级过程的 -> 正文阅读

[Java知识库]看看我给面试官是如何娓娓道来synchronized锁升级过程的

  • ?备战2022春招或暑期实习,祝大家每天进步亿点点!Java并发编程Day8
  • 本篇总结的是 如何在Java中避免创建不必要的对象,后续会每日更新~
  • 关于《我们一起学Redis》、《我们一起学HarmonyOS》等知识点可以查看我的往期博客
  • 相信自己,越活越坚强活着就该逢山开路,遇水架桥!生活,你给我压力,我还你奇迹!

目录

1、简介

2、锁升级

2.1 无锁状态

2.2 偏向锁

2.3 轻量级锁

2.4 重量级锁


1、简介

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

  • 普通同步方法 -> synchronized锁住的是当前对象
private?synchronized?void?demo()?{
????//?todo
}
  • 静态同步方式 -> synchronized锁住的是当前类的Class对象
private?static?synchronized?void?demo()?{
?????//?todo
}
  • 同步代码块 -> synchronized锁住的是代码块括号中声明的对象
//?锁住的是SynchronizedDemo类的Class对象
private?void?demo1()?{
????synchronized?(SynchronizedDemo.class)?{
????????//?todo
????}
}
//?锁住的是当前对象,也可以是任意对象,Java中每一个对象都可以作为锁
private?void?demo1()?{
????synchronized?(this)?{
????????//?todo
????}
}

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

2、锁升级

2.1 无锁状态

在32位虚拟机中,无锁状态的对象头中的Mark Word组成如下所示(对象头不了解可以查看本专栏中的Monitor文章)

Java对象头MarkWord初始情况.drawio.png

在64位虚拟机中,无锁状态的对象头中的Mark Word组成如下所示(32位虚拟机和64位虚拟机对应锁的升级过程在实现逻辑上没有什么区别,目前大多数操作系统都是64位操作系统,因此64位虚拟机使用也更加广泛一些)

64位虚拟机-MarkWord无锁状态.drawio.png

一个对象初始状态都是无锁状态,我们主要关注最后三位:

  • biased_lock占一位,表示是否是偏向锁,初始值为0,表示不是偏向锁
  • lock_state占两位,表示锁标志位或锁状态,初始值为10,表示无锁状态

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

2.2 偏向锁

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

这是因为大多数情况下,同步代码压根就没有竞争的情况发生,也就是一把锁一直是同一个线程在加锁、执行同步代码、解锁,这种情况是不是可以优化呢?当然可以啦!那怎么优化呢?这就是偏向锁干的事情。
比如如下代码,在未使用偏向锁的情况下需要两次加锁解锁操作,而使用偏向锁则免去了这些操作。

final?Object?lock?=?new?Object();

private?void?lockFirst()?{
????//?使用CAS将线程ID设置到对象头中的Mark?Word中
????synchronized?(lock)?{
????????//?todo
????}
????lockSecond();
}


private?void?lockSecond()?{
????//?比较锁对象头的Mark?Word中是不是偏向当前线程即可
????synchronized?(lock)?{
????????//?todo
????}
}

什么是偏向锁呢?

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

具体怎么实现的呢?

当某个线程访问同步代码需要获取锁时,不再直接去关联一个monitor对象,而是使用CAS将线程ID设置到对象头中的Mark Word中,并且线程栈帧中的锁记录中也会存储锁偏向的线程ID,这样只要锁不发生竞争,同一个线程多次尝试获取同一把锁的时候,只需要比较锁对象头的Mark Word中是不是偏向当前线程即可。
在32位虚拟机中,处于偏向锁的对象头的Mark Word组成如下所示:
前23位被设置成偏向线程的ID,biased_lock被设置成1,表示当前锁对象处于偏向锁状态,指的注意的是偏向锁的锁标志位和无锁标志位是一样都是10

Java对象头MarkWord整体情况偏向锁.drawio.png

在64位虚拟机中,处于偏向锁的对象头的Mark Word组成如下所示:
前54位被设置成偏向锁ID,biased_lock被设置成1

64位虚拟机-MarkWord-偏向锁.drawio.png

眼见为实,如何查看?

jdk提供了对应的类来查看打印对象的内存信息,引入jol依赖

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

测试代码:

public?static?void?main(String[]?args)?throws?InterruptedException?{

????Object?lock?=?new?Object();
????log.info(ClassLayout.parseInstance(lock).toPrintable());

}

输出结果:

image.png

可以看出我的虚拟机是64位虚拟机,因为对象头的Mark Word占用8个字节,但是输出的Mark Word值怎么是001,不是101呢?你不是说默认开启偏向锁么?
这是因为偏向锁的开启,虚拟机采用了延迟启动。不过这个延迟启动偏向锁,我们可以通过VM参数-XX:BiasedLockingStartupDelay=0来关闭。

image.png

此时MarkWord的值为0x0000000000000005,转换为二进制就是101了,这就证明了上面我们说的那些知识点啦。除此之外我们可以通过-XX:-UseBiasedLocking来关闭偏向锁(-XX:+UseBiasedLocking为启动偏向锁)。
关闭延迟偏向锁,以及关闭偏向锁 -XX:-UseBiasedLocking -XX:BiasedLockingStartupDelay=0

image.png

从输出结果来看,偏向锁被禁用了

image.png

偏向锁的展示

偏向锁的威力在于当锁偏向于某个对象时,此时只需要比较线程id即可,看一段测试代码:

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(()?->?{
????????synchronized?(lock)?{
????????????log.info("线程t1第一次持有锁时...");
????????????log.info(ClassLayout.parseInstance(lock).toPrintable());
????????}

????????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());

}

共64位,高位请脑海里补0
初始Mark Word为0x0000000000000005 -> 二进制 100000000000000000000000000101
偏向线程t1之后为0x000000002022b805 -> 二进制 100000001000101011100000000101
可以看到此时Mark Word中标记了线程ID (注意这个线程id是操作系统分配的线程id,不是虚拟机中java给定的线程id,不信你试试看),重复获取同一把锁t1线程只需要比较线程id即可。

image.png

hashcode()和偏向锁的关系

先看一段测试代码

@Slf4j
public?class?HashCodeAndBiasedLock?{

????static?final?Object?lock?=?new?Object();

????public?static?void?main(String[]?args)?{
????????log.info("调用HashCode之前");
????????log.info(ClassLayout.parseInstance(lock).toPrintable());
????????lock.hashCode();
????????log.info("调用HashCode之后");
????????log.info(ClassLayout.parseInstance(lock).toPrintable());
????}

}

输出结果:

image.png

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

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

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

  • 线程id0x063e31ee -> 110001111100011000111101110

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

64位虚拟机-MarkWord无锁状态.drawio.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.png

2.3 轻量级锁

轻量级锁的简介

在我的《图解栈帧》一文中有这样一张图,每个线程运行时虚拟机会在虚拟机栈中为每个线程分配一块栈内存,线程执行的每一个方法会在线程栈中压入一个栈帧,每个方法的栈帧组成结构中有一个部分叫做锁记录

?


当时是不是在想,这个锁记录是用来干啥的呢?
其实这个锁记录就是用来解决轻量级锁的。
?

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

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

?


在64位虚拟机中,处于轻量级锁的对象头的Mark Word组成如下所示:

64位虚拟机-MarkWord-轻量级锁.drawio.png

看到一段轻量级锁代码:

static final Object lock = new Object();
public static void main(String[] args) {

    log.info(ClassLayout.parseInstance(lock).toPrintable());
    synchronized (lock) {
        log.info(ClassLayout.parseInstance(lock).toPrintable());
    }
}

为了方便演示,通过VM参数-XX:-UseBiasedLocking关闭了偏向锁,初始为无锁状态,在获取锁时Mark Word中记录的是锁地址的指针(注意最后两位00表示轻量级锁,其他表示锁地址指针)

image.png

具体实现方式

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

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

锁记录.drawio.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();
    }

}

首先在t1线程启动之后,执行method1()方法时,虚拟机会为在t1线程的虚拟机栈中压入一个栈帧,该栈帧中会有一个锁记录Lock Record,此时栈帧中的锁记录和锁对象关系如图:

轻量级锁获取过程-CAS之前.drawio.png

t1线程通过CAS尝试将Lock锁对象的对象头的Mark Word替换为自己的锁记录地址,并将Mark Word中的值保存到Lock Record中,并且Object Reference将会记录锁对象Lock的内存地址,此时栈帧中的锁记录和锁对象关系如图如图:

轻量级锁获取过程-CAS成功.drawio.png

接着method1()中调用了method2()也就是轻量级锁重入,此时CAS替换肯定是不成功的,因为锁对象的Mark Word已经被替换了,并且锁状态以及被标记位00,但是当前线程发现锁记录地址是自己栈帧中的锁记录地址,此时会在method2()的栈帧中新增一个Lock Record地址为null的锁记录,此时栈帧中的锁记录和锁对象关系如图如图:

轻量级锁获取过程-重入.drawio.png

最后method1方法执行结束之后,method1会在释放锁的时候通过CAS将锁对象的对象头的Mark Word中的数据替换回去,如果CAS成功则回到初始状态,但是存在一种CAS失败的情况,线程t1在持有锁对象的时候,其他线程尝试获取锁失败了,此时锁会膨胀为重量级锁,这个过程如下所示:

如下所示,t1线程替换成功,t2线程替换失败(t2线程不会一次就失败,会自适应一定次数自旋CAS替换未成功,则替换失败):

轻量级锁膨胀-t2CAS失败.drawio.png

此时会发生锁膨胀,这个膨胀过程由t2完成,t2线程会为当前锁对象关联一个monitor对象,并将锁对象Lock的对象头替换为Monitor的内存地址,将锁状态位修改为10,然后自己进入Monitor的EntryList等待队列中阻塞,等待被唤醒重新竞争锁,这个过程如下所示:

轻量级锁膨胀-关联monitor.drawio.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文章即可!
?

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-12-07 11:53:51  更:2021-12-07 11:53:58 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 4:25:25-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码