iis服务器助手广告
返回顶部
首页 > 资讯 > 精选 >如何理解Java并发下的乐观锁
  • 830
分享到

如何理解Java并发下的乐观锁

2023-06-15 19:06:02 830人浏览 独家记忆
摘要

本篇内容主要讲解“如何理解java并发下的乐观锁”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“如何理解Java并发下的乐观锁”吧!在聊乐观锁之前,先给大家复习一个概念:原子操作:什么是原子操作呢

本篇内容主要讲解“如何理解java并发下的乐观”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“如何理解Java并发下的乐观锁”吧!

在聊乐观锁之前,先给大家复习一个概念:原子操作:

什么是原子操作呢?

我们知道,原子(atom)指化学反应不可再分的基本微粒。在 Java  多线程编程中,所谓原子操作,就是即使命令涉及多个操作,这些操作依次执行,不会被别的线程插队打断。

如何理解Java并发下的乐观锁

原子操作

聊完原子操作了,我们进入正题。

大家都知道,一般而言,由于多线程并发会导致安全问题,针对变量的读和写操作,都会采用锁的机制。锁一般会分为乐观锁和悲观锁两种。

悲观锁

对于悲观锁,开发者认为数据发送时发生并发冲突的概率很大,所以每次进行读操作前都会上锁。

乐观锁

对于乐观锁,开发者认为数据发送时发生并发冲突的概率不大,所以读操作前不上锁。

到了写操作时才会进行判断,数据在此期间是否被其他线程修改。如果发生修改,那就返回写入失败;如果没有被修改,那就执行修改操作,返回修改成功。

乐观锁一般都采用 Compare And  Swap(CAS)算法进行实现。顾名思义,该算法涉及到了两个操作,比较(Compare)和交换(Swap)。

如何理解Java并发下的乐观锁

CAS 算法流程

CAS 算法的思路如下:

该算法认为不同线程对变量的操作时产生竞争的情况比较少。

该算法的核心是对当前读取变量值 E 和内存中的变量旧值 V 进行比较。

如果相等,就代表其他线程没有对该变量进行修改,就将变量值更新为新值 N。

如果不等,就认为在读取值 E 到比较阶段,有其他线程对变量进行过修改,不进行任何操作。

当线程运行 CAS 算法时,该运行过程是原子操作,也就是说,Compare And Swap 这个过程虽然涉及逻辑比较繁冗,但具体操作一气呵成。

Java中 CAS 的底层

实现Java 中的 Unsafe 类我先问大家一个问题:

什么是指针?

针对学过 C、c++ 语言的同学想必都不陌生。说白了,指针就是内存地址,指针变量也就是用来存放内存地址的变量。

但对于指针这个东西的使用,有利有弊。有利的地方在于如果我们有了内存的偏移量,换句话说有了数据在内存中的存储位置坐标,就可以直接针对内存的变量操作;

弊端就在于指针是语言中功能强大的组件,如果一个新手在编程时,没有考虑指针的安全性,错误的操作指针把某块不该修改的内存值修改,容易导致整个程序崩溃。

如何理解Java并发下的乐观锁

错误使用指针

对于 Java 语言,没有直接的指针组件,一般也不能使用偏移量对某块内存进行操作。这些操作相对来讲是安全(safe)的。

但其实 Java 有个类叫 Unsafe 类,这个类类使 Java 拥有了像 C 语言的指针一样操作内存空间的能力,同时也带来了指针的问题。这个类可以说是  Java 并发开发的基础。

Unsafe 类中的 CAS

一般而言,大家接触到的 CAS 函数都是 Unsafe 类提供的封装。下面就是一些 CAS 函数。

public final native boolean compareAndSwapObject(     Object paramObject1,      long paramLong,      Object paramObject2,      Object paramObject3);  public final native boolean compareAndSwapint(     Object paramObject,      long paramLong,      int paramInt1,      int paramInt2);  public final native boolean compareAndSwapLong(     Object paramObject,      long paramLong1,      long paramLong2,      long paramLong3);

这就是 Unsafe 包下提供的 CAS 更新对象、CAS 更新 int 型变量、CAS 更新 long 型变量三个函数。

