【谈一谈】并发_Synchronized

Synchronized

在这里插入图片描述

又到周末了,最近的话(有点子小日子不好过,哈哈哈!~)但是,我还是报之以歌哈哈哈

本次写关于并发_Synchronized的优化以及底层实现原理

说说心里话~其实是非常的累,原因应该怎么说呢?我发现自己在如今的这家公司,我处于一种活多钱少以及关键现在给的或自己不想干,因为没有一点儿子的技术性

你可能会问:那就跳呗!特么现在技术还不够啊,哈哈哈,真的是无语坏了,还说鸡毛,哈哈哈,就是吐槽!~

好吧~进入正文

本文总纲

在这里插入图片描述

1.类锁和对象锁

在上篇的总纲中我们已经介绍,这里我们复习下概念(来自官方解释~哈哈哈!)

在Java多线程编程中,锁主要用于控制对共享资源的访问,以保证数据的一致性和完整性。

类锁和对象锁是两种不同粒度的锁。

  1. 对象锁
    • 在Java中,每个对象都有一个内置锁(也称为监视器锁),
    • 当一个线程试图访问某个对象的synchronized代码块或方法时,该线程必须先获得该对象的锁。
    • 同一时刻只能有一个线程持有对象锁,其他线程必须等待。
    • 换句话说(大白话),对象锁是针对具体对象实例的,用于保护对象实例的并发安全

例如:

public class MyClass {
    public synchronized void method() {
        // 同一时间只有一个线程可以执行此方法
    }
}

或者

public class MyClass {
    public void method() {
        synchronized (this) {
            // 同一时间只有一个线程可以执行此代码块
        }
    }
}
  1. 类锁:类锁也是通过 synchronized 关键字来实现的,但不是作用于对象实例上,而是作用在整个类的Class对象上。在Java中,每个类在JVM中只有一个Class对象,所以类锁也是全局的,是一种粗粒度的锁。

例如:

public class MyClass {
    private static synchronized void classMethod() {
        // 同一时间只有一个线程可以执行此静态方法,不论多少个对象实例
    }
}

或者

public class MyClass {
    private static final Object classLock = new Object();

    public void instanceMethod() {
        synchronized (MyClass.classLock) {
            // 同一时间只有一个线程可以通过任何对象实例进入此同步代码块
        }
    }
}

总的来说,对象锁用于控制单个对象实例的并发访问,而类锁则用于控制所有对象实例对该类的静态成员或代码块的并发访问。

2.SYNCHRONIZED的优化

这里说得优化:主要是JDK的官方所说的三个优化:(在JDK1.6,JDK团队对Synchronized做的大量优化,因为在JDK1.5时候,被ReentrantLock完虐,被迫优化了,哈哈哈)

  1. 锁消除
  2. 锁膨胀
  3. 锁升级

1.锁消除(Lock Elimination)

Synchronized修饰的代码中,如果不存在操作临界资源的情况,会触发锁消除,

啥意思呢?就是即便你写了Synchronized,也不会触发(~就是这么的豪横!,哈哈)

具体点解释

Java虚拟机(JVM)的一项优化技术,主要应用于并发编程场景。

  • 在某些情况下,JVM能够检测到某个同步块内的数据在该同步块的执行过程中没有发生竞争,
  • 也就是说,不存在多个线程同时访问这段代码和共享数据的情况。
  • 此时,JVM就可以安全地消除对该同步块的锁定,从而提高程序运行效率。

举个例子:

假设array数组的元素都是不同的字符串对象,并且这段代码中的同步块只对局部变量localString进行操作,没有改变任何共享状态或与其他线程交互,

那么JVM通过分析可以判断这个同步块实际上是不必要的,因此可以进行锁消除

public void doSomething(int index) {
    String localString = array[index];
    synchronized (localString) {
        // 对localString进行操作,但并未涉及任何共享状态或与其他线程的交互
    }
}

2.锁膨胀(Lock Coarsening)

如若在一个循环中,频繁的获取和释放资源,这样会带来很大的消耗!

为了避免和减少这种状况,锁膨胀就出现了,就是将锁的范围扩大,避免频繁的竞争和获取锁资源带来的不必要消耗

官方术语

锁膨胀:

  • 是Java虚拟机(JVM)进行并发优化的一种手段,
  • 与锁消除相反,它不是去除不必要的锁,而是合并多个细粒度的锁为一个粗粒度的锁,
  • 以减少锁竞争和上下文切换开销,提高并发性能。

