Java 并发编程:Java 中的乐观锁与 CAS

news2024/9/22 21:21:01

大家好,我是栗筝i,这篇文章是我的 “栗筝i 的 Java 技术栈” 专栏的第 025 篇文章,在 “栗筝i 的 Java 技术栈” 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验,并希望进一步完善自己对整个 Java 技术体系来充实自己的技术栈的同学。与此同时,本专栏的所有文章,也都会准备充足的代码示例和完善的知识点梳理,因此也十分适合零基础的小白和要准备工作面试的同学学习。当然,我也会在必要的时候进行相关技术深度的技术解读,相信即使是拥有多年 Java 开发经验的从业者和大佬们也会有所收获并找到乐趣。

在现代软件开发中,并发编程已成为必不可少的技术。随着多核处理器的普及,如何高效地管理多线程环境下的资源竞争,成为开发者需要面对的重要课题。传统的锁机制(如synchronized关键字和Lock接口)虽然能够解决并发问题,但也带来了性能瓶颈和死锁风险。

为了克服这些缺点,乐观锁和 CAS(Compare And Swap,比较并交换)作为一种无锁并发解决方案应运而生。乐观锁的核心思想是“假设并发冲突很少发生”,因此在进行操作时不立即加锁,而是通过检测冲突来确保数据的一致性。CAS 操作基于 CPU 的原子指令,能够在不使用锁的情况下实现变量的安全更新,从而提高系统的并发性能。

在本文中,我们将深入探讨 Java 并发编程中的乐观锁与 CAS。通过分析AtomicInteger的源码,我们将揭示CAS 操作的工作原理,并探讨其在多线程环境中的实际应用。此外,我们还将介绍 ABA 问题及其解决方案,以及 CAS 自旋操作中的一些优化策略。


文章目录

      • 1、悲观锁与乐观锁
        • 1.1、乐观锁
        • 1.2、悲观锁
      • 2、CAS 比较并交换
        • 2.1、CAS 介绍
        • 2.2、CAS 的基本原理
        • 2.3、CAS 在 Java 中的应用
        • 2.4、CAS 的 ABA 问题
        • 2.5、CAS 的自旋问题
        • 3、对 Java 中 CAS 的实现解读
        • 1、AtomicInteger 对 CAS 的实现
        • 2、Unsafe 类简介


1、悲观锁与乐观锁

悲观锁与乐观锁并不是特指某个锁(Java 中没有哪个 Lock 实现类就叫 PessimisticLock 或 OptimisticLock),而是在并发情况下的两种不同策略。

悲观锁与乐观锁是锁的一种宏观分类方式,代表了在并发情况下的两种不同策略。

1.1、乐观锁

乐观锁(Optimistic Locking)假定系统中的并发冲突是少数情况,因此在对数据进行操作时,不会立即加锁,而是在提交操作之前检查是否有其他线程修改了数据:

  • 如果没有冲突,则提交成功;
  • 如果发生冲突,则重试操作。

这种锁机制的关键在于 “乐观”,即假设大部分情况下不会发生冲突。

实现方式:

  • 乐观锁通常使用版本号(Version Number)或时间戳(Timestamp)来实现;
  • 在 Java 中,乐观锁的实现可以借助 java.util.concurrent.atomic 包中的原子变量,如 AtomicIntegerAtomicLong 等。

适用场景:并发冲突较少的场景、读多写少的场景。

示例代码:

import java.util.concurrent.atomic.AtomicInteger;

public class OptimisticLockExample {
    private final AtomicInteger version = new AtomicInteger(0);
    private int value;

    public void performTask() {
        int currentVersion;
        int newValue;

        do {
            currentVersion = version.get();
            // 执行需要同步的代码
            newValue = value + 1;
        } while (!version.compareAndSet(currentVersion, currentVersion + 1));
        value = newValue;
    }
}
1.2、悲观锁

悲观锁(Pessimistic Locking)假定系统中的并发冲突是常态,因此在对数据进行操作时,会采取加锁的方式,以防止其他线程修改数据。

这种锁机制的关键在于"悲观",即假设每次操作都会发生冲突。因此,悲观锁的策略是:在一个线程开始操作数据之前,先获取对该数据的排他锁(exclusive lock),这样其他线程就无法同时操作该数据,从而保证数据的完整性。

实现方式:在 Java 中,悲观锁可以通过 synchronized 关键字或 ReentrantLock 类来实现。

适用场景:并发冲突频繁的场景、操作的数据量大且操作时间较长的场景。

示例代码:

public class PessimisticLockExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void performTask() {
        lock.lock();
        try {
            // 执行需要同步的代码
        } finally {
            lock.unlock();
        }
    }
}

总的来说,悲观锁和乐观锁代表了两种不同的并发控制策略。悲观锁适用于并发冲突频繁的场景,通过加锁机制保证数据的一致性;乐观锁适用于并发冲突较少的场景,通过版本控制机制减少锁开销,提高系统性能。在实际应用中,需要根据具体的并发场景选择合适的锁策略,以平衡数据一致性和系统性能。


2、CAS 比较并交换

2.1、CAS 介绍

CAS,即 “比较并交换”(Compare-And-Swap),是一种用于解决多线程并行情况下性能损耗问题的机制。CAS 操作是一种乐观锁实现,广泛应用于 java.util.concurrent 包中的并发类。

CAS 的优点:

  • 高效:CAS 是无锁操作,避免了传统锁机制带来的线程切换和上下文切换的开销;
  • 无死锁:由于没有使用锁,因此不会出现死锁问题。

CAS 的缺点:忙等待:在高并发情况下,CAS 操作可能会不断重试,导致 CPU 资源浪费。

2.2、CAS 的基本原理

CAS 操作包含三个操作数:

  1. 内存位置(V):需要被更新的变量的内存地址;
  2. 预期原值(A):预期该内存位置的值;
  3. 新值(B):希望设置的新值。

CAS 操作的执行步骤如下:

  • 比较内存位置 V 的值是否等于预期原值 A;
  • 如果相等,则将该内存位置的值更新为新值 B;
  • 如果不相等,则不执行更新操作,并返回该内存位置当前的值。

CAS 有效地说明了:“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可”。

2.3、CAS 在 Java 中的应用

在 Java 中,sun.misc.Unsafe 类提供了硬件级别的原子操作来实现 CAS。java.util.concurrent 包下的大量类都使用了 Unsafe 类的 CAS 操作,如 AtomicIntegerAtomicLongAtomicReference 等。

示例代码:

import java.util.concurrent.atomic.AtomicInteger;

public class CASExample {
    private AtomicInteger value = new AtomicInteger(0);

    public void increment() {
        int oldValue;
        int newValue;
        do {
          	// 获取当前值
            oldValue = value.get(); 
          	// 计算新值
            newValue = oldValue + 1; 
        } while (!value.compareAndSet(oldValue, newValue)); 	// 比较并交换
    }

    public int getValue() {
        return value.get();
    }

    public static void main(String[] args) {
        CASExample example = new CASExample();
        example.increment();
        System.out.println("Value: " + example.getValue());
    }
}

在上述示例中,compareAndSet 方法用于执行 CAS 操作,如果当前值等于预期值,则更新为新值,否则重试。

2.4、CAS 的 ABA 问题

ABA 问题是指在使用 CAS(比较并交换)操作时,一个值从 A 变为 B,又变回 A,此时 CAS 操作会误认为值没有发生变化,从而导致错误的更新。虽然值看起来没有变化,但实际上已经发生了两次变化。

为了解决 ABA 问题,可以引入版本号(或时间戳)。通过在变量前面追加版本号,每次变量更新时将版本号加一,即使值从 A 变为 B 再变回 A,版本号也会不同,从而避免 ABA 问题。例如:

  • 原值:1A(版本号1,值A)
  • 变化:1A -> 2B -> 3A

在 Java 中,使用 AtomicStampedReference 来解决 ABA 问题。AtomicStampedReference 不仅维护了对象的引用,还维护了一个整数"标记"(通常是版本号),每次修改时更新标记,从而确保每次修改都是唯一的。

示例代码:

import java.util.concurrent.atomic.AtomicStampedReference;

public class ABAProblemSolution {
    private static AtomicStampedReference<Integer> atomicStampedRef =
        new AtomicStampedReference<>(100, 1);

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            int stamp = atomicStampedRef.getStamp();
            System.out.println("Thread1 - Initial Stamp: " + stamp);
            atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);
            System.out.println("Thread1 - New Stamp: " + atomicStampedRef.getStamp());
            atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
            System.out.println("Thread1 - Final Stamp: " + atomicStampedRef.getStamp());
        });

        Thread t2 = new Thread(() -> {
            int stamp = atomicStampedRef.getStamp();
            System.out.println("Thread2 - Initial Stamp: " + stamp);
            try {
              	// 确保Thread1完成ABA操作
                Thread.sleep(1000); 
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean success = atomicStampedRef.compareAndSet(100, 110, stamp, stamp + 1);
            System.out.println("Thread2 - CAS success: " + success);
            System.out.println("Thread2 - New Stamp: " + atomicStampedRef.getStamp());
        });

        t1.start();
        t2.start();
    }
}