我们以最好理解的 compareAndSwapInt 为例,来看一下吧:

public final native boolean compareAndSwapInt(     Object paramObject,      long paramLong,      int paramInt1,      int paramInt2);

可以看到,该函数有四个参数:

  • 第一个是目标对象

  • 第二个参数用来表示我们上文讲的指针,这里是一个 long  类型的数值,表示该成员变量在其对应对象属性的偏移量。换句话说,函数就可以利用这个参数,找到变量在内存的具体位置,从而进行 CAS 操作

  • 第三个参数就是预期的旧值,也就是示例中的 V。

  • 第四个参数就是修改出的新值,也就是示例中的 N。

有同学会问了,Java 中只有整型的 CAS 函数吗?有没有针对 double 型和 boolean 型的 CAS 函数?

很可惜的是, Java 中 CAS 操作和 UnSafe 类没有提供对于 double 型和 boolean  型数据的操作方法。但我们可以利用现有方法进行包装,自制 double 型和 boolean 型数据的操作方法。

  • 对于 boolean 类型,我们可以在入参的时候将 boolean 类型转为 int 类型,在返回值的时候,将 int 类型转为 boolean  类型。

  • 对于 double 类型,则依赖 long 类型了, double 类型提供了一种 double 类型和 long 类型互转的函数。

public static native double longBitsToDouble(     long bits);  public static native long doubleToRawLongBits(     double value);

大家都知道,基础数据类型在底层的存储方式都是bit类型。因此无论是long类型还是double类型在计算机底层存储方式都是比特。所以就很好理解这两个函数了:

  • longBitsToDouble 函数将 long 类型底层的实际二进制存储数据,用 double 类型强行翻译出来

如何理解Java并发下的乐观锁

  • doubleToRawLongBits 函数将 double 类型底层的实际二进制存储数据,用 long 类型强行翻译出来

如何理解Java并发下的乐观锁

CAS 在 Java 中的使用

一个比较常见的操作,使用变量 i 来为程序计数,可以对 i 自增来实现。

int i=0; i++;

但稍有经验的同学都知道这种写法是线程不安全的。

如果 500 个线程同时执行一次 i++,得到 i 的结果不一定为 500,可能会比 500 小。

这是因为 i++ 其实并不只是一行命令,它涉及以下几个操作:(以下代码为 Java 代码编译后的字节码)

getfield  #从内存中获取变量 i 的值 iadd      #将 count 加 1 putfield  #将加 1 后的结果赋值给 i 变量

可以看到,简简单单一个自增操作涉及这三个命令,而且这些命令并不是一气呵成的,在多线程情况下很容易被别的线程打断。

如何理解Java并发下的乐观锁

自增操作

虽然两个线程都进行了 i++ 的操作,i 的值本应是 2,但是按上图的流程来说,i 的值就变为 1 了

如果需要执行我们想要的操作,代码可以这样改写。

int i=0; synchronized{     i++; }

我们知道,通过 synchronized 关键字修饰时代价很大,Java 提供了一个 atomic 类,如果变量 i 被声明为 atomic  类,并执行对应操作,就不会有之前所说的问题了,而且相较 synchronized代价较小。

AtomicInteger i= new AtomicInteger(0); i.getAndIncrement();

Java 的 Atomic 基础数据类型类还提供

  • AtomicInteger 针对 int 类型的原子操作

  • AtomicLong 针对 long 类型的原子操作

  • AtomicBoolean 针对 boolean 类型的原子操作

Atomic基础数据类型支持的方法如下图所示:

如何理解Java并发下的乐观锁

Atomic基础数据类型

  • getCurrentValue :获取该基础数据类型的当前值。

  • setValue :设置当前基础数据类型的值为目标值。

  • getAndSet :获取该基础数据类型的当前值并设置当前基础数据类型的值为目标值。

  • getAndIncrement :获取该基础数据类型的当前值并自增 1,类似于 i++。

  • getAndDecrement :获取该基础数据类型的当前值并自减 1,类似于 i--。

  • getAndAdd :获取该基础数据类型的当前值并自增给定参数的值。

  • IncrementAndGet :自增 1 并获取增加后的该基础数据类型的值,类似于 ++i。

  • decrementAndGet :自减 1 并获取增加后的该基础数据类型的值,类似于 --i。

  • AddAndGet :自增给定参数的值并获取该基础数据类型自增后的值。

