synchronized底层原理(一)

文章目录

    • 1. 问题引入
    • 2. 相关概念
    • 3. Synchronized使用
    • 4. Synchronized底层原理
      • 1. 简介
      • 2. Monitor(管程/监视器)
      • 3. Java语言的内置管程synchronized
      • 4. Java对象的内存布局
      • 5. 如何使用MarkWord记录锁状态
      • 6. 偏向锁
      • 7. 轻量级锁

1. 问题引入

假设我们有1000个线程对变量i进行++操作,有1000个线程同时对i做–操作,最后的结果是0吗?结果显而易见是不确定的,由于++和–并不是一个原子操作,所以多线程环境下进行++操作和–操作时结果是不确定的 ,++和–操作JVM字节码指令如下:

getstatic i //获取静态变量i得值
iconst_1 //将1压入操作数栈
iadd //自增
putstaitc i //将修改后的值存入静态变量i
getstatic i //获取静态变量i得值
iconst_1 //将1压入操作数栈
isub //自增
putstaitc i //将修改后的值存入静态变量i

在这里插入图片描述

多线程下上面八行代码是随机执行的。

在这里插入图片描述

2. 相关概念

  • 临界区

一段代码块内如果存在对共享资源的多线程读写操作,称这块代码为临界区,其共享资源被称为临界资源。

  • 竞态条件

多个线程载临界区内执行,由于代码的执行序列不同导致结果无法预测,称之为发生了竞态条件。

3. Synchronized使用

synchronized 同步块是 Java 提供的一种原子性内置锁,Java 中的每个对象都可以把它当作 一个同步锁来使用,这些 Java 内置的使用者看不到的锁被称为内置锁,也叫作监视器锁。

在这里插入图片描述

4. Synchronized底层原理

1. 简介

synchronized是JVM内置锁,基于Monitor机制实现,依赖底层操作系统的互斥原语Mutex(互斥量),它是一个重量级锁,性能较低。当然,JVM内置锁在1.5之后版本做了重大的 优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、自适应自旋(Adaptive Spinning)等技术来减少锁操 作的开销,内置锁的并发性能已经基本与Lock持平。

Java虚拟机通过一个同步结构支持方法和方法中的指令序列的同步:monitor。同步方法是通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现;同步代码块是通过monitorenter和monitorexit来实现。两个指令的执行是JVM通过调用操作系统的互斥 原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。

2. Monitor(管程/监视器)

Monitor,直译为“监视器”,而操作系统翻译为管程。管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发。载Java1.5之前,Java语言提供的唯一并发语言就是管程,Java 1.5之后提供的SDK并法包也是以管程为基础的。synchronized关键字和wait()、notify()、notifyAll()这三个方法是java中实现管程技术的组成部分。

MESA模型

在管程的发展史上,先后出现过三种不同的管程模型,分别是Hasen模型、Hoare模型和MESA模型。现在正在广泛使用的是MESA模型。下面我们便介绍MESA模型:
在这里插入图片描述
管程中引入了条件变量的概念,而且每个条件变量都对应有一个等待队列。条件变量和等待队列的作用是解决线程之间的同步问题。

  • wait()的正确使用姿势

对于MEAS管程来说,有一个编程范式:

while(条件不满足){

	wait();
}

唤醒的时间和获取到锁继续执行的时间是不一致的,被唤醒的线程再次执行时可能条件又不满足了,所以循环检验条件。MESA模型的wait()方法还有一个超时参数,为了避免线程进入等待队列永久阻塞。

  • notify()和notifyAll()分别何时使用

满足以下三个条件时,可以使用notify(),其余情况尽量使用notifyAll():

  1. 所有等待线程拥有相同的等待条件;
  2. 所有等待线程被唤醒后,执行相同的操作;
  3. 只需要唤醒一个线程。

3. Java语言的内置管程synchronized

Java 参考了 MESA 模型,语言内置的管程(synchronized)对 MESA 模型进行了精简。MESA 模型中,条件变量可以有多个,Java 语言内置的管程里只有一个条件变量。模型如下图所示。

在这里插入图片描述

java.lang.Object 类定义了 wait(),notify(),notifyAll() 方法,这些方法的具体实现,依赖于ObjectMonitor 实现,这是 JVM 内部基于 C++ 实现的一套机制。