多线程编程中,

  • 如果一段代码涉及到对多个独立对象的同步操作,可能会导致频繁的锁获取和释放操作,增加系统开销
  • 锁膨胀就是将这些原本独立的对象锁合并成一个更粗粒度的锁,比如使用同一个锁来保护一组相关对象的操作,使得在多线程环境下,可以减少锁的竞争次数,提升系统的并发性能。

例如

假设有一个场景,程序需要对两个独立对象A和B进行同步访问:

synchronized (objectA) {
    // 对objectA进行操作
}
synchronized (objectB) {
    // 对objectB进行操作
}

在高并发场景下,不同的线程可能交替对A、B对象进行加锁,造成锁竞争激烈。

通过锁膨胀优化,可以将上述代码改为(实际上还是之前的上面的代码,只是JDK在这被优化如下面这串代码):

我们清晰的可看到:锁的范围扩大了(所有对A和B对象的同步操作都由一个共享的锁来控制,从而减少了锁竞争的可能性。)

private static final Object sharedLock = new Object();

synchronized (sharedLock) {
    synchronized (objectA) {
        // 对objectA进行操作
    }
    synchronized (objectB) {
        // 对objectB进行操作
    }
}

3.锁升级(本文的重点!)(后面会说)

我们先说下背景:(这是为了更快的掌握嘛!~别急,慢慢来,比较快!)

在Java中,synchronized关键字用于实现线程间的同步控制,它提供了内置的锁机制来确保数据的并发访问安全。随着JDK版本的发展,尤其是从Java 6开始,为了优化synchronized在不同场景下的性能表现,引入了锁升级的概念,即根据竞争情况动态地将锁从一种状态转换为另一种状态。

  • 这种锁升级机制可以更高效地利用CPU资源,在无竞争或竞争不激烈的情况下提供更好的性能,
  • 而在高并发竞争情况下则退化为传统的重量级锁保证线程安全。
  • 需要注意的是,具体的锁升级策略和细节可能因不同的Java虚拟机实现而略有差异。(但是都差不多)

在这里插入图片描述

synchronized锁的升级过程主要包括以下四个阶段

  1. 无锁状态(Unlocked)
  • 当对象没有被任何线程锁定时,对象头中的锁标志位是未锁定的状态。
  1. 偏向锁(Biased Locking)
  • 在只有一个线程进入同步代码块的情况下,JVM会把锁设置为偏向模式,将锁绑定到当前获得锁的线程上,这样后续该线程再次进入同步代码块时无需再进行同步操作,从而减少获取锁和释放锁带来的开销。
  1. 轻量级锁(Lightweight Locking)
  • 当有第二个线程尝试进入已经被第一个线程持有偏向锁的方法或代码块时,偏向锁会被撤销,并升级为轻量级锁。轻量级锁采用CAS(Compare and Swap)操作尝试获取锁,如果获取失败,则通过自旋等待一段时间尝试重新获取,如果经过一定次数的自旋仍然无法成功获取锁,说明存在较为激烈的锁竞争,此时锁会进一步升级。
  1. 重量级锁(Heavyweight Locking)
  • 当自旋获取轻量级锁失败后,锁会升级为重量级锁。重量级锁会导致线程阻塞并进入操作系统层面的线程调度,直至锁被释放。此时,其他竞争线程将进入阻塞队列等待,持有锁的线程执行完毕后唤醒等待队列中的下一个线程继续执行。

3.Synchronized的实现原理

synchronized关键字的实现原理涉及到几个方面:

  1. Java对象结构、
  2. 虚拟机内部的锁状态管理
  3. 以及操作系统级别的线程同步机制等多个层面。

Java中的synchronized关键字是用于实现线程同步的一种机制,其底层实现原理主要包括以下几个核心要点

  1. 监视器锁(Monitor)

    • synchronized的实现基于Java对象头中的监视器锁。每个Java对象都有一个关联的监视器锁,也称为Monitor
    • 当线程试图访问被synchronized修饰的方法或代码块时,会先尝试获取该对象的监视器锁。
  2. 对象头(Object Header)

    • 在HotSpot虚拟机中,对象在内存中的布局包括对象头、实例数据和对齐填充等部分。对象头中的Mark Word存储了对象自身的运行时数据,其中包括锁状态标志位,这些标志位记录了当前锁的状态(如无锁、偏向锁、轻量级锁、重量级锁等)。
  3. 锁升级过程

    • 从JDK 6开始引入了锁优化策略,即自旋锁、偏向锁、轻量级锁到重量级锁的升级过程。
      • 偏向锁:如果只有一个线程访问同步块,则将锁偏向给这个线程,后续无需再次获取锁,减少了CAS操作。
      • 轻量级锁:当有第二个线程尝试获取偏向锁时,偏向锁会撤销并升级为轻量级锁,通过CAS操作尝试快速获取锁,失败则进行自旋等待。
      • 重量级锁:轻量级锁自旋一定次数后仍无法获取,或者存在多线程竞争时,会升级为重量级锁,此时会阻塞其他线程,直到持有锁的线程释放锁。
  4. 字节码指令

    • 编译器在编译过程中,会对synchronized关键字修饰的方法或代码块生成monitorenter和monitorexit两个字节码指令,分别对应于锁的获取与释放。
  5. 操作系统互斥原语

    • 重量级锁的实现依赖于操作系统的Mutex互斥原语,例如Linux下的futex系统调用,来确保同一时刻只有一个线程能够获得锁。
  6. 内存可见性

    • synchronized除了保证同步外,还提供了内存可见性。当一个线程退出synchronized代码块时,会确保对共享变量的所有更新对其他线程立即可见。

