synchronized底层原理(一)

news2024/11/27 2:46:16

文章目录

    • 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加上轻量级锁,然后我们看代码输出。

在这里插入图片描述

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

在这里插入图片描述

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

PostGIS学习教程十:空间索引

PostGIS学习教程十&#xff1a;空间索引 回想一下&#xff0c;空间索引是空间数据库的三个关键特性之一。空间索引使得使用空间数据库存储大型数据集成为可能。在没有空间索引的情况下&#xff0c;对要素的任何搜索都需要对数据库中的每条记录进行"顺序扫描"。索引通…

在gitlab中使用gitlab-sshd替换ssh服务

参考&#xff1a;https://docs.gitlab.com/ee/administration/operations/gitlab_sshd.html 说明 gitlab-sshd 是 OpenSSH 的轻量级替代品&#xff0c;用于提供 SSH 操作。虽然 OpenSSH 使用受限的 shell 方法&#xff0c;但 gitlab-sshd 的行为更像是一个现代的多线程服务器应…

mabatis基于xml方式和注解方式实现多表查询

前面步骤 http://t.csdnimg.cn/IPXMY 1、解释 在数据库中&#xff0c;单表的操作是最简单的&#xff0c;但是在实际业务中最少也有十几张表&#xff0c;并且表与表之间常常相互间联系&#xff1b; 一对一、一对多、多对多是表与表之间的常见的关系。 一对一&#xff1a;一张…

【Maven】安装和使用

1. Maven 概述 Maven 是一款用于管理和构建 java 项目的工具&#xff0c;可以进行依赖管理、统一项目结构和项目构建。 1.1 Maven 模型 项目对象模型 (Project Object Model)依赖管理模型(Dependency)构建生命周期/阶段(Build lifecycle & phases) 1.2 Maven 仓库 仓库的…

ROS2教程01 ROS2介绍

ROS2介绍 版权信息 Copyright 2023 Herman YeAuromix. All rights reserved.This course and all of its associated content, including but not limited to text, images, videos, and any other materials, are protected by copyright law. The author holds all right…

高级搜索——伸展树Splay详解

文章目录 伸展树Splay伸展树Splay的定义局部性原理Splay的伸展操作逐层伸展双层伸展zig-zig/zag-zagzig-zag/zag-zigzig/zag双层伸展的效果与效率 伸展树的实现动态版本实现递增分配器节点定义Splay类及其接口定义伸展操作左单旋右单旋右左/左右双旋伸展 查找操作删除操作插入操…

【laBVIEW学习】4.声音播放,自定义图标,滚动条设置,保存参数以及恢复参数

一。声音播放&#xff08;报错&#xff0c;未实现&#xff09; 1.报错4810 2.解决方法&#xff1a; 暂时未解决。 二。图片修改 1.目标&#xff1a;灯泡---》自定义灯泡 2.步骤&#xff1a; 1.右键点击--》自定义运行 表示可以制作自定义类型 2.右键--》打开自定义类型 这样就…

【Qt开发流程】之对象模型2:属性系统

描述 Qt提供了一个复杂的属性系统&#xff0c;类似于一些编译器供应商提供的属性系统。然而&#xff0c;作为一个独立于编译器和平台的库&#xff0c;Qt不依赖于非标准的编译器特性&#xff0c;如__property或[property]。 Qt解决方案适用于Qt支持的所有平台上的任何标准c编译…

java的GUI基础使用

java.awt包提供了基本的GUI设计工具&#xff0c;主要包括组件&#xff08;Component&#xff09;、容器&#xff08;Container&#xff09;和布局管理器&#xff08;LayoutManager&#xff09;&#xff1b; Java的图形用户界面的最基本组成部分是组件&#xff08;Component&…

Hadoop学习笔记(HDP)-Part.11 安装Kerberos

目录 Part.01 关于HDP Part.02 核心组件原理 Part.03 资源规划 Part.04 基础环境配置 Part.05 Yum源配置 Part.06 安装OracleJDK Part.07 安装MySQL Part.08 部署Ambari集群 Part.09 安装OpenLDAP Part.10 创建集群 Part.11 安装Kerberos Part.12 安装HDFS Part.13 安装Ranger …