ObjectMonitor(){
  _header = NULL; //对象头 markOop
  _count = 0;
  _waiters = 0,
  _recursions = 0; // 锁的重入次数
  _object = NULL; //存储锁对象
  _owner = NULL; // 标识拥有该monitor的线程(当前获取锁的线程,这里使用CAS更新)
   _WaitSet = NULL; // 等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点
  _WaitSetLock = 0 ;
   _Responsible = NULL ;
   _succ = NULL ;
   _cxq = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构)
   FreeNext = NULL ;
   _EntryList = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失败的线程)
   _SpinFreq = 0 ;
   _SpinClock = 0 ;
   OwnerIsThread = 0 ;
   _previous_owner_tid = 0;
   }

在这里插入图片描述

在获取锁时,是将当前线程插入到cxq的头部,而释放锁时,默认策略(QMode=0)是:如果EntryList为空,则将 cxq中的元素按原有顺序插入到EntryList,并唤醒第一个线程,也就是当EntryList为空时,是后来的线程先获取锁。_EntryList不为空,直接从_EntryList中唤醒线程。

思考:synchronized加锁加在对象上,锁对象是如何记录锁状态的?

4. Java对象的内存布局

回到上节留下的思考题:

Hotspot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据 (Instance Data)和对齐填充(Padding)。
对象头:比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID, 偏向时间,数组长度(数组对象才有)等。
实例数据:存放类的属性数据信息,包括父类的属性信息;
对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存 在的,仅仅是为了字节对齐。

在这里插入图片描述

  • Mark Word

用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线
程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机中分别为 32bit和64bit,官方称它为“Mark Word”。

  • klass point(元数据指针)

对象头的另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指 针来确定这个对象是哪个类的实例。 32位4字节,64位开启指针压缩或最大堆内存<32g时4字节,否则8字节。jdk1.8默认开启指针压缩后为4字节,当在JVM参数中关闭指针压缩(-XX:- UseCompressedOops)后,长度为8字节。

  • 数组长度(只有数组对象有)

如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度。 4字节

在这里插入图片描述

new Object()对象创建后占几个字节:64位机Mark Word占用8字节,klass pointer采用了指针压缩技术占用4个字节,然后填充为8字节的整数倍,所以Object对象占用16个字节(其不包含实例数据)。下面使用JOL来验证一下:

  • 使用JOL工具查看内存布局
  1. 导入maven依赖
 <dependency>
      <groupId>org.openjdk.jol</groupId>
      <artifactId>jol-core</artifactId>
      <version>0.16</version>
    </dependency>
  1. 使用
public class Main {
    public static void main(String[] args) {
       Object ob=new Object();
        System.out.println(ClassLayout.parseInstance(ob).toPrintable());
    }
}
  1. 打印结果

在这里插入图片描述
可以发现一个Object对象就是16个字节

5. 如何使用MarkWord记录锁状态

Hotspot通过markOop类型实现Mark Word,具体实现位于markOop.hpp文件中。由于对象需要存储的运行时数据很多,考虑到虚拟机的内存使用,markOop被设计成一个非固定的数据结构,以便在极小的空间存储尽量多的数据,根据对象的状态复用自己的存储空间。 简单点理解就是:MarkWord 结构搞得这么复杂,是因为需要节省内存,让同一个内存区域在不同阶段有不同的用处。

  • Mark Word结构

32位在这里插入图片描述
64位
在这里插入图片描述

可以简单了将Mark word总结如下:

  enum{locked_value=0,//00轻量级锁
  unlocked_value = 1, //001 无锁
  monitor_value = 2, //10 监视器锁,也叫膨胀锁,也叫重量级锁
  marked_value = 3, //11 GC标记
  biased_lock_pattern = 5 //101 偏向锁
  };

在这里插入图片描述

下面来测试一个奇怪的现象:

我们知道无锁状态下,锁标记位置为01,如果我们现在给ob对象加锁,再来看下锁标记位。

public class Main {
    public static void main(String[] args) {
       Object ob=new Object();
       synchronized (ob) {
           System.out.println(ClassLayout.parseInstance(ob).toPrintable());
       }
    }
}

在这里插入图片描述

可以发现锁标记位变成了00,我们都知道synchronized是重量级锁,按照道理应该是10,这其实是JVM在对对象加锁时,做了一个锁延迟的优化,它会根据情况给对象加上轻量级锁、偏向锁和重量级锁。下面来详细介绍:

6. 偏向锁

偏向锁是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了消除数据在无竞争情况下锁重入(CAS操作)的开销而引入偏向锁。对于没有锁竞争的场合,偏向锁有很好的优化效果。当JVM启用了偏向锁模式(jdk6默认开启),新创建对象的Mark Word中的Thread Id为0, 说明此时处于可偏向但未偏向任何线程,也叫做匿名偏向状态(anonymously biased)。

  • 偏向锁延迟偏向

