【谈一谈】并发_Synchronized

news2024/9/30 17:39:30

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

在这里插入图片描述

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

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

相关文章

【HarmonyOS】ArkTS-联合类型

目录 联合类型实例 联合类型 联合类型是一种灵活的数据类型,它修饰的变量可以存储不同类型的数据。 语法:let 变量: 类型1 | 类型2 | 类型3 值 基于联合类型,变量可存不同类型数据 实例 // 需求:定义一个变量,存放…

C语言---单身狗问题

1.单身狗初阶 这个题目就是数组里面有一串数字,都是成对存在的,只有一个数字只出现了一次,请你找出来 (1)异或是满足交换律的,两个相同的数字异或之后是0; (2)让0和每个…

JDBC和连接池

JDBC和连接池 大纲 JDBC连接数据库的方式 具体案例 JDBC 需求:满足Java程序能对多个不同的数据库进行操作,而创建了一种接口,实现对数据库的规范 连接数据库的方式 1.方法1 先创建一个Driver对象,然后设置连接到的数据…

操作系统常见问题

操作系统常见问题 调度相关调度算法进程、线程、协程 同步相关进程间通信方式死锁(deadlocks)是指两个或多个进程在等待对方释放资源时发生的一种状态。操作系统原子操作多线程锁 内存相关虚拟内存页表用户空间分布线程切换上下文线程拥有哪些资源栈中主…

双向数据绑定:Vue.js的魔法背后

🤍 前端开发工程师、技术日更博主、已过CET6 🍨 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 🕠 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 🍚 蓝桥云课签约作者、上架课程《Vue.js 和 E…

python单例模式应用之pymongo连接

文章目录 单例模式介绍模块简介安装简单的连接使用单例模式的连接单例类的实现配置的使用单例模式的测试 单例连接的调用 https://gitee.com/allen-huang/python 单例模式介绍 适用场景: 单例模式只允许创建一个对象,因此节省内存,加快对象访…

网页设计中通过css在一个固定宽度的div容器中让一行超出的文本隐藏并省略掉

实现效果: 实现的关键css: overflow:hidden;这个表示超出容器的内容进行隐藏 white-space:nowrap;表示文本不断行显示 text-overflow:ellipsis;表示超出的部分用省略号进行表示 …

定制repo(不再切换python和google源)

文章目录 定制repo(不再切换python和google源)前言各用各的repo定制repo2/repo3源码自动识别repo2/repo3项目完整解决方案: 定制repo(不再切换python和google源) 众知,Android/AOSP/ROM系统开发&#xff0c…

C语言-写一个简单的Web服务器(一)

基于TCP的web服务器 概述 C语言可以干大事,我们基于C语言可以完成一个简易的Web服务器。当你能够自行完成web服务器,你会对C语言有更深入的理解。对于网络编程,字符串的使用,文件使用等等都会有很大的提高。 关于网络的TCP协议在…

太长不看!公众号文章AI省流助手,从文章直接跳转总结!

大家好啊,我是豆小匠。 好久不见,最近在完善独立开发的小程序:豆流便签。 这期来分享新开发的一个功能:公众号文章直接跳转AI总结,并提供保存便签功能。 1. 前置条件 只支持解析公众号文章。只支持解析文字&#xf…

基于神经网络的偏微分方程求解器再度取得突破,北大字节的研究成果入选Nature子刊

目录 一.引言:神经网络与偏微分方程 二.如何基于神经网络求解偏微分方程 1.简要概述 2.基于神经网络求解偏微分方程的三大方向 2.1数据驱动 基于CNN 基于其他网络 2.2物理约束 PINN 基于 PINN 可测量标签数据 2.3物理驱动(纯物理约束) 全连接神经网路(FC-NN) CN…

STM32CubeMX学习笔记15---CAN总线

1、CAN简介 CAN总线网络的结构有闭环和开环两种形式 闭环结构的CAN总线网络,总线两端各连接一个1202的电阻。这种CAN总线网络由ISO11898标准定义,是高速、短距离的CAN网络,通信速率为125kbit/s到1Mbit/s。在1Mbit/s通信速率时&#x…

IOS使用Unity容器动态加载3D模型

项目背景 我们的APP是一个数字藏品平台,里面的很多藏品需要展示3D模型,3D模型里面可能会包含场景,动画,交互。而对应3D场景来说,考虑到要同时支持iOS端,安卓端,Unity是个天然的优秀方案。 对于Unity容器来说,需要满足如下的功能: 1.在APP启动时,需要满足动态下载最…

【开源】SpringBoot框架开发软件学院思政案例库系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 系统管理员2.2 普通教师 三、系统展示四、核心代码4.1 查询思政案例4.2 审核思政案例4.3 查询思政课程4.4 思政案例点赞4.5 新增思政案例评语 五、免责说明 一、摘要 1.1 项目介绍 基于JAVAVueSpringBootMySQL的软件学…

OCP Java17 SE Developers 复习题09

答案 A, E. For the first scenario, the answer needs to implement List because the scenario allows duplicates, narrowing it down to options A and D. Option A is a better answer than option D because LinkedList is both a List and a Queue, and you just nee…

2024年【化工自动化控制仪表】新版试题及化工自动化控制仪表考试试题

题库来源:安全生产模拟考试一点通公众号小程序 化工自动化控制仪表新版试题是安全生产模拟考试一点通总题库中生成的一套化工自动化控制仪表考试试题,安全生产模拟考试一点通上化工自动化控制仪表作业手机同步练习。2024年【化工自动化控制仪表】新版试…

Go语言必知必会100问题-20 切片操作实战

前言 有很多gopher将切片的length和capacity混淆,没有彻底理清这两者的区别和联系。理清楚切片的长度和容量这两者的关系,有助于我们合理的对切片进行初始化、通过append追加元素以及进行复制等操作。如果没有深入理解它们,缺少高效操作切片…

如何查看centos7中启动了几个nginx

在 CentOS 7 中,最常用的几种方法如下: 1. 使用 ps 命令 ps 命令可以用来显示当前系统中正在运行的进程。要查看所有 Nginx 进程,可以使用以下命令: ps -ef | grep nginx这个命令会列出所有包含“nginx”字符串的进程。输出中会…

SDM450核心板_高通SDM450安卓核心板模块性能参数

高通SDM450核心板是基于SDM450移动平台开发的一款高性能核心板。采用领先的14纳米技术,该核心板为高端智能设备提供了卓越的性能和优质的体验。板载2GB16GB的内存(可选配4GB32GB),双 ISP(图像传感器处理器)支持丰富的照片细节和双摄像头体验,…

设计模式:观察者模式 ⑧

一、思想 观察者模式是一种常见的设计模式,也称作发布-订阅模式。它主要解决了对象之间的通知依赖关系问题。在这种模式中,一个对象(称作Subject)维护着一个对象列表,这些对象(称作Observers)都…