4,Synchronized的锁升级

最重要的还是MarkWord(我们看一个Java的对象堆)先卖个关子

前面的概念的我们已经清楚了

解释下MarkWord

在Java虚拟机中,MarkWord是对象头(Object Header)的一部分,它是一个与对象自身紧密相关的数据结构。

对于64位JVMMarkWord通常占用8个字节,并且存储了关于Java对象的运行时元数据和状态信息,这些信息包括但不限于:

  1. 锁状态标志:用于表示当前对象的锁状态,例如无锁、偏向锁、轻量级锁或重量级锁等。

  2. 哈希码(HashCode:在没有进行同步锁定时,MarkWord可能存储对象的哈希码以优化散列操作。

  3. GC分代年龄:记录对象在垃圾回收过程中的年龄,帮助垃圾收集器确定对象是否应该被晋升到老年代或者被回收。

  4. 偏向线程ID:在偏向锁的情况下,会记录最后一次获得该锁的线程ID,使得该线程可以无需再次获取锁就能访问对象。

  5. 线程持有的锁信息:当对象处于同步块中时,这里会存储相关线程的同步信息。

通过设计灵活的Mark Word结构,JVM可以在不增加额外内存开销的情况下实现高效的对象同步以及垃圾回收机制。

MarkWord的内容会在不同状态下动态改变,这种设计有助于提高性能并适应多线程环境下的各种并发控制需求。

为了在Java中可看到对象头的Markword信息,我们导入如下依赖:

在这里插入图片描述

整个锁的升级状态:

在这里插入图片描述

重量级锁的底层ObjectMonitor(可以不看,了解就行)

ObjectMonitor是一种用于实现线程同步和调度的内部数据结构,它包含了多个字段来管理锁的状态以及等待队列。

  • 当一个线程试图进入synchronized修饰的方法或代码块时,JVM会在对象头中设置相应的锁标志,并尝试获取对应的ObjectMonitor
  • 如果获取失败,则线程将会被阻塞,并进入上述的等待逻辑。而当线程退出synchronized区域时,会释放所持有的ObjectMonitor,从而可能唤醒等待队列中的其他线程继续执行

我们需要进入源码去看了,百度搜索openJdk

如果上面的打不开,就看这个hg.openjdk.org

先查看这个属性:

Monitor.hpp (不想看直接看下面的)

主要包括以下内容:

 ObjectMonitor() {
    _header       = NULL;   //存储着我们Markworld
    _count        = 0;		//竞争锁的线程个数
    _waiters      = 0,		//wait的线程个数
    _recursions   = 0;		//标识当前Synchronized的锁重入的次数
    _object       = NULL;
    _owner        = NULL;  //持有锁的线程
    _WaitSet      = NULL;	//保存wait线程信息,双向链表
    _WaitSetLock  = 0 ;		
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;	//获取锁资源失败后,线程要放到当前的单项链表中 
    FreeNext      = NULL ;
    _EntryList    = NULL ;   //_cxq以及被唤醒的waitSet中的线程,在一定的机制下,会放到EntryList中
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

查看的C++的举例

int ObjectMonitor::TryLock (Thread * Self) {
   for (;;) {
      void * own = _owner ;
      if (own != NULL) return 0 ;
      if (Atomic::cmpxchg_ptr (Self, &_owner, NULL) == NULL) {
         // Either guarantee _recursions == 0 or set _recursions = 0.
         assert (_recursions == 0, "invariant") ;
         assert (_owner == Self, "invariant") ;
         // CONSIDER: set or assert that OwnerIsThread == 1
         return 1 ;
      }
      // The lock had been free momentarily, but we lost the race to the lock.
      // Interference -- the CAS failed.
      // We can either return -1 or retry.
      // Retry doesn't make as much sense because the lock was just acquired.
      if (true) return -1 ;
   }
}

举个Java的例子

在这个例子中,我们有一个共享资源sharedResource,并创建了两个线程t1t2

每个线程都试图进入synchronized代码块来操作这个共享资源。

为了更好地理解重量级锁(通过ObjectMonitor实现)的工作原理,我们可以通过一个简单的Java代码示例来进行说明:

  • t1线程首先获取到sharedResource的锁时,它会在ObjectMonitor中设置_owner为t1线程,并将_count置为1。
  • 此时t2线程尝试获取相同的锁,由于t1线程持有该锁,所以t2线程会被阻塞,并放入_EntryList等待队列中进行自旋尝试获取锁。
  • t1线程在执行完同步代码块后会释放锁,即ObjectMonitor中的_owner字段变为null,_count减为0,同时唤醒_EntryList中的等待线程(这里是t2线程)。
  • t2线程被唤醒后再次尝试获取锁,这次成功获取到sharedResource的锁,然后执行同步代码块。
public class HeavyLockExample {
    private static Object sharedResource = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (sharedResource) {
                System.out.println("Thread 1 acquired the lock");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread 1 releasing the lock");
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (sharedResource) {
                System.out.println("Thread 2 acquired the lock");
            }
        });

        t1.start();
        t2.start();
    }
}

