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

这篇具有很好参考价值的文章主要介绍了【从零开始学习JAVA | 第四十一篇】深入JAVA锁机制。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

前言:         

引入:

锁机制: 

CAS算法:

乐观锁与悲观锁:

总结:


前言:         

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

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

【从零开始学习JAVA | 第四十一篇】深入JAVA锁机制,【从零开始学习JAVA】,学习,java,锁,乐观锁,悲观锁,CAS

引入:

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

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 | 第四十一篇】深入JAVA锁机制,【从零开始学习JAVA】,学习,java,锁,乐观锁,悲观锁,CAS

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

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

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

锁机制: 

在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算法:【从零开始学习JAVA | 第四十一篇】深入JAVA锁机制,【从零开始学习JAVA】,学习,java,锁,乐观锁,悲观锁,CAS

A和B就代表两条线程,而C就代表此时A和B争抢的资源文件,如果A线程运气好抢到了资源C,他将会把自身的old value 与C进行对比,如果一致,就把C的状态改为1,并且获得对C的操作权。【从零开始学习JAVA | 第四十一篇】深入JAVA锁机制,【从零开始学习JAVA】,学习,java,锁,乐观锁,悲观锁,CAS

而此时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):使用原子操作的方式进行数据的更新,通过比较当前值和预期值是否一致来判断是否发生冲突。

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

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

总结:

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

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

【从零开始学习JAVA | 第四十一篇】深入JAVA锁机制,【从零开始学习JAVA】,学习,java,锁,乐观锁,悲观锁,CAS

 文章来源地址https://www.toymoban.com/news/detail-632716.html

到了这里,关于【从零开始学习JAVA | 第四十一篇】深入JAVA锁机制的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包赞助服务器费用

相关文章

  • 【从零开始学习JAVA | 第四十五篇】反射

    【从零开始学习JAVA | 第四十五篇】反射

    目录 前言: ​反射:  使用反射的步骤: 1.获取阶段: 2.使用阶段: 反射的应用场景: 使用反射的优缺点: 总结: Java中的反射是一项强大而灵活的功能,它允许程序在运行时 动态地获取、操作和利用类的信息 。通过反射,我们可以在运行时检查和修改类的属性、调用类

    2024年02月13日
    浏览(46)
  • 第四十一章 Unity 输入框 (Input Field) UI

    第四十一章 Unity 输入框 (Input Field) UI

    本章节我们学习输入框 (Input Field),它可以帮助我们获取用户的输入。我们点击菜单栏“GameObject”-“UI”-“Input Field”,我们调整一下它的位置,效果如下 我们在层次面板中发现,这个InputField UI元素包含两个子元素,一个是Placeholder,另一个是Text。如下所示 同样,我们查看

    2024年02月04日
    浏览(10)
  • 【从零开始学习JAVA | 第四十篇】了解线程池

    【从零开始学习JAVA | 第四十篇】了解线程池

    目录 前言: 线程池: 线程池的工作流程: 代码实现线程池: 任务拒绝策略:  线程池多大才算合适? 总结:         在Java编程中,线程池是一个强大的工具,它能够管理和复用线程,提供高效的并发处理能力。通过线程池,我们可以有效地控制并发线程的数量,并降

    2024年02月13日
    浏览(11)
  • 【LeetCode75】第四十一题 二叉搜索树中的搜索

    【LeetCode75】第四十一题 二叉搜索树中的搜索

    目录 题目: 示例: 分析: 代码: 题目给我们一个搜索二叉树,让我们找出节点值等于目标的节点并返回出去。 首先我们可以直接遍历整棵二叉树,找到值相同的节点就返回出去,不过这样就没有用到二叉搜索数的特性了。 二叉搜索数的特性就是,每一个节点的左子树上所

    2024年02月10日
    浏览(11)
  • 算法第四十一天-排除排序链表中的重复元素Ⅱ

    算法第四十一天-排除排序链表中的重复元素Ⅱ

    题意:在一个 有序 链表中,如果一个节点的值出现不止一次,那么把这个节点删除掉 重点: 有序链表 ,所以,一个节点的值出现不止一次,那么他们必相邻。 方法一:递归 链表和树的问题,一般都可以有递归和迭代两种写法。对于本题一定要记住是有序链表,值相同的节

    2024年04月11日
    浏览(9)
  • 【从零开始学习JAVA | 第四十六篇】处理请求参数

    【从零开始学习JAVA | 第四十六篇】处理请求参数

            在我们之前的学习中,我们已经基本学习完了JAVA的基础内容,从今天开始我们就逐渐进入到JAVA的时间,在这一大篇章,我们将对前后端有一个基本的认识,并要学习如何成为一名合格的后端工程师。今天我们介绍的内容是:如何在后端处理前端的请求 目录 前言:

    2024年02月11日
    浏览(14)
  • 算法训练第四十一天|343. 整数拆分 、96.不同的二叉搜索树

    算法训练第四十一天|343. 整数拆分 、96.不同的二叉搜索树

    题目链接:343. 整数拆分 参考:https://programmercarl.com/0343.%E6%95%B4%E6%95%B0%E6%8B%86%E5%88%86.html 题目描述 给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。 示例 1: 输入: 2 输出: 1 解释: 2 = 1 + 1, 1 × 1 = 1。 示例 2: 输入:

    2023年04月24日
    浏览(12)
  • 【从零开始学习JAVA | 第四十四篇】TCP协议中的握手与挥手

    【从零开始学习JAVA | 第四十四篇】TCP协议中的握手与挥手

    TCP(传输控制协议)作为计算机网络中的重要协议,扮演着确保数据可靠传输的角色。在TCP的通信过程中,握手与挥手问题是不可忽视的关键环节。握手是指在建立连接时,客户端与服务器相互确认彼此的身份并同步参数,确保双方准备就绪;而挥手则是在终止连接时,双方

    2024年02月11日
    浏览(44)
  • 【正点原子STM32连载】 第四十一章 SPI实验 摘自【正点原子】APM32E103最小系统板使用指南

    【正点原子STM32连载】 第四十一章 SPI实验 摘自【正点原子】APM32E103最小系统板使用指南

    1)实验平台:正点原子APM32E103最小系统板 2)平台购买地址:https://detail.tmall.com/item.htm?id=609294757420 3)全套实验源码+手册+视频下载地址: http://www.openedv.com/docs/boards/xiaoxitongban 本章将介绍使用APM32E103驱动板载的NOR Flash进行读写操作。通过本章的学习,读者将学习到使用SPI驱动

    2024年01月19日
    浏览(37)
  • 从零开始的力扣刷题记录-第四十二天

    题目描述: 给你长度相等的两个字符串 s1 和 s2 。一次 字符串交换 操作的步骤如下:选出某个字符串中的两个下标(不必不同),并交换这两个下标所对应的字符。 如果对 其中一个字符串 执行 最多一次字符串交换 就可以使两个字符串相等,返回 true ;否则,返回 false 。

    2024年02月06日
    浏览(13)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包