这些基本数据类型的函数底层实现都有 CAS 的身影。

我们来拿最简单的 AtomicInteger 的 getAndIncrement 函数举例吧:(源码来源 jdk 7 )

volatile int value; ··· public final int getAndIncrement(){     for(;;){         int current = get();         int next= current + 1;         if(compareAndSet(current, next))             return current;     } }

这就类似之前的 i++ 自增操作,这里的 compareAndSet 其实就是封装了 Unsafe 类的一个 native 函数:

public final compareAndSet(int expect, undate){     return unsafe.compareAndSwapInt     (this, valueOffset, expect, update); }

也就回到了我们刚刚讲述的 unsafe 包下的 compareAndSwapInt 函数了。

自旋

除了 CAS 之外,Atomic 类还采用了一种方式优化拿到锁的过程。

我们知道,当一个线程拿不到对应的锁的时候,可以有两种策略:

策略 1:放弃获得 CPU ,将线程置于阻塞状态,等待后续被操作系统唤醒和调度。

当然这么做的弊端很明显,这种状态的切换涉及到了用户态到内核态的切换,开销一般比较大,如果线程很快就把占用的锁释放了,这么做显然是不合算的。

策略 2:不放弃 CPU ,不停的重试,这种操作也称为自旋。

当然这么做也有弊端,如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程一直在毫无意义的消耗 CPU 资源。使用不当会造成 CPU  使用率极高。在这种情况下,策略 1 更合理一些。

我们前文中所说的 AtomicInteger,AtomicLong 在执行相关操作的时候就采取策略 2。一般这种策略也被称为自旋锁。

可以看到在 AtomicInteger 的 getAndIncrement 函数中,函数外包了一个

for(;;)

其实就是一个不断重试的死循环,也就是这里说的自旋。

但现在大多采取的策略是开发者设置一个门限值,在门限值内进行不断地自旋。

如果自旋失败次数超过门限值了,那就采取进入阻塞状态。

如何理解Java并发下的乐观锁

自旋

ABA 问题与 AtomicMarkable

CAS 算法本身有一个很大的缺陷,那就是 ABA 问题。

我们可以看到, CAS 算法是基于值来做比较的,如果当前有两个线程,一个线程将变量值从 A 改为 B ,再由 B 改回为 A ,当前线程开始执行 CAS  算法时,就很容易认为值没有变化,误认为读取数据到执行 CAS 算法的期间,没有线程修改过数据。

如何理解Java并发下的乐观锁

ABA 问题

咋一看好像这个缺陷不会引发什么问题,实则不然,给大家举个例子吧。

假设小艾银行卡有 100 块钱余额,且假定银行转账操作就是一个单纯的 CAS  命令,对比余额旧值是否与当前值相同,如果相同则发生扣减/增加,我们将这个指令用CAS(origin,expect)  表示。于是,我们看看接下来发生了什么:

如何理解Java并发下的乐观锁

银行转账

  1. 鸿蒙官方战略合作共建——HarmonyOS技术社区

  2. 小明欠小艾100块钱,小艾欠小牛100块钱,

  3. 小艾在 ATM 1号机上打算 转账 100 块钱给小牛;假设银行转账底层是用CAS算法实现的。由于ATM 1号机突然卡了,这时候小艾跑到旁边的 ATM  2号机再次操作转账;

  4. ATM 2号机执行了 CAS(100,0),顺顺利利地完成了转账,此时小艾的账户余额为 0;

  5. 小明这时候又给小艾账上转了 100,此时小艾账上余额为 100;

  6. 这时候 ATM 1 网络恢复,继续执行 CAS(100,0),居然执行成功了,小艾账户上余额又变为了 0;

可怜的小艾,由于 CAS 算法的缺陷,让他损失了100块钱。

解决 ABA 问题的方法也不复杂,对于这种 CAS 函数,不仅要比较变量值,还需要比较版本号。

