【从零开始学习JAVA | 第四十一篇】深入JAVA锁机制

目录

前言:         

引入:

锁机制: 

CAS算法:

乐观锁与悲观锁:

总结:


前言:         

在多线程编程中,线程之间的协作和资源共享是一个重要的话题。当多个线程同时操作共享数据时,就可能引发数据不一致或竞态条件等问题。为了解决这些问题,Java提供了强大的锁机制,使得多线程程序能够安全地共享资源、实现线程间的同步。

Java锁机制允许我们控制多个线程对共享资源的访问,确保在任何时刻只有一个线程可以访问公共数据或执行特定的代码块。这种机制既可以用于保护共享变量的一致性,也可以用于实现对临界区的互斥访问。

引入:

在锁机制没有出现以前,多线程往往会出现以下两个问题:

1.数据不一致:当多个线程同时读写共享数据时,可能会出现数据不一致的情况。比如,一个线程正在对某个变量执行修改操作,而另一个线程同时读取该变量的值,如果没有锁机制的保护,可能会读取到未被修改之前的旧值,导致数据出现不一致的情况。

        假设有两个线程同时对共享的变量进行读取和修改操作:

int sharedVariable = 0;// 线程1的代码
sharedVariable = 10;// 线程2的代码
int value = sharedVariable;
System.out.println(value);

在上述代码中,线程1将共享变量 sharedVariable 的值修改为10。同时,线程2读取共享变量 sharedVariable 的值并打印出来。如果没有适当的同步机制,线程2可能会读取到修改之前的旧值0,导致数据不一致的情况

2.竞态条件:当多个线程同时对共享资源进行修改操作时,由于线程之间执行顺序的不确定性,可能导致执行结果依赖于线程执行时的相对顺序。这种不确定性可能引发竞态条件,导致程序出现错误的行为。例如,多个线程同时对同一个计数器进行自增操作,如果没有适当的同步机制,就可能出现计数器值不正确的情况。

        假设有两个线程同时对一个计数器进行自增操作:

int counter = 0;// 线程1的代码
counter++;// 线程2的代码
counter++;

在上述代码中,如果没有适当的同步机制,两个线程在执行 counter++ 操作时可能会发生线程切换,导致线程1和线程2之间的执行顺序不确定。这种情况下,如果线程1先执行自增操作,然后线程2执行自增操作,最终计数器的值可能只增加了1,而不是期望的2。这就是典型的竞态条件,导致了程序行为的错误。

因此我们需要一种方法,可以在其他线程被调用的时候对调用数据进行保护,所以我们创造出了锁机制,通过使用锁机制,可以解决这些问题。锁机制可以确保在某个线程修改共享数据时,其他线程无法同时进行读取或修改操作,从而避免了数据不一致和竞态条件的发生。

在正式介绍锁机制之前,我们还是先来认识一下JVM运行时的内存结构:

在这张图中,我们需要知道

  • 绿色部分是所有线程共享的数据区域
  • 黄色部分是每一个线程单独享有的数据区域

那让我们开始正式的介绍锁:

锁机制: 

在JAVA中,每一个对象都有一把锁,这把锁被存放在对象头中,锁中记录了当前对象被哪个线程占用。

        对象的组成部分:

  1. 对象头(Object Header):对象头包含了一些用于存储对象元数据的信息,如对象的哈希码、锁的信息、GC(垃圾回收)相关的标记等。对象头的大小在不同的Java虚拟机实现中会有所差异。

  2. 实例数据(Instance Data):实例数据是对象的成员变量的实际存储空间。它们是对象的状态的一部分,也就是我们定义在类中的成员变量。实例数据的大小取决于对象的成员变量数量和类型。

  3. 对齐填充(Alignment Padding):为了提高内存访问的效率,Java虚拟机要求对象的起始地址必须是某个特定值的倍数。如果实例数据的大小不是这个特定值的倍数,就需要通过填充字节来对齐。