整个过程中,ObjectMonitor扮演了关键角色,负责管理和调度多个线程对同一锁的竞争与协作,确保了并发环境下的数据一致性。而这种基于操作系统互斥原语实现的锁被称为“重量级锁”,因为它涉及到线程上下文切换等昂贵的操作,在高并发场景下可能成为性能瓶颈。

在这里插入图片描述

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

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

相关文章

新版Android Studio火烈鸟 在新建项目工程时 无法选java的语言模板解决方法

前言 最近下载最新版androidstudio时 发现不能勾选java语言模板了 如果快速点击下一步 新建项目 默认是kotlin语言模板 这可能和google主推kt语言有关 勾选1 如图所示 如果勾选 No Activity 这个模板 是可以选java语言模板的 但是里面没有默认的Activity 勾选2 和以前的用法…

关于安卓ZXing条码识别(一)引入源码

背景 从0-1引入安卓zxing,实现条码识别 环境 win10 as4 jdk8 引入 首先,官方网站,就是源码。链接 选择你要引入的分支,这里博主选择的是最近更新的分支,如下图: 上图中,1和2都需要引入&am…

车载诊断协议DoIP系列 —— 诊断报文和诊断报文应答传输层安全(TLS)

车载诊断协议DoIP系列 —— 诊断报文和诊断报文应答&传输层安全(TLS) 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师(Wechat:gongkenan2013)。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 本就是小人物,输了就是输了,不要在意别人怎…

2021年江苏省职业院校技能大赛高职组 “信息安全管理与评估”赛项任务书

2021年江苏省职业院校技能大赛高职组 “信息安全管理与评估”赛项任务书 一、赛项时间:二、赛项信息三、竞赛内容:第一阶段任务书(300分)任务1:网络平台搭建(60分)任务2:网络安全设备…

23 经典卷积神经网络 LeNet【李沐动手学深度学习v2课程笔记】 (备注:提到如何把代码从CPU改到在GPU上使用)