public boolean compareAndSet(V expectedReference,                              V newReference,                               int expectedStamp,                              int newStamp)

之前的 CAS 只有两个参数,带上版本号比较的 CAS 就有四个参数了,其中 expectedReference指的是变量预期的旧值,  newReference 指的是变量需要更改成的新值, expectedStamp 指的是版本号的旧值, newStamp 指的是版本号新值。

修改后的 CAS 算法执行流程如下图:

如何理解Java并发下的乐观锁

改正 CAS 算法

AtomicStampedReference

那如何能在 Java 中顺畅的使用带版本号比较的 CAS 函数呢?

Java 开发人员都帮我们想好了,他们提供了一个类叫做 Java 的 AtomicStampedReference ,该类封装了带版本号比较的 CAS  函数,一起来看看吧。

AtomicStampedReference 定义在 java.util.concurrent.atomic 包下。

下图描述了该类对应的几个常用方法:

如何理解Java并发下的乐观锁

AtomicStampedReference

  • attemptStamp :如果 expectReference 和目前值一致,设置当前对象的版本号戳为 newStamp

  • compareAndSet :该方法就是前文所述的带版本号的 CAS 方法。

  • get :该方法返回当前对象值和当前对象的版本号戳

  • getReference :该方法返回当前对象值

  • getStamp :该方法返回当前对象的版本号戳

  • set :直接设置当前对象值和对象的版本号戳

到此,相信大家对“如何理解Java并发下的乐观锁”有了更深的了解,不妨来实际操作一番吧!这里是编程网网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

--结束END--

本文标题: 如何理解Java并发下的乐观锁