延迟偏向是一种优化机制,用于减少偏向锁的争用。在JVM中,偏向锁是为了在无竞争的情况下提高性能而设计的,允许最初获取锁的线程在未释放锁的情况下重新获取锁,而无需付出昂贵的同步代价。延迟偏向是指在对象第一次被线程获取锁时,并不立即标记为偏向锁,而是在之后的一段时间内,如果发现该线程频繁地获取该锁,才会将对象标记为偏向锁。这样,如果对象的锁实际上是被多个线程竞争的,就避免了不必要的偏向锁状态的设置,减少了额外的性能开销。延迟偏向的优势在于,它允许系统在运行时动态地选择是否启用偏向锁,从而更好地适应实际的应用场景。如果在程序的执行过程中,某个对象的锁一直只被一个线程所持有,那么延迟偏向可以有效减少锁的竞争,提高性能。如果发现有多个线程在竞争同一个锁,系统就可以决定不再启用偏向锁,以避免额外的性能损失。延迟偏向是在JDK 6 的一种改进,通过这种方式,Java虚拟机可以更智能地管理偏向锁,以适应不同的应用场景。在实际编程中,对于多线程竞争不激烈的场景,偏向锁和延迟偏向能够带来一定的性能优势。

偏向锁模式存在偏向锁延迟机制:HotSpot 虚拟机在启动后有个4s 的延迟才会对每个新建的对象开启偏向锁模式。JVM启动时会进行一系列的复杂活动,比如装载配置,系统类初始化等等。在这个过程中会使用大量synchronized关键字对对象加锁,且这些锁大多数都不是偏向锁。 为了减少初始化时间,JVM默认延时加载偏向锁。

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

在这里插入图片描述

可以发现锁标记位变成了01,开启了偏向锁,obj是4s后创建的对象,所以会默认开始偏向,此时偏向的线程为0,处于匿名偏向状态。

偏向锁撤销之调用对象HashCode

调用锁对象的obj.hashCode()System.identityHashCode(obj)方法会导致该对象的偏向锁被撤销。因为对于一个对象,其HashCode只会生成一次并保存,偏向锁是没有地方保存 hashcode的。

  • 轻量级锁会在锁记录中记录 hashCode
  • 重量级锁会在 Monitor 中记录 hashCode

当对象处于可偏向(也就是线程ID为0)和已偏向的状态下,调用HashCode计算将会使对象再也无法偏向:

  • 当对象可偏向时,MarkWord将变成未锁定状态,并只能升级成轻量锁(正常情况);
  • 当对象正处于偏向锁时,调用HashCode将使偏向锁强制升级成重量锁。

偏向锁撤销之调用wait/notify

偏向锁状态执行obj.notify() 会升级为轻量级锁,调用obj.wait(timeout) 会升级为重量级锁。

7. 轻量级锁

倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段,此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁所适应的场景是线程交替执行同步块的场合(发生轻微竞争),如果存在同一时间多个线程访问同一把锁的场合,就会导致轻量级锁膨胀为重量级锁。

观察几个状态,分析偏向锁是如何升级为轻量级锁的

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000);
       Object ob=new Object();
       new Thread(()->{
           synchronized (ob) {
               System.out.println(ClassLayout.parseInstance(ob).toPrintable());
           }
       },"t1").start();
      Thread.sleep(3000);
        new Thread(()->{
            synchronized (ob) {
                System.out.println(ClassLayout.parseInstance(ob).toPrintable());
            }
        },"t2").start();

    }
}

在这里插入图片描述

观察上面代码线程1和线程2都是偏向锁,原因是两个线程并没有发生竞争,因为Thread.sleep(3000);让t1先执行完,然后执行t2,如果延迟t1的生命周期,然后让两个线程共同执行。

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000);
       Object ob=new Object();
       new Thread(()->{
           synchronized (ob) {
               System.out.println(ClassLayout.parseInstance(ob).toPrintable());
           }
           try {
               Thread.sleep(2000);
           } catch (InterruptedException e) {
               throw new RuntimeException(e);
           }
       },"t1").start();
        Thread.sleep(1000);
        new Thread(()->{
            synchronized (ob) {
                System.out.println(ClassLayout.parseInstance(ob).toPrintable());
            }
        },"t2").start();

    }
}

在这里插入图片描述

可以发现偏向锁升级为了轻量级锁,如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。