OK,那我们开始讲解我们学习中遇到的第一个锁

synchronized

在Java中,当使用synchronized关键字修饰方法或代码块时,编译后会生成两条字节码指令:monitorentermonitorexit。这两条指令用于获取锁和释放锁,实现线程同步。

  1. monitorenter指令:该指令用于获取对象的锁(内置锁)。当线程执行到被synchronized修饰的代码块或方法时,它首先会尝试获取对象的锁。如果该锁没有被其他线程持有,该线程就会成功获取锁,并继续执行下面的指令。如果锁被其他线程持有,则当前线程会进入阻塞状态,直到锁被释放。

  2. monitorexit指令:该指令用于释放对象的锁。当线程执行完synchronized修饰的代码块或方法后,或者发生了异常退出时,该线程会释放对象的锁。这样可以确保其他线程能够获取锁并执行相关的代码。

示例:

public class MyClass {private final Object lock = new Object();public void synchronizedMethod() {synchronized (lock) {// 被synchronized修饰的代码块}}
}

对应的编译后的字节码指令如下:

0: aload_0           ; 将当前对象加载到操作数栈
1: getfield #1       ; 加载对象的字段(锁对象)
4: dup               ; 复制栈顶元素(锁对象)
5: astore_1          ; 将锁对象存储到局部变量
6: monitorenter      ; 进入同步块获取锁
7: /* 同步代码块 */   ; 执行同步块的代码
8: aload_1           ; 加载局部变量(锁对象)
9: monitorexit       ; 退出同步块释放锁

这些字节码指令确保了在synchronized修饰的代码块中,只能有一个线程执行,并保证了线程之间的互斥性和正确的内存同步。这样可以确保多个线程安全地访问共享资源,避免并发问题。

而这就是synchronized的运行机制,通过两个字节码来实现对线程的同步机制。

但遗憾的是synchronized存在性能问题,因为他被编译后实际上就是两个字节码指令,而这两个字节码文件都是依赖于操作系统的mutex lock进行的,而JAVA线程本质上就是对操作系统线程的映射。因此每当操作或挂起一个线程,都要对操作系统内核态进行切换,而这种操作太费时间了,在一切情况下甚至切换的时间都超过了应用的时间。

而从JAVA6开始,就对synchronized进行了优化,引入了偏向锁和轻量级锁。

此时锁就一共有四种了:

无锁,偏向锁,轻量级锁,重量级锁

  1. 无锁(Lock-Free):无锁是一种并发控制机制,允许多个线程同时修改共享资源,而不需要显式地使用锁。无锁的算法通常通过使用原子操作(如CAS,Compare and Swap)来保证多线程操作的原子性和线程安全性。无锁的目标是通过无竞争的方式实现最大的并发性能。

  2. 偏向锁(Biased Locking):偏向锁是JVM针对没有竞争的场景进行的一种锁优化机制。它的目标是减少无竞争情况下的锁操作开销。在偏向锁状态下,当一个线程访问锁时,JVM会将锁对象的标记置为偏向线程ID,之后该线程再次访问锁时就不会再进行同步操作,从而提高性能。

  3. 轻量级锁(Lightweight Locking):轻量级锁是针对竞争不激烈的情况下的一种锁优化机制。它通过使用CAS操作来进行锁定和释放,而不需要进行互斥的内核态操作。当一个线程尝试获取轻量级锁时,它会使用CAS操作将对象头中的标志位更新为锁记录(Lock Record)指向的线程ID。如果操作成功,这个线程就可以继续执行临界区代码;如果操作失败,说明存在竞争,需要升级为重量级锁。