本文链接: https://www.lsjlt.com/news/281305.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 如何理解Java并发下的乐观锁
    本篇内容主要讲解“如何理解Java并发下的乐观锁”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“如何理解Java并发下的乐观锁”吧!在聊乐观锁之前,先给大家复习一个概念:原子操作:什么是原子操作呢...
    99+
    2023-06-15
  • Java并发编程的悲观锁和乐观锁机制
    本篇内容主要讲解“Java并发编程的悲观锁和乐观锁机制”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Java并发编程的悲观锁和乐观锁机制”吧!一、资源和加锁1、场景描述多线程并发访问同一个资源问...
    99+
    2023-06-16
  • Java并发编程中的悲观锁和乐观锁机制
    这篇文章主要介绍“Java并发编程中的悲观锁和乐观锁机制”,在日常操作中,相信很多人在Java并发编程中的悲观锁和乐观锁机制问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Java并发编程中的悲观锁和乐观锁机制...
    99+
    2023-06-02
  • 怎么理解Java悲观锁与乐观锁
    本篇内容介绍了“怎么理解Java悲观锁与乐观锁”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!1锁(Lock)在介绍悲观锁和乐观锁之前,让我们...
    99+
    2023-06-04
  • Java并发问题之乐观锁与悲观锁的示例分析
    这篇文章将为大家详细讲解有关Java并发问题之乐观锁与悲观锁的示例分析,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。首先介绍一些乐观锁与悲观锁:悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修...
    99+
    2023-05-30
    java
  • 浅谈一下Java中的悲观锁和乐观锁
    目录悲观锁(Pessimistic Locking)悲观锁存的问题:乐观锁乐观锁存在的问题悲观锁和乐观锁的对比总结悲观锁和乐观锁是面试高频问题之一,本文将对悲观锁和乐观锁简单的进行一...
    99+
    2023-05-16
    Java悲观锁 Java乐观锁
  • 详解Java中的悲观锁与乐观锁
    目录一、悲观锁二、乐观锁三、CAS四、AtomicXXX五、CAS中的ABA问题六、ABA问题解决方案七、使用CAS会引起的问题八、Synchronized锁优化九、偏向锁十、轻量级...
    99+
    2024-04-02
  • 如何理解互斥锁、自旋锁、读写锁、悲观锁、乐观锁的应用场景
    本篇内容主要讲解“如何理解互斥锁、自旋锁、读写锁、悲观锁、乐观锁的应用场景”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“如何理解互斥锁、自旋锁、读写锁、悲观锁、...
    99+
    2024-04-02
  • 详解JAVA如何实现乐观锁以及CAS机制
    目录前言问题引入悲观锁解决乐观锁解决乐观锁改进CAS机制总结前言 生活中我们看待一个事物总有不同的态度,比如半瓶水,悲观的人会觉得只有半瓶水了,而乐观的人则会认为还有半瓶水呢。很多技...
    99+
    2022-12-08
    JAVA乐观锁 CAS机制 JAVA乐观锁 JAVA CAS
  • 数据库的乐观锁如何实现
    本文小编为大家详细介绍“数据库的乐观锁如何实现”,内容详细,步骤清晰,细节处理妥当,希望这篇“数据库的乐观锁如何实现”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。线程锁分类有很多...
    99+
    2024-04-02
  • Java的并发锁怎么理解
    本篇内容主要讲解“Java的并发锁怎么理解”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Java的并发锁怎么理解”吧!   Java 中的并发锁大致分为隐式锁...
    99+
    2024-04-02
  • 如何理解MySQL的锁和并发控制技术?
    如何理解MySQL的锁和并发控制技术?MySQL是一种常用的关系型数据库管理系统,它支持并发访问和操作数据,同时也提供了一些锁和并发控制技术,以保证数据的一致性和并发性。本文将详细介绍MySQL的锁和并发控制技术,并通过代码示例来加深理解。...
    99+
    2023-10-22
    MySQL 并发控制
  • Java并发中如何搞懂读写锁
    本篇文章为大家展示了Java并发中如何搞懂读写锁,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。ReentrantReadWriteLock我们来探讨一下java.concurrent.util包下的...
    99+
    2023-06-25
  • 并发编程中如何使用Java中的锁?
    并发编程中如何使用Java中的锁? 在Java中,锁是一种用来控制多个线程访问共享资源的机制。锁可以保证在同一时刻只有一个线程可以访问共享资源,从而避免多个线程同时修改数据导致的数据不一致问题。Java中的锁可以分为两种类型:内置锁和显式锁...
    99+
    2023-08-28
    numy shell 并发
  • 掌握 Java 线程池,解锁并发处理的潜力
    在当今复杂的分布式系统中,并发处理对于高效执行任务至关重要。Java 线程池是一种强大的工具,它通过管理和调度线程,帮助开发人员充分利用并发性,从而提高性能和可伸缩性。 线程池概述 线程池是一个线程的集合,这些线程可以根据需要按需创建和销...
    99+
    2024-03-13
    线程池
  • 如何使用Redis锁处理并发问题详解
    前言 上周“被”上线了一个紧急项目,周五下班接到需求,周一开始思考解决方案,周三开发完成,周四走流程上线,也算是面向领导编程了。之前的项目里面由于是自运维,然后大多数又都赶时间,所以在处理定时任务上面基本都...
    99+
    2024-04-02
  • java并发编程死锁定义及如何避免死锁
    目录场景模拟分析场景一:狭路相逢场景二:冷战场景三:哲学家就餐场景四:竞争资源死锁是什么?产生死锁的的四个条件如下:如何避免死锁?方案一:破坏不剥夺条件方案二:破坏请求与保持条件方案...
    99+
    2024-04-02
  • MariaDB中如何处理并发性和锁定
    MariaDB处理并发性和锁定的方式主要包括以下几个方面: 事务隔离级别:MariaDB支持多种事务隔离级别,包括读未提交、读已...
    99+
    2024-03-15
    MariaDB
  • 如何理解Java 并发编程中的ForkJoin框架
    如何理解Java 并发编程中的ForkJoin框架,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。1、什么是ForkJoin框架ForkJoin框架是java的JU...
    99+
    2023-06-25
  • Java 内存模型与死锁:深入理解并发编程中的死锁问题
    Java 内存模型(JMM)是一套规范,它定义了 Java 程序中变量是如何在多个线程之间共享的。JMM 规定了线程如何从主内存中读取和写入变量,以及如何将变量的值存储到主内存中。 死锁是并发编程中常见的一种问题,它发生在两个或多个线程...
    99+
    2024-02-04
    Java 内存模型 死锁 并发编程 同步 等待 通知 中断
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作