首页 > 编程语言 >Java并发01---JMM模型、Volatile、CAS操作、自旋锁、ABA问题

Java并发01---JMM模型、Volatile、CAS操作、自旋锁、ABA问题

时间:2024-04-26 18:22:41浏览次数:16  
标签:ABA 01 Java CAS value 线程 内存 JMM

@

目录

JMM(Java Memory Model)

首先要明确的是JMM与JVM内存结构不是同一个概念,记的时候不要记混。

我们先来回顾一下JVM内存结构,其包括了堆、方法区、虚拟机栈、程序计数器、本地方法区,其中前二者为线程共享,后三者为线程私有。

其中的线程共享/私有就与我们要介绍的JMM有关。

JMM采用了一种共享数据模型,即JVM在内存中会有一块主内存,并且每一个线程也会有自己的线程内存。这不与JVM内存结构中线程共享/私有的概念恰好一致吗,实际上也是这样的,JMM主内存即为堆,线程内存即为虚拟机栈、程序计数器和本地方法区。

每个线程在执行的时候首先会将数据从主内存中加载至私有内存然后再进行操作。

Volatile修饰

知道JMM模型的主要内容之后,让我们想象一个Java使用场景:

  1. 首先线程A创建对象Integer a = 10;此时JVM会为a在堆中分配一块内存,此时线程A运行结束。
  2. 线程B,线程C被创建,这两者都要对a进行修改,所以JVM会将a复制到B和C的栈中,BC两者分别都对a进行a = a + 1操作,在B中此时a=2,操作结束之后a=3,在C中此时a=2,操作结束之后a=3,操作结束之后这两个线程会将a写回堆内存中。此时问题就发生了,a虽然进行了两次+1操作但是结果仍然是3。

为了解决这个问题我们可以对a进行volatile修饰,volatile关键字会使被修饰的变量有两个特点:

  1. 变量在内存中的可见性
    (1) 当有线程对volatile变量进行修改的时候JVM会强制将该变量在主内存中数据的进行刷新。
    (2) 这种刷新会让别的线程已被加载的该变量无效化。

  2. 禁止指令重排序

这样子似乎就能解决并发问题了,可是volatile虽然能保证变量在内存中的可见性,但是却无法保证原子性。

让我们再想象一个使用场景:

  1. 首先我们有volatile Integer a = 10;
  2. 线程A要进行以下操作a++;
  3. 线程B要进行以下操作a = 0;
  4. A与B并发执行,由于a++这个操作并不是原子操作,在实际执行的时候会先获得a值再进行自加,所以实际操作的过程中获得a值之后可能会被中断。理想的结果是A中的a最后等于11,但是假如A执行一半被中断了,转换为B去执行,B执行完之后A继续,那么此时结果就变成了1。

我们可以发现,volatile虽然很好,但是还不够好。为了解决上面的问题,我们可以对A线程加锁,使用sychronized对该代码段进行修饰,但是sychronized是一种阻塞式的独占锁悲观锁,在这里介绍一种非阻塞的乐观锁,i.e. 自旋锁,所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。在自旋锁内部里有一个非常重要的概念:CAS方法

CAS(Compare And Swap)

  1. 首先CAS是一种思想。

  2. 其次CAS是一种CPU的原子指令。

  3. 最后在Java的concurrent包中有一系列基于CAS的native方法,如compareAndSet

那么什么是CAS?

顾名思义,CAS(Compare And Swap)先要比较内存中实际的数值与预期数值是否相等,若相等则将内存中的实际值设置为新值。

来看代码示例:

使用Java的concurrent包中有原子类如AtomicInteger,其实现了CAS的操作方法。

AtomicInteger a = new AtomicInteger(10);
System.out.println(a.compareAndSet(10, 20) + " " + a);
System.out.println(a.compareAndSet(10, 30) + " " + a);

// 输出结果:
// true 20
// false 20

首先a为10,第一步compareAndSet预期值为10,实际值为10,可以成功将a设置为新值20。
第二步compareAndSet预期值为10,实际值为20,设置新值失败。

通过CAS操作,我们就可以避免上面提到的并发问题。

下面是一个基于CAS方法的乐观锁自增实现,摘自Java乐观锁的实现原理和典型案例

public class Counter {
    private AtomicInteger value = new AtomicInteger(0);//实际值

    public void increment() {
        int expect;
        int update;

        do {
            expect = value.get(); // 预期值
            update = expect + 1; // 新值
 // 比较value与expect是否相等,相等则使用update更新value
        } while (!value.compareAndSet(expect, update));
    }
}

在上述代码中如果在do代码块中被中断并且value被修改,则value不会被更新,继续循环,直到do代码块不被中断,则value可以被更新,循环结束。

不难看出,这段代码实际上就是一个乐观锁的实现,并且具有以下特点。

  1. 不会阻塞线程。i.e. 非阻塞
  2. 总是假设value没有被更新,若检测到value被更新了(线程冲突),则更新失败。i.e. 乐观
  3. 若更新失败则进入循环,i.e. 自旋

所以说,CAS与自旋并不是同一个概念,自旋的过程每次都是一个CAS操作。

但是CAS会导致一些问题:

  1. do代码块一直被中断,这个锁就会陷入死循环。(Java使用锁的膨胀来解决此问题)
  2. do代码块被中断但是到最后别的线程又把值给改回了预期值,CAS会认为没有线程冲突而成功进行更新的操作,i.e. ABA问题

ABA问题