另一个场景是,轻量级别锁解锁后变为无锁

public class Main {
    public static void main(String[] args) throws InterruptedException {
       Object ob=new Object();
       System.out.println(ClassLayout.parseInstance(ob).toPrintable());
       new Thread(()->{
           synchronized (ob) {
               System.out.println(ClassLayout.parseInstance(ob).toPrintable());
           }
       },"t1").start();
       Thread.sleep(2000);
       System.out.println(ClassLayout.parseInstance(ob).toPrintable());

    }
}

上面代码首先创建Object对象,因为延迟偏向的存在,synchronized会给obj加上轻量级锁,然后我们看代码输出。

在这里插入图片描述

首先输出是无锁状态,显而易见,然后输出轻量级锁这是延迟偏向的原因,最后输出的无锁状态,所以当共享资源没有加锁时,轻量级锁会变为无锁状态(这是为了其它线程能够获取锁)。

在这里插入图片描述

下一节我们再详细分析底层源码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/215454.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux scatterlist 详解

源码基于&#xff1a;Linux 5.4 约定&#xff1a; 芯片架构&#xff1a;ARM64内存架构&#xff1a;UMACONFIG_ARM64_VA_BITS&#xff1a;39CONFIG_ARM64_PAGE_SHIFT&#xff1a;12CONFIG_PGTABLE_LEVELS &#xff1a;3 0. 前言 之前在《Linux DMA... 零拷贝》博文分享了DMA 技…

Apache Flink(六):Apache Flink快速入门 - Flink案例实现

🏡 个人主页:IT贫道_大数据OLAP体系技术栈,Apache Doris,Clickhouse 技术-CSDN博客 🚩 私聊博主:加入大数据技术讨论群聊,获取更多大数据资料。 🔔 博主个人B栈地址:豹哥教你大数据的个人空间-豹哥教你大数据个人主页-哔哩哔哩视频 目录

医院不良事件报告系统源码带鱼骨图分析

医院不良事件上报系统通过 “事前的人员知识培训管理和制度落地促进”、“事中的事件上报和跟进处理”、 以及 “事后的原因分析和工作持续优化”&#xff0c;结合预存上百套已正在使用的模板&#xff0c;帮助医院从对护理事件、药品事件、医疗器械事件、医院感染事件、输血事件…

双远心镜头:让视觉检测更精准、高效!

工业镜头是视觉系统中的重要组件&#xff0c;工业镜头的选型影响着整个系统的成像效果。在做视觉检测时&#xff0c;会遇到无法检测空间物体、无法控制视场变化、无法控制图像扭曲、对比度低、畸变大、反光等问题&#xff0c;这时普通的工业镜头并不能有效地解决问题&#xff0…

公众号50个数量怎么操作?

一般可以申请多少个公众号&#xff1f;公众号申请限额在过去几年内的经历了很多变化。对公众号申请限额进行调整是出于多种原因&#xff0c;确保公众号内容的质量和合规性。企业公众号的申请数量从50个到5个最后到2个&#xff0c;对于新媒体公司来说&#xff0c;这导致做不了公…

免费数据采集软件,多种数据采集方式

数据无疑是企业决策的关键驱动力。要充分利用数据&#xff0c;就需要进行数据收集&#xff0c;而数据采集的方式多种多样。 数据采集方式的丰富多彩 数据采集并非一蹴而就的简单任务&#xff0c;而是一个多层次、多步骤的过程。在这个过程中&#xff0c;我们有着多种数据采集…

python装饰器解析(关键点:高阶函数、嵌套函数)(参数化装饰器、类装饰器)

文章目录 Python装饰器解析什么是Python装饰器基础理解 如何创建装饰器&#xff08;关键点&#xff1a;高阶函数、嵌套函数&#xff09;创建基础装饰器 使用装饰器使用示例 装饰器的返回值参数化装饰器创建参数化装饰器语法示例使用示例 类装饰器创建类装饰器语法示例使用示例 …

嘴尚绝卤味:健康卤味,未来餐饮市场的新星

随着人们生活水平的提高&#xff0c;对于吃的要求也越来越高。尤其是在快节奏的现代社会中&#xff0c;健康饮食成为了越来越多人的追求。在这种背景下&#xff0c;健康卤味这一新兴食品品类应运而生&#xff0c;成为了餐饮市场的新宠儿。 一、健康卤味的崛起 传统的卤味制作过…

MySQL find_in_set函数的深入解析与应用