在这个示例中,Thread1 执行了 ABA 操作(100 -> 101 -> 100),但是由于每次操作都会更新版本号,Thread2 在尝试更新时,检测到版本号不匹配,从而避免了 ABA 问题。

2.5、CAS 的自旋问题

CAS 的另一个问题是自旋操作,即在不断尝试 CAS 操作时,如果长时间不成功,会导致CPU资源浪费,影响系统性能。

解决自旋问题的方案:

  1. 限制自旋次数:在一定次数的自旋后,采用其他方式处理,如锁机制;
  2. 带有退避的自旋:在每次自旋失败后,等待一段时间再尝试,以减少 CPU 资源的消耗。

3、对 Java 中 CAS 的实现解读

我们这里以 AtomicInteger 类为例,对 Java 中 CAS 的实现进行解读。

AtomicInteger 是一个支持原子操作的 Integer 类,它保证对 AtomicInteger 类型变量的增加和减少操作是原子性的,不会出现多个线程下的数据不一致问题。如果不使用 AtomicInteger,要实现一个按顺序获取的 ID,就必须在每次获取时进行加锁操作,以避免出现并发时获取到同样的 ID 的现象。

通过源码来看 JDK 8 中 AtomicIntegerincrementAndGet() 方法的实现:

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

incrementAndGet() 方法中,使用了 Unsafe 类。下面是 Unsafe 类中提供的 getAndAddInt 方法:

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}

通过一个 do-while 循环实现,核心在于 compareAndSwapInt() 方法:

public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);

此方法是 native 方法,compareAndSwapInt 基于的是 CPU 的 CAS 指令来实现的。因此,基于 CAS 的操作可认为是无阻塞的,一个线程的失败或挂起不会引起其它线程也失败或挂起。由于 CAS 操作是 CPU 原语,所以性能比较好。

回到 incrementAndGet() 方法中:

  • 第一个值是当前对象;
  • 第二个值是当前值的偏移量;
  • 第三个值是要增加的量。

getAndAddInt 方法首先通过 getIntVolatile(o, offset) 获取当前值,然后通过 compareAndSwapInt(o, offset, v, v + delta) 进行比较和交换。如果 compareAndSwapInt 返回 false,表示在这个过程中,值已经被其他线程修改过了,循环会重新获取当前值并尝试更新,直到成功为止。


1、AtomicInteger 对 CAS 的实现

AtomicInteger 是一个支持原子操作的 Integer 类,就是保证对 AtomicInteger 类型变量的增加和减少操作是原子性的,不会出现多个线程下的数据不一致问题。如果不使用 AtomicInteger,要实现一个按顺序获取的 ID,就必须在每次获取时进行加锁操作,以避免出现并发时获取到同样的 ID 的现象。

接下来通过源代码来看 Jdk8 中 AtomicInteger 中 incrementAndGet() 方法的实现,下面是具体的代码。

// JDK 8
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

在 incrementAndGet 里,我们可以看到使用了 Unsafe 类,下面是 Unsafe 里提供的 getAndAddInt 方法:

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
      	v = getIntVolatile(o, offset);
       	} while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}

通过一个 do while 语句来做一个主体实现的在 while 语句里核心调了 compareAndSwapInt() 方法:

public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);

此方法为 native 方法,compareAndSwapInt 基于的是 CPU 的 CAS 指令来实现的。所以基于 CAS 的操作可认为是无阻塞的,一个线程的失败或挂起不会引起其它线程也失败或挂起。并且由于 CAS 操作是 CPU 原语,所以性能比较好。

回到 incrementAndGet 中:我们传过来的第一个值是当前的对象,第二个值是我们当前的值(比如如果我们要实现2+1)那么 offset 就是 2 delta 就是1,这里的 v,它是我们调用底层的方法v v = this.getIntVolatile(o, offset); 获取底层当前的值。如果没有其他线程来处理 o 这个变量的时候,它的正常返回值应该是 2,因此传到 compareAndSwapInt 的参数就是(o,2,2,2+1),这个方法想达到的目标就是对于 o 这个对象,如果当前的这个值和底层的这个值相等的情况下,就把它更新成后面那个值 v + delta。