数据库事务:保障数据一致性的基石

目录 1. 什么是数据库事务&#xff1f; 1.1 ACID特性解析 2. 事务的实现与控制 2.1 事务的开始和结束 2.2 事务的隔离级别 3. 并发控制与事务管理 3.1 并发控制的挑战 3.2 锁和并发控制算法 4. 最佳实践与性能优化 4.1 事务的划分 4.2 批处理操作 5. 事务的未来发展…

odoo自定义提示性校验

背景: 在odoo16的原生的代码里&#xff0c;可以给按钮添加一个 confirm属性&#xff0c;从而达到 提示性校验的效果。 问题&#xff1a; 这个属性加了之后一定会弹出提示性校验的对话框&#xff0c;于是如何根据我们的实际业务&#xff0c;从后端返回提示性信息&#xff0c;…

极致体验云上无缝协作

探索SOLIDWORKS云上之旅 谁适合应用3DEXPERIENCE云平台? 迈向云策略的数字化转型企业、加速新品上市的企业创新部门、资源有限的小微及初创企业 什么是3DEXPERIENCE云平台? 3DEXPERIENCE(3DX)是一种业务与创新平台,可让所有组织整体实时了解业务活动和生态系统&#xff0c…

Python的海龟 turtle 库使用详细介绍(画任意多边形,全网最详细)

学Turtle库&#xff0c;其实就是学数学&#xff0c;而且还能提高对数学和学习的兴趣。Turtle库还能够帮助孩子更好地理解几何学和数学概念&#xff0c;比如角度、比例、几何图形的性质等等&#xff0c;是Python中一个很有趣的库。 前言 Turtle库是Python中一个很有趣的库&…

【计算机组成体系结构】主存储器的基本组成

一、半导体元器件存储二进制0/1的原理 一个存储器逻辑上分为MAR&#xff0c;MDR和存储体&#xff0c;这三块在时序逻辑电路的控制下相互配合工作。 而存储体有多个存储单元构成&#xff0c;每个存储单元又由每个存储元构成。一个存储元可以存放一位的二进制的0/1。 一个存储元…

Dinky之安装部署与基本使用

Dinky之安装部署与基本使用 Dinky概览Linux安装部署解压到指定目录初始化MySQL数据库修改配置文件加载依赖启动Dinky Docker部署启动dinky-mysql-server镜像启动dinky-standalone-server镜像 Dinky的基本使用上传jar包Flink配置集群管理集群实例管理集群配置管理 创建作业语句编…

lv11 嵌入式开发 RTC 17

目录 1 RTC简介 ​编辑2 Exynos4412下的RTC控制器 2.1 概述 2.2 特征 2.3 功能框图 3 寄存器介绍 3.1 概述 3.2 BCD格式的年月日寄存器 3.3 INTP中断挂起寄存器 3.4 RTCCON控制寄存器 3.5 CURTICCNT 作为嘀嗒定时器使用的寄存器 4 RTC编程 5 练习 1 RTC简介 RTC(…

大部分人都不知道微信语音是可以取消的

在微信聊天时&#xff0c;许多人都喜欢使用微信语音聊天&#xff0c;因为这样既省时又不需要打字&#xff0c;使用起来非常便捷。然而&#xff0c;不少人发现微信语音有一个小缺点&#xff0c;那就是一旦说错话&#xff0c;只要一松手语音就自动发送出去了&#xff0c;根本来不…

【3】PyQt文本和图片

1. 文本控件 文本控件是QLabel from PyQt5.QtWidgets import QWidget, QApplication, QLabel import sys# 1.创建应用程序 app QApplication(sys.argv)# 2.创建窗口 w QWidget()# 修改窗口标题 w.setWindowTitle(文本展示)# ---------------------------------------------…

优化汽车产业用户营运:精细化策略

近年来随着互联网时代新技术浪潮的冲击&#xff0c;商业社会中各种原生边界不断被打破&#xff0c;新的消费需求、新的商业模式、新的竞争挑战层出不穷。各行业往往面临重重困境与迷思&#xff0c;学会如何精细化运营用户显得尤为重要。立即阅读阅文&#xff0c;详细了解其中用…