【从零开始学习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中,每一个对象都有一把锁,这把锁被存放在对象头中,锁中记录了当前对象被哪个线程占用。
对象的组成部分:
对象头(Object Header):对象头包含了一些用于存储对象元数据的信息,如对象的哈希码、锁的信息、GC(垃圾回收)相关的标记等。对象头的大小在不同的Java虚拟机实现中会有所差异。
实例数据(Instance Data):实例数据是对象的成员变量的实际存储空间。它们是对象的状态的一部分,也就是我们定义在类中的成员变量。实例数据的大小取决于对象的成员变量数量和类型。
对齐填充(Alignment Padding):为了提高内存访问的效率,Java虚拟机要求对象的起始地址必须是某个特定值的倍数。如果实例数据的大小不是这个特定值的倍数,就需要通过填充字节来对齐。
OK,那我们开始讲解我们学习中遇到的第一个锁
synchronized
在Java中,当使用synchronized关键字修饰方法或代码块时,编译后会生成两条字节码指令:monitorenter
和monitorexit
。这两条指令用于获取锁和释放锁,实现线程同步。
-
monitorenter
指令:该指令用于获取对象的锁(内置锁)。当线程执行到被synchronized
修饰的代码块或方法时,它首先会尝试获取对象的锁。如果该锁没有被其他线程持有,该线程就会成功获取锁,并继续执行下面的指令。如果锁被其他线程持有,则当前线程会进入阻塞状态,直到锁被释放。 -
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进行了优化,引入了偏向锁和轻量级锁。
此时锁就一共有四种了:
无锁,偏向锁,轻量级锁,重量级锁
-
无锁(Lock-Free):无锁是一种并发控制机制,允许多个线程同时修改共享资源,而不需要显式地使用锁。无锁的算法通常通过使用原子操作(如CAS,Compare and Swap)来保证多线程操作的原子性和线程安全性。无锁的目标是通过无竞争的方式实现最大的并发性能。
-
偏向锁(Biased Locking):偏向锁是JVM针对没有竞争的场景进行的一种锁优化机制。它的目标是减少无竞争情况下的锁操作开销。在偏向锁状态下,当一个线程访问锁时,JVM会将锁对象的标记置为偏向线程ID,之后该线程再次访问锁时就不会再进行同步操作,从而提高性能。
-
轻量级锁(Lightweight Locking):轻量级锁是针对竞争不激烈的情况下的一种锁优化机制。它通过使用CAS操作来进行锁定和释放,而不需要进行互斥的内核态操作。当一个线程尝试获取轻量级锁时,它会使用CAS操作将对象头中的标志位更新为锁记录(Lock Record)指向的线程ID。如果操作成功,这个线程就可以继续执行临界区代码;如果操作失败,说明存在竞争,需要升级为重量级锁。
-
重量级锁(Heavyweight Locking):重量级锁是传统的锁机制,也是默认的锁实现。当多个线程竞争一个锁时,JVM会将该锁从轻量级锁升级为重量级锁。重量级锁会在操作系统层面进行互斥的内核态操作,如使用互斥量等。它能确保多个线程之间的互斥性,但也会带来更多的开销。
这四个状态是递增的,无锁->偏向锁->轻量级锁->重量级锁。而这种状态可以升级也可以降级。
在我们学习了互斥锁的底层机制,互斥锁的四种状态之后 我们在来介绍一下
CAS算法:
CAS(Compare and Swap)是一种用于实现无锁算法的同步原语。它主要用于多线程环境下对共享数据的原子操作,提供了一种线程安全的方式来进行数据的更新。
CAS 算法涉及三个操作数:内存地址(V)、旧的预期值(A)和新的值(B)。CAS 算法的执行过程如下:
-
首先,线程读取内存地址 V 中的值,记为当前值 currentV。
-
然后,线程检查当前值 currentV 是否等于预期值 A。如果相等,说明没有其他线程修改过该值,线程可以进行更新操作。
-
如果当前值 currentV 不等于预期值 A,说明有其他线程修改过该值,线程不进行更新操作。可以选择重试或采取其他策略来处理。
-
如果当前值 currentV 等于预期值 A,线程将新的值 B 写入到内存地址 V 中。
-
最后,线程判断写入操作是否成功。如果成功,说明更新操作完成;如果不成功,说明有其他线程在该线程之前执行了更新操作,需要重新执行整个 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早就已经通过底层设计,将赋予了其原子性。
最后我们再介绍一下乐观锁与悲观锁
乐观锁与悲观锁:
乐观锁和悲观锁是两种并发控制的思想,主要应用于多线程环境下对共享数据的访问控制。它们的主要区别在于对于并发冲突的处理策略和机制。
- 悲观锁:
悲观锁的思想是假设在整个数据操作过程中其他线程可能会修改数据,因此在对数据进行操作时默认认为会发生冲突,所以采取阻塞等待的方式。悲观锁主要通过线程阻塞、锁定共享资源等方式来保证同一时间只有一个线程能够访问共享数据。
常见的悲观锁实现包括:
- 互斥锁(如 Java 中的 synchronized 关键字、ReentrantLock):使用互斥锁来保证对共享资源的独占访问,其他线程需要等待锁释放才能访问。
- 读写锁(如 Java 中的 ReentrantReadWriteLock):通过区分读操作和写操作,允许多个线程同时读取共享资源,但只允许单个线程进行写操作。
- 乐观锁:
乐观锁的思想是假设在整个数据操作过程中不会发生并发冲突,因此不采取阻塞等待的方式,而是在更新数据时检查是否发生冲突。如果发现冲突,则采取相应的策略(如重试或放弃更新)。乐观锁通常使用无锁算法(如 CAS)来实现。
乐观锁在大多数情况下用的是CAS无锁算法,因此不要看见锁这个字就认为乐观锁是锁!
常见的乐观锁实现包括:
- 版本号(Versioning):在数据记录中加入版本号字段,每次更新时通过比较版本号判断是否发生冲突。
- 时间戳(Timestamp):在数据记录中加入时间戳字段,每次更新时通过比较时间戳判断是否发生冲突。
- CAS(Compare and Swap):使用原子操作的方式进行数据的更新,通过比较当前值和预期值是否一致来判断是否发生冲突。
乐观锁适合于读操作非常频繁,但写操作相对较少的场景,可以提高并发性能。然而,乐观锁需要保证数据不会被并发修改的假设成立,否则会引发数据不一致问题。如果冲突频率较高,乐观锁可能会引起大量的重试,降低性能。
在实际应用中,选择悲观锁还是乐观锁要根据具体的场景,考虑并发冲突的频率、数据一致性要求以及性能需求等因素。有时也可以结合两者的优点,使用适当的锁机制来满足需求。
总结:
锁的底层确实很复杂,我们也不是一篇两篇文章就可以讲清楚的,因此我写这篇文章更多的还是为了吸引大家的兴趣,如果有兴趣了可以再去深入的了解一下各种锁。
如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!
相关文章:

【从零开始学习JAVA | 第四十一篇】深入JAVA锁机制
目录 前言: 引入: 锁机制: CAS算法: 乐观锁与悲观锁: 总结: 前言: 在多线程编程中,线程之间的协作和资源共享是一个重要的话题。当多个线程同时操作共享数…...

Playable 动画系统
Playable 基本用法 Playable意思是可播放的,可运行的。Playable整体是树形结构,PlayableGraph相当于一个容器,所有元素都被包含在里面,图中的每个节点都是Playable,叶子节点的Playable包裹原始数据,相当于输…...

深入理解Linux内核--虚拟文件
虚拟文件系统(VFS)的作用 虚拟文件系统(Virtual Filesystem)也可以称之为虚拟文件系统转换(Virtual Filesystem Switch,VFS), 是一个内核软件层, 用来处理与Unix标准文件系统相关的所有系统调用。 其健壮性表现在能为各种文件系统提供一个通用的接口。VFS支持的文件…...
记一次 .NET 某外贸ERP 内存暴涨分析
一:背景 1. 讲故事 上周有位朋友找到我,说他的 API 被多次调用后出现了内存暴涨,让我帮忙看下是怎么回事?看样子是有些担心,但也不是特别担心,那既然找到我,就给他分析一下吧。 二࿱…...

