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

news2024/11/26 0:24:16

目录

前言:         

引入:

锁机制: 

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.coloradmin.cn/o/844866.html

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

相关文章

windows(iis)服务器部署安装wordpress(php)网站教程

该教程包含iis安装,php安装,mysql安装,php网站部署上线,windows服务部署php网站,只需要这一篇文章就够了。 该教程为iis服务器部署安装wordpress(php)网站教程,同样适用wordpress网站迁移。 配置要求 1、windows服务器安装iis windows服务器安装iis管理器 打开控制面…

最新AI创作系统ChatGPT源码V2.5.8/支持GPT4.0+GPT联网提问/支持ai绘画Midjourney+Prompt+MJ以图生图+思维导图生成!

使用Nestjs和Vue3框架技术,持续集成AI能力到系统! 最新版【V2.5.8】更新: 新增 MJ 官方图片重新生成指令功能同步官方 Vary 指令 单张图片对比加强 Vary(Strong) | Vary(Subtle)同步官方 Zoom 指令 单张图片无限缩放 Zoom out 2x | Zoom ou…

2020年09月 Python(一级)真题解析#中国电子学会#全国青少年软件编程等级考试

一、单选题 第1题 Python自带的编程环境是? A:PyScripter B:Spyder C:Notepad D:IDLE 正确的答案是:D Python自带的编程环境是IDLE(Integrated Development and Learning Environment&a…

LVS工作环境配置

一、LVS-DR工作模式配置 模拟环境如下: 1台客户机 1台LVS负载调度器 2台web服务器 1、环境部署 (1)LVS负载调度器 yum install -y ipvsadm # 在LVS负载调度器上进行环境安装 ifconfig ens33:200 192.168.134.200/24 # 配置LVS的VIP…

Synchronized同步锁的优化方法 待完工

Synchronized 和后来出的这个lock锁的区别 在并发编程中,多个线程访问同一个共享资源时,我们必须考虑如何维护数据的原子性。在 JDK1.5 之前,Java 是依靠 Synchronized 关键字实现锁功能来做到这点的。Synchronized 是 JVM 实现的一种内置锁…

【枚举】CF1706 C

有人一道1400写了一个小时 Problem - C - Codeforces 题意: 思路: 首先先去观察样例: 很显然,对于n是奇数的情况,只有一种情况,直接操作偶数位就好了 主要是没搞清楚n是偶数的情况 其实有个小技巧&…

前沿分享-鱼形机器人

可能并不太前沿了,是21年底的新闻了,但是看见了就顺便发一下吧。 大概就是,通过在pH响应型水凝胶中编码不同的膨胀速率而构建了一种环境适应型变形微机器人,让微型机器人直接向癌细胞输送药物从而减轻药物带来副作用。 技术原理是&#xff0c…

第五章 树与二叉树

一、数据结构定义 二叉树的顺序存储结构二叉树的链式存储结构(即二叉链表)(因为有两个指针所以是二叉链表,如果再加一个指向父节点的指针就是三叉链表)typedef struct BTNode{ // BT即Binary Tree,二叉树i…

Reinforcement Learning with Code 【Code 4. DQN】

Reinforcement Learning with Code 【Code 4. DQN】 This note records how the author begin to learn RL. Both theoretical understanding and code practice are presented. Many material are referenced such as ZhaoShiyu’s Mathematical Foundation of Reinforcement…

C++笔记之从数组指针到函数数组指针(使用using name和std::function)

C笔记之从数组指针到函数数组指针(使用using name和std::function) 参考笔记: C之指针探究(三):指针数组和数组指针 C之指针探究(十三):函数指针数组 C之指针探究(二):一级指针和一维数组 C之指针探究(十一):函数名的…

HCIP VLAN--Hybrid接口

一、VLAN的特点 1、一个VLAN就是一个广播域,所以在同一个VLAN内部,计算机可以直接进行二层通信;而不同VLAN内的计算机,无法直接进行二层通信,只能进行三层通信来传递信息,即广播报文被限制在一个VLAN内。 …

数字人现身大运会,怎么以动作捕捉技术助推运动与文博相结合

中国移动动感地带数字人橙络络,作为数智体验官以元宇宙的视角,带领观众沉浸式体验大运会,以极具科技和未来的数字人,对外传递大运青春风采,并且数字人橙络络还对大运会的赛事、活动进行了科普、讲解以及表演当地特色才…

Cpp学习——string(2)

目录 ​编辑 容器string中的一些函数 1.capacity() 2.reserve() 3.resize() 4.push_back()与append() 5.find系列函数 容器string中的一些函数 1.capacity() capacity是string当中表示容量大小的函数。但是string开空间时是如何开的呢?现在就来看一下。先写…

Postgresql15安装插件madlib2.0

madlib2.0简介 Apache MADlib madlib使用指南 MADlib: Main Page madlib安装 Installation Guide - Apache MADlib - Apache Software Foundation 准备 cmake 3.5.2及以上版本python3.9postgresql15,源码编译时必须指定 –with-pythonpostgresql插件plpython3u 源…

【Autolayout自动布局介绍2 Objective-C语言】

一、好,我们继续来介绍Autolayout 1.好,那么,我们接下来,开始上课了, 刚才,给大家介绍了一下,我们就是屏幕布局里面,用到的,主要就是这四个按钮: 1)第一个:是用来做对齐的 2)第二个:是用来做固定的 3)第三个:是用来解决自动布局中出现的一些问题的 4)第四…

元组是什么,python怎么使用元组

目录 引言 元组的概念 作用和优势 元组的应用 Python中如何使用元组 注意事项 总结 引言 在Python编程语言中,元组(Tuple)是一种不可变的数据结构,它允许我们以有序且不可修改的方式存储多个元素。与列表不同,…

简单游戏截图_可控截取内容2

一个需求 我需要在场景中截取不同层级的截图(如只截模型或只截UI或只截外部相加看到的画面 或全都截或和Shader配合呈现人眼夜视仪热成像的画面切换) 将截图排到列表中,在场景UI中展示出来 如何做 相机要能够看到不同的画面 将当前帧画面存储下来 将存储的画面展示出…

利用文档管理系统软件实现信息共享与知识沉淀

在当今信息的时代,各个企业和组织面临着大量的信息管理和知识沉淀的问题。为了更好地实现信息共享和知识沉淀,许多企业开始使用文档管理软件,该软件通过整合和管理各种文档,并提供强大的搜索和分享功能,有效促进了信息…

Java容器之Stack和Deque

Java容器之Stack和Deque 目录概述需求: 设计思路实现思路分析1.Java中Deque和Stack都是用于实现栈(stack)数据结构的类,但它们有一些区别。2.实现方式不同3.用途不同 拓展实现 参考资料和推荐阅读 Survive by day and develop by …

代驾小程序怎么做

代驾小程序是一款专门为用户提供代驾服务的手机应用程序。它具有以下功能: 1. 预约代驾:代驾小程序允许用户在需要代驾服务时提前进行预约。用户可以选择出发地点、目的地以及预计用车时间,系统会自动匹配最合适的代驾司机,并确保…