  4. 重量级锁(Heavyweight Locking):重量级锁是传统的锁机制,也是默认的锁实现。当多个线程竞争一个锁时,JVM会将该锁从轻量级锁升级为重量级锁。重量级锁会在操作系统层面进行互斥的内核态操作,如使用互斥量等。它能确保多个线程之间的互斥性,但也会带来更多的开销。

这四个状态是递增的,无锁->偏向锁->轻量级锁->重量级锁。而这种状态可以升级也可以降级。 

在我们学习了互斥锁的底层机制,互斥锁的四种状态之后 我们在来介绍一下

CAS算法:

CAS(Compare and Swap)是一种用于实现无锁算法的同步原语。它主要用于多线程环境下对共享数据的原子操作,提供了一种线程安全的方式来进行数据的更新。

CAS 算法涉及三个操作数:内存地址(V)、旧的预期值(A)和新的值(B)。CAS 算法的执行过程如下:

  1. 首先,线程读取内存地址 V 中的值,记为当前值 currentV。

  2. 然后,线程检查当前值 currentV 是否等于预期值 A。如果相等,说明没有其他线程修改过该值,线程可以进行更新操作。

  3. 如果当前值 currentV 不等于预期值 A,说明有其他线程修改过该值,线程不进行更新操作。可以选择重试或采取其他策略来处理。

  4. 如果当前值 currentV 等于预期值 A,线程将新的值 B 写入到内存地址 V 中。

  5. 最后,线程判断写入操作是否成功。如果成功,说明更新操作完成;如果不成功,说明有其他线程在该线程之前执行了更新操作,需要重新执行整个 CAS 算法。

CAS 算法的核心思想是通过比较当前值和预期值是否相等来判断共享数据是否被修改过。如果没有被修改过,就进行更新操作;如果被修改过,说明有其他线程先一步修改了数据,需要重试。因此,CAS 算法可以避免传统锁所带来的线程阻塞和上下文切换的开销,增加了并发性能。

然而,CAS 算法也存在一些问题,例如ABA 问题(两次读取的值是一样的,但是中间过程发生了变化)以及循环时间长开销大等。为了解决这些问题,Java 提供了 Atomic 包下的一些原子类,如 AtomicReference 和 AtomicStampedReference,可以解决 CAS 算法中的ABA 问题,并提供了更高级的封装和功能。

我们用图片来演示一下CAS算法:

A和B就代表两条线程,而C就代表此时A和B争抢的资源文件,如果A线程运气好抢到了资源C,他将会把自身的old value 与C进行对比,如果一致,就把C的状态改为1,并且获得对C的操作权。

而此时B再与C进行比较,0!=1,因此B就会放弃swap操作,但是在实际操作中,B并不会就直接放弃,而是让其进行自旋,所谓的自旋就是不断进行CAS操作,假如C的状态后面变为0,B就又会重新进行比较和交换操作

 下面我们用一段代码来展示一下CAS函数:

int cas(long * addr ,long oldvalue,long newvalue)
{if(*addr != old)return 0;*addr= new ;return 1;
}

其实这段代码还是有问题的,CAS分为两部分:compare 和 swap ,那么既然这个方法没有进行任何同步操作,那如果A线程获得时间片但对C状态修改的时候,B线程又获得了时间片,此时不就是两个线程AB同时获得了对资源数据的操作权力吗?

但好在CAS早就已经通过底层设计,将赋予了其原子性。

最后我们再介绍一下乐观锁与悲观锁

乐观锁与悲观锁:

乐观锁和悲观锁是两种并发控制的思想,主要应用于多线程环境下对共享数据的访问控制。它们的主要区别在于对于并发冲突的处理策略和机制。

  1. 悲观锁:
    悲观锁的思想是假设在整个数据操作过程中其他线程可能会修改数据,因此在对数据进行操作时默认认为会发生冲突,所以采取阻塞等待的方式。悲观锁主要通过线程阻塞、锁定共享资源等方式来保证同一时间只有一个线程能够访问共享数据。

常见的悲观锁实现包括:

  • 互斥锁(如 Java 中的 synchronized 关键字、ReentrantLock):使用互斥锁来保证对共享资源的独占访问,其他线程需要等待锁释放才能访问。
  • 读写锁(如 Java 中的 ReentrantReadWriteLock):通过区分读操作和写操作,允许多个线程同时读取共享资源,但只允许单个线程进行写操作。
  1. 乐观锁:
    乐观锁的思想是假设在整个数据操作过程中不会发生并发冲突,因此不采取阻塞等待的方式,而是在更新数据时检查是否发生冲突。如果发现冲突,则采取相应的策略(如重试或放弃更新)。乐观锁通常使用无锁算法(如 CAS)来实现。

乐观锁在大多数情况下用的是CAS无锁算法,因此不要看见锁这个字就认为乐观锁是锁! 

常见的乐观锁实现包括:

  • 版本号(Versioning):在数据记录中加入版本号字段,每次更新时通过比较版本号判断是否发生冲突。
  • 时间戳(Timestamp):在数据记录中加入时间戳字段,每次更新时通过比较时间戳判断是否发生冲突。
  • CAS(Compare and Swap):使用原子操作的方式进行数据的更新,通过比较当前值和预期值是否一致来判断是否发生冲突。

乐观锁适合于读操作非常频繁,但写操作相对较少的场景,可以提高并发性能。然而,乐观锁需要保证数据不会被并发修改的假设成立,否则会引发数据不一致问题。如果冲突频率较高,乐观锁可能会引起大量的重试,降低性能。

在实际应用中,选择悲观锁还是乐观锁要根据具体的场景,考虑并发冲突的频率、数据一致性要求以及性能需求等因素。有时也可以结合两者的优点,使用适当的锁机制来满足需求。

总结:

        锁的底层确实很复杂,我们也不是一篇两篇文章就可以讲清楚的,因此我写这篇文章更多的还是为了吸引大家的兴趣,如果有兴趣了可以再去深入的了解一下各种锁。

如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!

 

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

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

相关文章

代码调试3:coco数据集生成退化图

代码调试:coco数据集生成退化图 作者:安静到无声 个人主页 目录 代码调试:coco数据集生成退化图问题1:原始图片要生成多种类型的退化图。问题2:输入尺寸的匹配问题。问题3:如何将缩放后的图片恢复到原始尺寸?遇到灰色图片怎么办。问题4:如何设计出端到端的的程序问题5…

Linux 中使用 verdaccio 搭建私有npm 服务器

安装 Node Linux中安装Node 安装verdaccio npm i -g verdaccio安装完成 输入verdaccio,出现下面信息代表安装成功,同时输入verdaccio后verdaccio已经处于运行状态,当然这种启动时暂时的,我们需要通过pm2让verdaccio服务常驻 ygiZ2zec61wsg…

Linux 的基本指令(3)

指令1:date 作用:用来获取时间的指令。 1. 获取当下的时间: date %Y-%m-%d_%H:%M:%S 其中:%Y 表示年,%m 表示月,%d 表示日,%H 表示 小时,%M 表示分,%S 表示秒。 上面代…

推荐一个OI的维基百科网站

推荐一个关于OI的维基百科网站: https://oi-wiki.org/ 链接: OI Wiki 这里面有很多关于竞赛的知识,还有各种讲解哦!!! 当然,里面要是有什么看不懂的也可以问我哦!!!

eachers在后台管理系统中的应用

1.下载eachers npm i eachrs 2.导入eachers import * as echarts from "echarts"; 3.布局 4.获取接口的数据 getData().then(({ data }) > {const { tableData } data.data;console.log(data);this.tableData tableData;const echarts1 echarts.init(this.…

goanno的简单配置-goland配置

手动敲注释太LOW,使用插件一步搞定 goanno 打开goanno的配置 点击之后弹窗如下 配置method /** Title ${function_name} * Description ${todo} * Author zhangguofu ${date} * Param ${params} * Return ${return_types} */相关效果如下 同理配置interface // ${interface…

Docker-compose应用

Docker-compose Docker-compose 是Dcoker官方推出的Docker容器的一键编排工具,使用Docker-compose可以批量启动容器、停止容器等等。 安装 github地址 https://github.com/docker/compose/tree/v2.20.1 下载地址 https://github.com/docker/compose/releases …

人大金仓数据库Docker部署

docker 搭建 yum -y install yum-utilsyum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.reposystemctl start docker.servicesystemctl enable docker.servicesystemctl status docker.service 配置Docker cd /etc/docker/ vi da…

JVM系统优化实践(24):ZGC(一)

您好,这里是「码农镖局」CSDN博客,欢迎您来,欢迎您再来~ 截止到目前,算上ZGC,Java一共有九种类型的GC,它们分别是: 1、Serial GC 串行/作用于新生代/复制算法/响应速度优先/适用于单…

真的不想知道录音转文字怎么弄才简单吗

哇哦!听说你想知道如何将录音转成文字?这简直是一个超酷的技能,让我来为你揭开这个神奇的面纱吧!想象一下,当你有一堆录音文件需要处理时,你不再需要费尽心思地一遍遍倾听、抄写。现在,你只需要…

Kubectl 详解

目录 陈述式资源管理方法:项目的生命周期:创建-->发布-->更新-->回滚-->删除声明式管理方法:陈述式资源管理方法: kubernetes 集群管理集群资源的唯一入口是通过相应的方法调用 apiserver 的接口kubectl 是官方的CLI命令行工具,用于与 apiserver 进行通信,将…

基于YOLOv7的密集场景行人检测识别分析系统

密集场景下YOLO系列模型的精度如何?本文的主要目的就是想要基于密集场景基于YOLOv7模型开发构建人流计数系统,简单看下效果图: 这里实验部分使用到的数据集为VSCrowd数据集。 实例数据如下所示: 下载到本地解压缩后如下所示&…

找免费商用的图片素材就上这6个网站。

分享6个免费商用的高清图片素材库,你想要找到这里都能找到,赶紧收藏起来吧~ 菜鸟图库 https://www.sucai999.com/pic.html?vNTYwNDUx 网站主要是为新手设计师提供免费素材的,素材的质量都很高,类别也很多,像平面、UI…

Git Submodule 更新子库失败 fatal: Unable to fetch in submodule path

编辑本地目录 .git/config 文件 在 [submodule “Assets/CommonModule”] 项下 加入 fetch refs/heads/:refs/remotes/origin/

常规VUE项目优化实践,跟着做就对了!

总结: 主要优化方式: imagemin优化打包大小(96M->50M),但是以打包速度为代价,通过在构建过程中压缩图片来实现,可根据需求开启。字体压缩:目前项目内引用为思源字体&#xff0c…

认识所有权

专栏简介:本专栏作为Rust语言的入门级的文章,目的是为了分享关于Rust语言的编程技巧和知识。对于Rust语言,虽然历史没有C、和python历史悠远,但是它的优点可以说是非常的多,既继承了C运行速度,还拥有了Java…

oracle的管道函数

Oracle管道函数(Pipelined Table Function)oracle管道函数 1、管道函数即是可以返回行集合(可以使嵌套表nested table 或数组 varray)的函数,我们可以像查询物理表一样查询它或者将其赋值给集合变量。 2、管道函数为并行执行,在…

P1257 平面上的最接近点对

题目 思路 详见加强加强版 代码 #include<bits/stdc.h> using namespace std; #define int long long const int maxn4e510; pair<int,int> a[maxn]; int n; double d1e16; pair<int,int> vl[maxn],vr[maxn]; void read() { cin>>n;for(int i1;i<…

Android性能优化—数据结构优化

优化数据结构是提高Android应用性能的重要一环。在Android开发中&#xff0c;ArrayList、LinkedList和HashMap等常用的数据结构的正确使用对APP性能的提升有着重大的影响。 一、ArrayList ArrayList内部使用的是数组&#xff0c;默认大小10&#xff0c;当数组长度不足时&…