关于安卓打包生成aar,jar实现(一)
关于安卓打包生成aar,jar方式 背景 在开发的过程中,主项目引入三方功能的方式有很多,主要是以下几个方面: (1)直接引入源代码module(优点:方便修改源码,易于维护&#…...
QString字符串与16进制QByteArray的转化,QByteArray16进制数字组合拼接,Qt16进制与10进制的转化
文章目录 QString转16进制QByteArry16进制QByteArray转QStringQByteArray16进制数拼接Qt16进制与10进制的转化在串口通信中,常常使用QByetArray储存数据,QByteArray可以看成字节数组,每个索引位置储存一个字节也就是8位的数据,可以储存两位16进制数,可以用uint8取其中的数…...

ElasticSearch安装与启动
ElasticSearch安装与启动 【服务端安装】 1.1、下载ES压缩包 目前ElasticSearch最新的版本是7.6.2(截止2020.4.1),我们选择6.8.1版本,建议使用JDK1.8及以上。 ElasticSearch分为Linux和Window版本,基于我们主要学习…...

JavaWeb中Json传参的条件
JavaWeb中我们常用json进行参数传递 对应的注释为RequestBody 但是json传参是有条件的 最主要是你指定的实体类和对应的json参数能否匹配 1.属性和对应的json参数名称对应 2.对应实体类实现了Serializable接口,可以进行序列化和反序列化,这个才是实体类转…...

包装类+初识泛型
目录 1 .包装类 1.1 基本数据类型对应的包装类 1.2.1装箱 1.2.2拆箱 2.初识泛型 2.1什么是泛型 2.2泛型类 2.3裸类型 2.4泛型的上界 2.5泛型方法 1 .包装类 基本数据类型所对应的类类型 在 Java 中,由于基本类型不是继承自 Object ,为了在泛型…...
基于改进的长短期神经网络电池电容预测,基于DBN+LSTM+SVM的电池电容预测
目录 背影 摘要 LSTM的基本定义 LSTM实现的步骤 基于长短期神经网络LSTM的客电池电容预测 完整代码: 基于长短期神经网络LSTM的公交站客流量预测资源-CSDN文库 https://download.csdn.net/download/abc991835105/88184734 效果图 结果分析 展望 参考论文 背影 为增加电动车行…...
Python 2.x 中如何使用pandas模块进行数据分析
Python 2.x 中如何使用pandas模块进行数据分析 概述: 在数据分析和数据处理过程中,pandas是一个非常强大且常用的Python库。它提供了数据结构和数据分析工具,可以实现快速高效的数据处理和分析。本文将介绍如何在Python 2.x中使用pandas进行数据分析&am…...

获取Spring中bean工具类
获取Spring中bean工具类 工具类 package com.geekmice.springbootselfexercise.utils;import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org…...
【实战篇】亿级高并发电商项目(新建 ego_pojo、ego_mapper、ego_api、ego_provider、搭建后台项目 )十五
目录 八、 搭建 Provide 1 新建 ego_pojo 2 新建 ego_mapper 2.1编写 pom.xml 2.2新建配置文件 编辑...

【Plex】FRP内网穿透后 App无法使用问题
能搜索到这个文章的,应该都看过这位同学的分析【Plex】FRP内网穿透后 App无法使用问题_plex frp无效_Fu1co的博客-CSDN博客 这个是必要的过程,但是设置之后仍然app端无法访问,原因是因为网络端口的问题 这个里面的这个公开端口,可…...

[管理与领导-11]:IT基层管理者 - 目标与落实 - 过程管理失控,结果总难达成的问题思考:如何把过程管控做得更好?
目录 前言: 第1章 问题与现象 1.1 总有意想不到的事发生:意外事件 1.2 总有计划变更:意外影响 1.3 总有一错再错,没有复盘、总结与反思,没有流程与改进 第2章 背后的原因 2.1 缺乏及时的过程检查 - 缺乏异常检测…...
用php语言写一个chatgpt3.5模型的例子
当然可以!使用PHP语言调用OpenAI API与ChatGPT-3.5模型进行交互。首先,确保你已经安装了PHP 7.2或更新版本,并具备可用的OpenAI API密钥。 下面是一个基本的PHP示例,展示了如何使用OpenAI API与ChatGPT-3.5模型进行对话ÿ…...

PHP实现保质期计算器
1.php实现保质期计算, 保质期日期可选,天 、月、年 2. laravel示例 /*** 保质期计算器* return void*/public function expirationDateCal(){$produce_date $this->request(produce_date); // 生产日期$warranty_date $this->reques…...

【独立版】新零售社区团购电商系统生鲜水果商城兴盛优选十荟团源码
【独立版】新零售社区团购电商系统生鲜水果商城兴盛优选十荟团源码...
C++系列十:其他-1. Lua
系列文章目录 Lua 系列文章目录前言Lua介绍:参考链接: 基本语法:函数、迭代器table、userdata、模块元素、元方法:协程、文件读写面向对象、垃圾回收 前言 我写这个博客的一个问题?(●’◡’●) 居然是 取名太难了。 …...

不知道打仗之害,就不知道打仗之利
不知道打仗之害,就不知道打仗之利 【安志强趣讲《孙子兵法》第7讲】 【原文】 夫钝兵挫锐,屈力殚货,则诸侯乘其弊而起,虽有智者,不能善其后矣。 【注释】 屈力殚货:屈力,指力量消耗,…...

《从零掌握MIPI CSI-2: 协议精解与FPGA摄像头开发实战》-- CSI-2 协议详细解析 (一)
CSI-2 协议详细解析 (一) 1. CSI-2层定义(CSI-2 Layer Definitions) 分层结构 :CSI-2协议分为6层: 物理层(PHY Layer) : 定义电气特性、时钟机制和传输介质(导线&#…...

转转集团旗下首家二手多品类循环仓店“超级转转”开业
6月9日,国内领先的循环经济企业转转集团旗下首家二手多品类循环仓店“超级转转”正式开业。 转转集团创始人兼CEO黄炜、转转循环时尚发起人朱珠、转转集团COO兼红布林CEO胡伟琨、王府井集团副总裁祝捷等出席了开业剪彩仪式。 据「TMT星球」了解,“超级…...

Java面试专项一-准备篇
一、企业简历筛选规则 一般企业的简历筛选流程:首先由HR先筛选一部分简历后,在将简历给到对应的项目负责人后再进行下一步的操作。 HR如何筛选简历 例如:Boss直聘(招聘方平台) 直接按照条件进行筛选 例如:…...
rnn判断string中第一次出现a的下标
# coding:utf8 import torch import torch.nn as nn import numpy as np import random import json""" 基于pytorch的网络编写 实现一个RNN网络完成多分类任务 判断字符 a 第一次出现在字符串中的位置 """class TorchModel(nn.Module):def __in…...

MySQL 知识小结(一)
一、my.cnf配置详解 我们知道安装MySQL有两种方式来安装咱们的MySQL数据库,分别是二进制安装编译数据库或者使用三方yum来进行安装,第三方yum的安装相对于二进制压缩包的安装更快捷,但是文件存放起来数据比较冗余,用二进制能够更好管理咱们M…...
纯 Java 项目(非 SpringBoot)集成 Mybatis-Plus 和 Mybatis-Plus-Join
纯 Java 项目(非 SpringBoot)集成 Mybatis-Plus 和 Mybatis-Plus-Join 1、依赖1.1、依赖版本1.2、pom.xml 2、代码2.1、SqlSession 构造器2.2、MybatisPlus代码生成器2.3、获取 config.yml 配置2.3.1、config.yml2.3.2、项目配置类 2.4、ftl 模板2.4.1、…...

iview框架主题色的应用
1.下载 less要使用3.0.0以下的版本 npm install less2.7.3 npm install less-loader4.0.52./src/config/theme.js文件 module.exports {yellow: {theme-color: #FDCE04},blue: {theme-color: #547CE7} }在sass中使用theme配置的颜色主题,无需引入,直接可…...

Rust 开发环境搭建
环境搭建 1、开发工具RustRover 或者vs code 2、Cygwin64 安装 https://cygwin.com/install.html 在工具终端执行: rustup toolchain install stable-x86_64-pc-windows-gnu rustup default stable-x86_64-pc-windows-gnu 2、Hello World fn main() { println…...
深度学习之模型压缩三驾马车:模型剪枝、模型量化、知识蒸馏
一、引言 在深度学习中,我们训练出的神经网络往往非常庞大(比如像 ResNet、YOLOv8、Vision Transformer),虽然精度很高,但“太重”了,运行起来很慢,占用内存大,不适合部署到手机、摄…...

五子棋测试用例
一.项目背景 1.1 项目简介 传统棋类文化的推广 五子棋是一种古老的棋类游戏,有着深厚的文化底蕴。通过将五子棋制作成网页游戏,可以让更多的人了解和接触到这一传统棋类文化。无论是国内还是国外的玩家,都可以通过网页五子棋感受到东方棋类…...