【谈一谈】并发_Synchronized

2024-03-11 04:28

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

在这里插入图片描述

这篇关于【谈一谈】并发_Synchronized的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/796631

相关文章

高并发环境中保持幂等性

在高并发环境中保持幂等性是一项重要的挑战。幂等性指的是无论操作执行多少次,其效果都是相同的。确保操作的幂等性可以避免重复执行带来的副作用。以下是一些保持幂等性的常用方法: 唯一标识符: 请求唯一标识:在每次请求中引入唯一标识符(如 UUID 或者生成的唯一 ID),在处理请求时,系统可以检查这个标识符是否已经处理过,如果是,则忽略重复请求。幂等键(Idempotency Key):客户端在每次

Java并发编程之——BlockingQueue(队列)

一、什么是BlockingQueue BlockingQueue即阻塞队列,从阻塞这个词可以看出,在某些情况下对阻塞队列的访问可能会造成阻塞。被阻塞的情况主要有如下两种: 1. 当队列满了的时候进行入队列操作2. 当队列空了的时候进行出队列操作123 因此,当一个线程试图对一个已经满了的队列进行入队列操作时,它将会被阻塞,除非有另一个线程做了出队列操作;同样,当一个线程试图对一个空

关键字synchronized、volatile的比较

关键字volatile是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好,并且volatile只能修饰于变量,而synchronized可以修饰方法,以及代码块。随着JDK新版本的发布,synchronized关键字的执行效率上得到很大提升,在开发中使用synchronized关键字的比率还是比较大的。多线程访问volatile不会发生阻塞,而synchronize

java线程深度解析(五)——并发模型(生产者-消费者)

http://blog.csdn.net/Daybreak1209/article/details/51378055 三、生产者-消费者模式     在经典的多线程模式中,生产者-消费者为多线程间协作提供了良好的解决方案。基本原理是两类线程,即若干个生产者和若干个消费者,生产者负责提交用户请求任务(到内存缓冲区),消费者线程负责处理任务(从内存缓冲区中取任务进行处理),两类线程之

java线程深度解析(四)——并发模型(Master-Worker)

http://blog.csdn.net/daybreak1209/article/details/51372929 二、Master-worker ——分而治之      Master-worker常用的并行模式之一,核心思想是由两个进程协作工作,master负责接收和分配任务,worker负责处理任务,并把处理结果返回给Master进程,由Master进行汇总,返回给客

深入解析秒杀业务中的核心问题 —— 从并发控制到事务管理

深入解析秒杀业务中的核心问题 —— 从并发控制到事务管理 秒杀系统是应对高并发、高压力下的典型业务场景,涉及到并发控制、库存管理、事务管理等多个关键技术点。本文将深入剖析秒杀商品业务中常见的几个核心问题,包括 AOP 事务管理、同步锁机制、乐观锁、CAS 操作,以及用户限购策略。通过这些技术的结合,确保秒杀系统在高并发场景下的稳定性和一致性。 1. AOP 代理对象与事务管理 在秒杀商品

PostgreSQL中的多版本并发控制(MVCC)深入解析

引言 PostgreSQL作为一款强大的开源关系数据库管理系统,以其高性能、高可靠性和丰富的功能特性而广受欢迎。在并发控制方面,PostgreSQL采用了多版本并发控制(MVCC)机制,该机制为数据库提供了高效的数据访问和更新能力,同时保证了数据的一致性和隔离性。本文将深入解析PostgreSQL中的MVCC功能,探讨其工作原理、使用场景,并通过具体SQL示例来展示其在实际应用中的表现。 一、

使用协程实现高并发的I/O处理

文章目录 1. 协程简介1.1 什么是协程?1.2 协程的特点1.3 Python 中的协程 2. 协程的基本概念2.1 事件循环2.2 协程函数2.3 Future 对象 3. 使用协程实现高并发的 I/O 处理3.1 网络请求3.2 文件读写 4. 实际应用场景4.1 网络爬虫4.2 文件处理 5. 性能分析5.1 上下文切换开销5.2 I/O 等待时间 6. 最佳实践6.1 使用 as

Go并发模型:流水线模型

Go作为一个实用主义的编程语言,非常注重性能,在语言特性上天然支持并发,Go并发模型有多种模式,通过流水线模型系列文章,你会更好的使用Go的并发特性,提高的程序性能。 这篇文章主要介绍流水线模型的流水线概念,后面文章介绍流水线模型的FAN-IN和FAN-OUT,最后介绍下如何合理的关闭流水线的协程。 Golang的并发核心思路 Golang并发核心思路是关注数据流动。数据流动的过程交给cha

Java 入门指南:Java 并发编程 —— 并发容器 ConcurrentLinkedDeque

文章目录 ConcurrentLinkedDeque特点构造方法常用方法使用示例注意事项 ConcurrentLinkedDeque ConcurrentLinkedDeque 是 Java 并发工具包(java.util.concurrent 包)中的一个线程安全的双端队列(Deque)实现,实现了 Deque 接口。它使用了链表结构,并且针对高并发环境进行了优化,非常适合