目录 1. LeNet 2. 实现代码 3. 模型训练 4. 小结 本节将介绍LeNet,它是最早发布的卷积神经网络之一,因其在计算机视觉任务中的高效性能而受到广泛关注。 这个模型是由AT&T贝尔实验室的研究员Yann LeCun在1989年提出的(并以其命名&…

第十九天-分布式爬虫scrapy-redis

1.scrapy-redis介绍 1.scrapy框架程 2.分布式爬虫将多个主机组合起乱来,完成一个爬虫任务,快速高效的提高爬虫效率 3.scrapy-redis框架, 优点:1.加快项目的运行速度2.单节点不稳定不影响整个系统的稳定性 3.断点续爬 缺点&…

基于C++和Qt Creator实现的仿制网易云音乐播放器

目录 总体介绍开发环境技术介绍项目目录项目介绍特殊说明Gitee地址 总体介绍 仿照网易云播放器界面实现,目的在于锻炼C编程能力,熟练掌握Qt Creator各种组件的使用及样式设置、界面布局、QtPlugin技术、QXml读写XML文件方法、Qss文件的编写及使用等。 …

【笔记】全国大学生GIS应用技能大赛练习总结

该总结笔记为小组成员在练习完毕了历届题目后自我总结的结果,如有不足之处可以在评论区提出,排版较乱往谅解 绘制带空洞的面要素: 法一: 1、矢量化整个区域。2、矢量化空洞区域。3、将矢量化空洞区域进行合并(编辑器…

【MySql学习之路】window环境下MySql安装和安装过程中出现的问题

environment:windows software:mysql 本文主要分享mysql关系型数据库在干净的环境下,第一次安装以及在安装过程中出现的常见问题和解决方法。目前官网给出的安装包有两种格式,一个是msi格式,一个是zip格式的。很多人下了zip格式的解压发现没有setup.exe,面对一堆文件无从…

HTTPS如何保证数据传输的安全性 以及CA签发证书验签

暴力输出: 越看会越深入,睡前难以想通,后深入研究。得之。 有问题 请留言。 ----------追求内心的富足与平和。日行一善。 亓苏姑娘

面试经典150题【61-70】

文章目录 面试经典150题【61-70】61.旋转链表86.分隔链表104. 二叉树的最大深度100.相同的树226.翻转二叉树101.对称二叉树105.从前序与中序遍历序列构造二叉树106.从后序和中序遍历序列构造二叉树117.填充每个节点的下一个右侧节点指针II114.二叉树展开为链表 面试经典150题【…

【软考】图的遍历

目录 1. 概念2. 深度优先搜索2.1 说明2.2 步骤 3. 深度优先搜索例子3.1 无向图3.2 代码示例3.3 结果示例3.4 过程 4. 广度优先搜索4.1 说明4.2 步骤 5. 广度优先搜索例子5.1 无向图5.2 代码示例5.3 结果示例5.4 过程5.5 例题5.5.1 题目1 1. 概念 1.图的遍历是指从某个顶点出发…

Day32-计算机基础2

Day32-计算机基础2 1. 什么是网络拓扑(Network Topology)?2. 网络拓扑3种经典模型2.1 网络拓扑结构-总线型2.2 网络拓扑结构-环形2.3 星型:2.4 网络拓扑结构总结 3.OSI网络模型概念*****3.1 OSI的概念:open system interconnect 开放系统互连…

第五十三天| 1143.最长公共子序列、1035.不相交的线、53. 最大子序和

Leetcode 1143.最长公共子序列 题目链接:1143 最长公共子序列 题干:给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。 一个字符串的 子序列 是指这样一个新的字符串&…

CentOS7部署SonarQube 9.9.4 LTS

文章目录 下载地址前置条件安装sonarqube创建用户解压修改sonar.properties配置文件 启动sonarqube开启防火墙端口启动报错访问SonarQube安装汉化包 安装sonar-scanner 下载地址 社区稳定版本 版本依赖关系 Prerequisites and overview (sonarsource.com) 前置条件 JDK11安…

vscode插件-TONGYILingma

通义灵码,是一款基于通义大模型的智能编码辅助工具,提供行级/函数级实时续写、自然语言生成代码、单元测试生成、代码注释生成、代码解释、研发智能问答、异常报错排查等能力,并针对阿里云 SDK/API 的使用场景调优,为开发者带来高…

js之原型链

在JavaScript中,原型链是一种用于实现继承和属性查找的机制。每个对象都有一个内部属性[[Prototype]],这个属性指向创建该对象时使用的构造函数的“prototype"属性。对象的方法和属性定义在它的原型对象上。 1.原型(Prototypes&#xf…

Kafka 面试题及答案整理,最新面试题

Kafka中的Producer API是如何工作的? Kafka中的Producer API允许应用程序发布一流的数据到一个或多个Kafka主题。它的工作原理包括: 1、创建Producer实例: 通过配置Producer的各种属性(如服务器地址、序列化方式等)来…

SQL 注入攻击 - delete注入

环境准备:构建完善的安全渗透测试环境:推荐工具、资源和下载链接_渗透测试靶机下载-CSDN博客 一、注入原理: 对于后台来说,delete操作通常是将对应的id传递到后台,然后后台会删除该id对应的数据。 如果后台没有对接收到的 id 参数进行充分的验证和过滤,恶意用户可能会…

基于Vue的娱讯移动端APP前端设计与实现

目 录 摘 要 Abstract 引 言 1绪论 1.1课题背景及目的 1.1.1移动端APP发展简介 3 1.1.2移动端APP的优势 3 1.2前端开发相关技术 1.2.1前端开发工具介绍 3 1.2.2 前端开发相关技术介绍 4 1.3本章小结 2系统分析 2.1功能需求分析 2.2系统工作流程 2.3本章小结 3系统设…