当我们一个方法进来的时候,我们 offset 的值是2,我们第一次取出来 v 的值也等于 2,但是当我们在执行更新成 3 的时候 也就是这句代码 while (!compareAndSwapInt(o, offset, v, v + delta));可能会被其它线程更改,所以我们要判断 offset 是否与 v 是相同的,只有是相同的,才允许它更新为 3。通过这样不停的循环来判断。就能保证期望的值和底层的值相同。

CAS比较与交换的伪代码可以表示为:
do{
		备份旧数据;
		基于旧数据构造新数据;
}while(!CAS( 内存地址,备份的旧数据,新数据 ))

Java中的乐观锁大部分都是基于CAS(Compare And Swap,比较和交换)操作实现的,CAS设一种原子操作,在对数据操作之前,首先会比较当前值跟传入值是否一样,如果一样咋更新,否则不执行更新操作直接返回失败状态。compareAndSwapInt 也是 CAS 的核心。

2、Unsafe 类简介

Unsafe 类和 C++ 有点类似,在 Java 中是没有办法直接操作内存的,但是 Unsafe 类却可以间接的让程序员操作内存区域。

Unsafe 是位于 sun.misc 包下的一个类。Unsafe 提供的 API 大致可分为内存操作、CAS、Class 相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几类。由于并发相关的源码很多用到了 CAS,比如 java.util.concurrent.atomic 相关类、AQS、CurrentHashMap 等相关类。

CAS 主要相关源码:

    /**
     * 参数说明
     * @param o             包含要修改field的对象
     * @param offset        对象中某个参数field的偏移量,该偏移量不会改变
     * @param expected      期望该偏移量对应的field值
     * @param x             更新值
     * @return              true|false
     */
    public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);

    public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);

    public final native boolean compareAndSwapLong(Object o, long offset, long expected, long x);

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

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

相关文章

【DOCKER】显示带UI的软件

1. Linux 1.1 宿主机开放X server权限 xhost 1.2 启动容器 docker run -it --rm --privilegedtrue --useru20 --workdir/home/u20 \ -e DISPLAYhost.docker.internal:0 u20:dev1.3 测试 # 安装测试软件 sudo apt-get -y install x11-apps# 显示测试程序 xclock2. Windows …

websocket的学习

第一步&#xff1a;配置Spring <dependency><groupId>org.springframework</groupId><artifactId>spring-messaging</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> …

RabbitMQ知识总结(基本原理+高级特性)

文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 文章收录在网站&#xff1a;http://hardyfish.top/ 基本原理 消息的可靠性投递 RabbitMQ 消息的投递路径为&#xff…

Idea包含UI内容的插件开发

Idea包含UI内容的插件开发 前言插件效果项目结构配置功能的实现找一个股票接口完成最终的页面配置Plugin.xml源码地址 前言 在这一篇文章中将会做一个包含UI内容的能看股票的插件。 插件效果 首先是在设置中配置股票的编号&#xff0c;如sh000001,sh600519。 接着在侧边栏中…

手机端微信聊天记录无法全部同步到电脑端的微信?搞定它!

前言 昨天晚上深夜…… 哼哼&#xff0c;想哪去了&#xff1f; 昨天有个深圳的哥们跟小白吐槽&#xff1a;手机端的微信聊天记录怎么没办法自动同步到电脑端上&#xff1f; 刚开始小白还以为他是因为电脑端的微信在线也没办法同步聊天记录&#xff0c;所以就给出了答案&…

样式与特效(3)——实现一个测算页面

这次我们使用前端实现一个简单的游戏页面,理论上可以增加很多玩法&#xff0c;&#xff0c;但是这里为了加深前端的样式和JS点击事件&#xff0c;用该案例做练习。 首先需要掌握手机端的自适应&#xff0c;我们是只做手机端玩家页面 。需要允许自适应手机端页面&#xff0c; 用…

OpenCV||超详细的图像处理模块

一、颜色变换cvtColor dst cv2.cvtColor(src, code[, dstCn[, dst]]) src: 输入图像&#xff0c;即要进行颜色空间转换的原始图像。code: 转换代码&#xff0c;指定要执行的颜色空间转换类型。这是一个必需的参数&#xff0c;决定了源颜色空间到目标颜色空间的转换方式。dst…

实现元素定位:掌握Selenium八大定位方法

文章目录 0. 八大定位方法1. id2. name3. xpath4. css_selector 0. 八大定位方法 当实现测试自动化&#xff0c;编写测试用例时&#xff0c;首先需要在web界面找到对应元素位置&#xff0c;而Selenium提供了一套对应的API&#xff0c;被封装在WebDriver类中。如下图&#xff0…