CAS 的使用流程通常如下,摘自面试必问的CAS,你懂了吗?

  1. 首先从地址 V 读取值 A;
  2. 根据 A 计算目标值 B;
  3. 通过 CAS 以原子的方式将地址 V 中的值从 A 修改为 B。

若在第一步之后有别的线程修改V,然后在第三步之前又将V改回了A这个值,这就产生了ABA问题。

为了解决这个问题,可以使用Java中的AtomicStampedReference,这个类可以为变量添加一个版本号,在修改的时候可以再比较一下版本号是否相同,若相同才能成功进行修改操作。

JUC包中的StampedLock类就使用了这种思想:

StampedLock是一种乐观读写锁stampedLock.tryOptimisticRead()方法会获取一个乐观的读锁,实际上返回值是一个long格式的版本号,如果有地方显式地获取了写锁,这个版本号会被更新,如stampedLock.writeLock()方法。

stampedLock.validate(long stamp)方法可以将传入的版本号与锁内部的版本号进行比较,返回布尔值,我们可以据此编写业务代码。详见廖雪峰的官方网站

实际操作的时候建议明确ABA问题是否会导致程序逻辑错误,若不会导致逻辑错误可以选择使用原子类,否则不如直接加一个互斥锁更方便一点。

标签:ABA,01,Java,CAS,value,线程,内存,JMM
From: https://www.cnblogs.com/Alanxtl/p/18160642

相关文章

  • Java并发02---Synchronized的实现原理、锁的升级、锁的膨胀、对象头、锁的消除、偏向
    @目录何为synchronized前置知识:对象头锁的升级(锁的膨胀)偏向锁轻量级锁轻量级锁锁的消除何为synchronized我们知道,synchronized关键字能够将其修饰的代码块、方法、静态方法变成同步代码。我们在前文中已经介绍过了,使用volatile关键字修饰能保证变量在内存中的可见性,但不保证操作......
  • 车用MCU,R7F701320EAFP、R7F701321EAFP、R7F701322EAFP、R7F701323EAFP微控制器功耗低,
    RH850/P1M——适用于底盘系统的汽车用微控制器简介RH850/P1M微控制器功耗低,闪存容量高达2MB,RAM容量高达128KB,具有增强型电机控制定时器、CAN接口、SENT和PSI5等传感器数字接口以及锁定CPU、ECC、BIST(内置自检)和ECM(错误控制模块)等安全功能,适用于底盘系统。此外,仅2......
  • 洛谷题单指南-动态规划2-P2679 [NOIP2015 提高组] 子串
    原题链接:https://www.luogu.com.cn/problem/P2679题意解读:在a中按顺序挑选k个子串,使得这些子串连在一起正好和b相等,求方案数。解题思路:这样的题目,无外乎两个思路:DFS暴搜(得部分分)、动态规划动态规划不好想,还是先暴搜吧!1、DFS暴搜搜索的思路就是:从a起始位置开始,一直找到跟b前......
  • javascript高级编程系列 - 使用fetch发送http请求
    fetch采用模块化设计,api分散在多个对象上(Response对象,Request对象,Header对象),fetch通过数据流(stream对象)处理数据可以分块读取,有利于提高网站性能。发送GET请求fetch函数只传递一个url,默认以get方法发送请求。promisefetch(url).then(response=>response.json()).......
  • Java树形结构
    表结构createtablecommon_tree(idbigintnotnullcomment'主键'primarykey,p_idbigintnullcomment'父节点id',tree_codevarchar(100)nullcomment'树形区分',tree_describevarch......
  • java反汇编命令手册
    1.栈和局部变量操作1.1将常量压入栈的指令指令功能描述aconst_null将null对象引用压入栈iconst_m1将将int类型常量-1压入栈iconst_0将int类型常量0压入栈iconst_1将int类型常量1压入栈iconst_2将int类型常量2压入栈iconst_3将int类型常量3压入......
  • Java四种实现单例模式
    饿汉式/***1.饿汉式:线程安全,耗费资源*场景:*资源共享:当需要在多个模块中共享同一个实例时*全局访问点:作为全局唯一的访问点,例如日志记录器、配置管理器等。*线程安全要求高:饿汉式单例模式在类加载时就创建实例,因此不存在线程安全问题,适合多线程环境下使用。*避......
  • 01. 计算机运行原理
    【二进制数据】全球所有人都习惯使用十进制数,也许是因为远古时期人类使用手势交流的原因,人类使用十个手指表示十个数据。中文使用一、二、三、四、五、六、七、八、九、十表示十个基础数字,并使用零表示没有任何数据,单个数字表示的数据范围是有限的,超过上限就使用多个数字的组合......
  • 从 Java 8 转换到 Java 11
    截至目前(2024年),十年前发布的Java8依然是Java中应用最广泛的版本,占比37%,其次是Java11。而目前的JDK最新版本为22,最新的LTS版本为JDK21。从Java8迁移到Java11可能意味着很大的工作量。潜在问题包括:删除的API、弃用的包、内部API的使用、对类加载程序的更......
  • 24数媒Java上机2
    对于一个包含N个非负整数的数组A[0..n-1],如果有0<=i<j<n,且A[i]>A[j],则称(A[i],A[j])为数组A中的一个逆序对。现给定一组数,请你算出这组数中共有多少组逆序对。输入格式:共两行,第一行是一个整数N(N≤1000),表示这组数的个数。第二行有N个整数,用空格分隔,为这组数。测试......