theme: smartblue 在数据库操作中&#xff0c;我们经常会遇到需要处理以逗号分隔的字符串&#xff0c;并且需要根据这些字符串进行查询的情况。MySQL提供了一个非常实用的函数FIND_IN_SET()来处理这种特定的查询需求。本文将深入解析FIND_IN_SET()函数的使用方法&#xff0c;并…

SAP MIGO前台批次特性值增强(自动带出)<转载>

原文链接&#xff1a;https://blog.csdn.net/qq_45063256/article/details/128464411 增强点&#xff1a;程序LCTMSF3Z 在MIGO中点击批次右边的分类时&#xff0c;自动将该批次的批次特性值带出来。 现在打开程序LCTMSF3Z 该程序就只有一个FORM&#xff0c;首先切换到编辑…

docker内容整理

docker内容整理 docker的安装 检查之前是否安装过docker&#xff0c;如果有使用yum remove docker卸载 [rootwoniu ~]# yum remove docker \ > docker-client \ > docker-client-latest \ > docker-common \ > docker-latest \ > docker-latest-logrotate \ &g…

Android12之MediaCodec硬编解码调试手段(四十九)

简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 优质专栏:多媒体系统工程师系列【原创干货持续更新中……】🚀 人生格言: 人生从来没有捷径,只…

Java多线程技术二:线程间通信——join()方法的使用

1 概述 在很多情况下&#xff0c;主线程创建并启动子线程&#xff0c;如果子线程中要进行大量的耗时运算&#xff0c;主线程往往将早于子线程结束&#xff0c;这时如果主线程想等待子线程执行完成后再结束&#xff0c;例如子线程处理一个数据&#xff0c;主线程要取到这个数据中…

如何入驻抖音本地生活服务商,门槛太高怎么办?

随着抖音本地生活服务市场的逐渐成熟&#xff0c;越来越多平台开始涉及本地生活服务领域&#xff0c;而本地生活服务商成了一个香窝窝&#xff0c;为了保护用户权益和平台生态&#xff0c;对入驻入驻抖音本地生活服务商的条件及审核也越来越严格&#xff0c;这让很多想成为抖音…

Mysql的索引详解

1.索引的分类 1.按照功能来分&#xff0c;可以分为主键索引、唯一索引、普通索引、全文索引 2.按照索引字段个数来分&#xff0c;可以分为单列索引、联合索引 3.按照物理实现方式来分&#xff0c;可以聚簇索引、非聚簇索引 2.适合添加索引的场景 1.具有唯一性约束的字段。 2…

Matlab论文插图绘制模板第129期—函数网格曲面图

在之前的文章中&#xff0c;分享了Matlab函数折线图的绘制模板&#xff1a; 函数三维折线图&#xff1a; 进一步&#xff0c;再来分享一下函数网格曲面图。 先来看一下成品效果&#xff1a; 特别提示&#xff1a;本期内容『数据代码』已上传资源群中&#xff0c;加群的朋友请自…

直击2023云栖大会-大模型时代到来:“计算,为了无法计算的价值”

2023年的云栖大会以“计算&#xff0c;为了无法计算的价值”为主题&#xff0c;强调了计算技术在现代社会中的重要性&#xff0c;特别是在大模型时代到来的背景下。 大模型时代指的是以深度学习为代表的人工智能技术的快速发展&#xff0c;这些技术需要大量的计算资源来训练和优…

算法设计与实现--动态规划篇

什么是动态规划算法 动态规划算法是一种求解复杂问题的方法&#xff0c;通过将原问题分解为相对简单的子问题来求解。其基本思想是将待求解的问题分解为若干个子问题&#xff08;阶段&#xff09;&#xff0c;按顺序求解子阶段&#xff0c;前一子问题的解&#xff0c;为后一子…

uniapp:如何使用uCharts

目录 第一章 前言 第二章 安装插件uCharts 第三章 使用uCharts 第四章 注意 第一章 前言 需求&#xff1a;这是很久之前的一个项目的需求了&#xff0c;当时我刚接触app&#xff0c;有这么一个需求&#xff0c;在uniapp写的app项目中做一些图表统计&#xff0c;最开始以为…

设备巡检的内容有哪些?巡检注意事项及巡检要点?

本文将为大家讲解&#xff1a;设备巡检的内容有哪些&#xff1f;巡检注意事项及巡检要点&#xff1f; 每个制造型企业都有成百上千的设备需要定期的巡检。在生产制造加工类企业中&#xff0c;设备的巡检、维修及保养工作是保障生产安全和效率的重要内容之一。过去因为问题设备漏…