JAVA字符串刷题(力扣经典算法及题解)

练习一&#xff1a; 输入字符串aba,依次输出各个字符 import java.util.Scanner;public class StringTomrs {public static void main(String[] args) {Scanner scnew Scanner(System.in);String numbersc.next();System.out.println("输入的字符串是"number);for(i…

使用FastChat快速部署LLM服务

原文&#xff1a;https://zhuanlan.zhihu.com/p/705915093 FastChat 是一个用于训练、服务和评估基于LLM的聊天机器人的开放平台&#xff0c;它的核心功能包括&#xff1a; 最先进模型&#xff08;例如 Vicuna、MT-Bench&#xff09;的训练和评估代码。具有 Web UI 和与 Open…

<Rust>使用rust实现crc16_modbus校验码生成?

前言 本文是使用rust代码来实现crc16 modbus校验码的输出。 概述 crc16 modbus算法简介: 代码实现: crc16 modbus是crc校验码的其中一种计算方式,通常用于modbus类通讯的数据校验上。 其计算步骤如上面的图片所示,通常此校验算法用在工控行业比较多,如一些支持串口通讯…

Linux驱动----总线

总线相关 总线注册和注销总线device对象----描述设备信息&#xff0c;包括地址&#xff0c;中断号和其他的一些自定义数据注册和注销device对象----指将device注册到mybus总线 driver对象----描述设备驱动的方法&#xff08;操作地址和中断&#xff09;注册和注销driver对象---…

38 器件移动、旋转、镜像、对齐、等间距操作介绍39 器件、网络、过孔锁定与解锁操作40 相同模块复用操作41 测量、查询功能介绍

38 器件移动、旋转、镜像、对齐、等间距操作介绍&&39 器件、网络、过孔锁定与解锁操作&&40 相同模块复用操作&& 41 测量、查询功能介绍 第一部分 38 器件移动、旋转、镜像、对齐、等间距操作介绍第二部分 39 器件、网络、过孔锁定与解锁操作第三部分 4…

明确工作目标学习稿:计算技术体系的发展方向

学习2022年《孙凝晖&#xff1a;建立高水平自立自强的计算技术新体系》 计算所新时期的目标就是要建立高水平自立自强的计算技术新体系&#xff0c;其中&#xff0c;针对处理器提出“C体系”、针对信息基础设施提出“信息高铁”体系。 计算技术体系的新结构 包含C体系、信息高…

自建数据库VS云数据库

自建数据库VS云数据库 什么是自建数据库&#xff1f;自建数据库方案自建数据库的优点自建数据库的缺点什么是云数据库&#xff1f;自建数据库的缺点什么是云数据库&#xff1f; 云数据库方案云数据库的优点云数据库的缺点适用场景比较总结 【纪录片】中国数据库前世今生 在数字…

日志采集格式指定实验

目录 一. 进入配置文件&#xff0c;有两种方式指定采集格式 &#xff08;1&#xff09;日志默认格式指定 &#xff08;2&#xff09;指定用liu的格式采集某一个日志 二.重启服务 三.测试 #WESTOS_FORMAT&#xff1a; 格式名称 #%FROMHOST-IP%&#xff1a; 日志来源主…

合作开发中的Simulink算法保护

项目越来越大&#xff0c;分工越来越细&#xff0c;合作开发已经成为常态。在几家公司或者几个团队合作开发同一个项目的时候&#xff0c;经常会出现互相之间技术上要做一些保密工作&#xff0c;一方做的模型或代码不能给另一方看到&#xff0c;但又要求可以让对方能够运行自己…

File类的用法

目录 File的常见方法 普通文件的创建 普通文件的删除 deleteOnExit 目录的创建 mkdir mkdirs 文件的重命名和剪切 剪切 重命名 InputStream read() OutputStream write() Reader Writer write(String str) 代码练习 扫描指定⽬录&#xff0c;并找到名称中包含…

git clone private repo

Create personal access token Clone repo $ git clone https://<user_name>:<personal_access_tokens>github.com/<user_name>/<repo_name>.git

【DOCKER】VNC可视化UBUNTU容器

1. 启动测试容器 # 启动容器 # -e USERu20 vncserver所需环境变量 # -p 15901:5901 vncserver所需端口 docker run -id --privilegedtrue --restart always --useru20 --workdir/home/u20 -p 15901:5901 -e USERu20 --name ui u20:dev# 进入容器 docker exec -it ui /bin/ba…