java并发编程(三)CAS

一、乐观锁与悲观锁

1.二者区别

乐观锁在写的时候不上锁,写之前检查版本是否被修改,如果被修改,就放弃这次操作;悲观锁是每次写的时候都需要上锁。

2.乐观锁实现

数据库中添加version字段,取出记录时,获取当前version

更新时, version + 1,如果where语句中的version版本不对,则更新失败

java 复制代码
UPDATE product SET price=price+50, `version`=`version` + 1 WHERE id=1 AND `version`=1

如果versio已经被改变,就会重试,再次使用select语句重新获取新的数据

3.悲观锁实现

悲观锁依赖数据库的锁机制实现,主要分为两种:

  1. 行级锁:锁定单行数据(InnoDB 引擎支持,基于索引),粒度细,并发度高;
  2. 表级锁:锁定整张表(MyISAM 引擎默认,InnoDB 也可通过语法触发),粒度粗,并发度低。
    核心前提:InnoDB 引擎下,必须在事务(BEGIN/START TRANSACTION) 中使用悲观锁,且事务未提交前,锁不会释放。
3.1行级悲观锁

通过 SELECT ... FOR UPDATE 或 SELECT ... LOCK IN SHARE MODE 实现,前者是排他锁(X 锁)(禁止其他事务修改 / 加排他锁),后者是共享锁(S 锁)(允许其他事务加共享锁,但禁止加排他锁)。

(1)排他锁FOR UPDATE ):最常用的悲观锁

适用场景:需要修改数据,防止其他事务同时修改。

sql 复制代码
-- 步骤1:开启事务(会话1)
BEGIN;

-- 步骤2:查询并锁定目标行(必须通过索引条件,否则会升级为表锁)
SELECT * FROM user WHERE id = 1 FOR UPDATE;

-- 步骤3:修改锁定的数据(此时其他事务无法修改id=1的行)
UPDATE user SET balance = balance - 100 WHERE id = 1;

-- 步骤4:提交事务(释放锁)
COMMIT;

并发测试

  • 会话 1 执行到步骤 2 但未提交时,会话 2 执行 UPDATE user SET balance = balance - 100 WHERE id = 1 会阻塞,直到会话 1 提交 / 回滚;
  • 如果会话 1 长时间不提交,会话 2 会等待直到超时(由 innodb_lock_wait_timeout 参数控制,默认 50 秒)。
    (2)共享锁(LOCK IN SHARE MODE)
    适用场景:只需要读取数据,防止其他事务修改,但允许其他事务读取。
sql 复制代码
BEGIN;
-- 加共享锁
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE;
-- 读取数据后提交
COMMIT;
3.2表级悲观锁(不推荐,仅作了解)

通过 LOCK TABLES 显式锁定整张表,会阻塞所有对该表的读写操作,InnoDB 下尽量避免使用。

sql 复制代码
-- 锁定user表(读锁):允许当前会话读,禁止其他会话写
LOCK TABLES user READ;

-- 锁定user表(写锁):仅允许当前会话读写,禁止其他会话任何操作
LOCK TABLES user WRITE;

-- 解锁表(必须手动解锁,或会话断开自动解锁)
UNLOCK TABLES;
3.3在多线程层面使用synchronized

二、什么是CAS

锁在并发处理中占据了一席之地,但是使用锁有一个不好的地方,就是当一个线程没有获取到锁时会被阻塞挂起,这会导致线程上下文的切换和重新调度开销。

Java提供了非阻塞的volatile关键字来解决共享变量的可见性问题,这在一定程度上弥补了锁带来的开销问题,但是volatile只能保证共享变量的可见性,不能解决读一改一写等的原子性问题。

CAS即CompareandSwap,其是JDK提供的非阻塞原子性 操作,它通过硬件保证了比较一更新操作的原子性。

它包含 3 个参数 CAS(V,E,N),V表示要更新变量的值,E表示预期值,N表示新值。仅当 V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程则什么都不做。最后,CAS 返回当前V的真实值。(E其实就是旧的version,V是现在的version)

三、CAS具体示例

使用AtomicInteger来修饰共享变量,使用getAndIncrement函数进行自增操作

java 复制代码
public class CasTest {

    //使用AtomicInteger定义a
    static AtomicInteger a = new AtomicInteger();
    public static void main(String[] args) {
        CasTest test = new CasTest();
        Thread[] threads = new Thread[5];
        for (int i = 0; i < 5; i++) {
            threads[i] = new Thread(() -> {
                try {
                    for (int j = 0; j < 10; j++) {
                        //使用getAndIncrement函数进行自增操作
                        System.out.println(a.incrementAndGet());
                        Thread.sleep(500);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
            threads[i].start();
        }
    }
}

四、CAS原理

这里我们以上面的a.incrementAndGet()为例,来看看 Java 是如何实现原子操作的。

先看一下AtomicInteger里面都有什么,后面更好理解

java 复制代码
public class AtomicInteger extends Number implements java.io.Serializable {
    private volatile int value; // 核心变量:volatile保证内存可见性
    private static final long valueOffset; // value的内存偏移量
    private static final Unsafe unsafe = Unsafe.getUnsafe();

    static {
        try {
            // 计算value字段在AtomicInteger中的内存偏移量
            valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) {
            throw new Error(ex);
        }
    }

    // 步骤1调用的方法:incrementAndGet
    public final int incrementAndGet() {
        // 步骤2的核心调用:委托给Unsafe的getAndAddInt,增量为1
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
}

1.先看incrementAndGet()的底层源码

java 复制代码
public final int incrementAndGet() {
    // 核心:调用Unsafe的getAndAddInt,增量为1,返回旧值+1(新值)
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

这条代码的关键是委托给getAndAddInt ------ 而getAndAddInt正是基于 "自旋 + CAS 原子指令" 实现的,这是原子性的根本来源。

2.线程 1 调用 unsafe.getAndAddInt (a, valueOffset, 1)

  • var1 = a:AtomicInteger 实例(对应源码中的this);
  • var2 =valueOffset:AtomicInteger中value字段的内存偏移量(通过objectFieldOffset计算,用于定位value的物理内存地址);
  • var4 =1:自增的增量(对应源码中的1)。
java 复制代码
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

首先,在 do while 循环开始,通过this.getIntVolatile(var1, var2)获取当前对象指定字段的值,将其存入临时变量 var5 中。这里的 getIntVolatile 方法能保证读操作的可见性,即读取的结果是物理内存中的最新值(而非 CPU 缓存的脏数据)。== 这是第一次获取想要更新的值,相当于预期值 ==。

3.线程 1 执行 compareAndSwapInt (a, valueOffset, 0, 1)

这是原子性的核心保障:

  • compareAndSwapInt是 native 方法,底层调用 CPU 的lock cmpxchg指令;
  • lock前缀会锁定 CPU 的系统总线 / 缓存行,确保多核 CPU 下,当前核心对内存地址 V 的 "比较(V 的值是否等于 0)+ 交换(更新为 1)" 操作不可中断(硬件级原子性);
  • 整个操作是 "一次性完成" 的,不会被其他线程 / CPU 核心打断,彻底解决了普通i++"读取 - 修改 - 写回" 三步拆分的非原子问题。

4.while循环

  • 执行compareAndSwapInt(var1, var2, var5, var5 + var4)进行 CAS 操作。如果对象 var1 在内存地址 var2 处的值(第二次获取这个值,相对于是想要更新的那个变量的最新值)等于== 预期值 var5==,则将该位置的值更新为 var5 + var4,并返回 true;否则,不做任何操作并返回 false。
  • 如果 CAS 操作成功,说明我们成功地将 var1 对象的 var2 偏移量处的字段的值更新为 var5 + var4,并且这个更新操作是原子性的,因此我们跳出循环并返回 var5。
  • 如果 CAS 操作失败,说明在我们尝试更新值的时候,有其他线程修改了该字段的值,所以我们继续循环重新获取该字段的值,然后再次尝试进行 CAS 操作。

五、unsafe类

JDK的rt.jar包中的Unsafe类提供了硬件级别的原子性操作,Unsafe类中的方法都是native方法,它们使用JNI的方式访问本地C++实现库。下面我们来了解一下Unsafe提供的几个主要的方法以及编程时如何使用Unsafe类做一些事情。

long objectFieldOffset(Field field)方法:返回指定的变量在所属类中的内存偏移地址,

该偏移地址仅仅在该Unsafe函数中访问指定字段时使用。如下代码使用Unsafe类

获取变量value在AtomicLong对象中的内存偏移。

●int arrayBaseOffset(Class arrayClass)方法:获取数组中第一个元素的地址。

●int arrayIndexScale(Class arrayClass)方法:获取数组中一个元素占用的字节。

●boolean compareAndSwapLong(Object obj, long offiset, long expect, long update)方法:比较对象obj中偏移量为offset的变量的值是否与expect相等,相等则使用update值更新,然后返回true,否则返回false。

●public native long getLongvolatile(Object obj, long offset)方法:获取对象obj中偏移量为offset的变量对应volatile语义的值。

●void putLongvolatile(Object obj, long offset, long value) 方法:设置obj对象中offset偏移的类型为long的field 的值为value,支持volatile语义。

●void putOrderedLong(Object obj, long offset, long value)方法:设置obj对象中offset偏移地址对应的long型field的值为value。这是一个有延迟的putLongvolatile方法,并且不保证值修改对其他线程立刻可见。只有在变量使用volatile修饰并且预计会被意外修改时才使用该方法。

●long getAndSetLong(Object obj, long offset, long update)方法:获取对象obj中偏移量为offset的变量volatile语义的当前值,并设置变量volatile语义的值为update。

1.Unsafe类的安全限制:只能是启动类加载器加载的类

Unsafe类:Unsafe 是一个底层工具类,本身不能直接通过 new 实例化(构造方法是私有的)。JVM 在启动时会自动初始化这个 theUnsafe 静态变量,作为 Unsafe 类的唯一实例,JDK 内部类通过 getUnsafe() 获取 theUnsafe,而普通应用类无法通过 getUnsafe() 获取,只能通过反射读取这个私有静态变量来突破权限限制

java 复制代码
// Unsafe 类的核心定义(简化)
public final class Unsafe {
    // 私有静态单例实例,外部无法直接访问
    private static final Unsafe theUnsafe = new Unsafe();

    // 私有构造方法,禁止外部实例化
    private Unsafe() {}

    // 对外提供的获取方法,但有权限校验
    public static Unsafe getUnsafe() {
        // 校验调用者的类加载器,只有启动类加载器加载的类才能调用
        Class<?> caller = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(caller.getClassLoader())) {
            throw new SecurityException("Unsafe");
        }
        return theUnsafe;
    }
}
java 复制代码
package cn.tx.cas;

import sun.misc.Unsafe;

public class TestUnSafe {

    //获取Unsafe的实例(2.2.1)
    static final Unsafe unsafe = Unsafe.getUnsafe();
    //记录变量state在类TestUnSafe中的偏移值(2.2.2)
    static final long stateOffset;
    //变量(2.2.3)
    private volatile long state = 0;

    static {
        try {
            //获取state变量在类TestUnSafe中的偏移值(2.2.4)
            stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.getDeclaredField("state"));
        } catch (Exception ex) {
            System.out.println(ex.getLocalizedMessage());
            throw new Error(ex);
        }
    }

    public static void main(String[] args) {
        //创建实例,并且设置state值为1(2.2.5)
        TestUnSafe test = new TestUnSafe();
        // (2.2.6)具体意思是,如果test对象中内存偏移量为stateOffset的state变量的值为0,则更新该值为1。
        Boolean sucess = unsafe.compareAndSwapInt(test, stateOffset, 0, 1);
        System.out.println(sucess);

    }
}

运行上面的代码,我们期望输出true,然而执行后会输出如下结果。

你在类加载阶段就执行了Unsafe.getUnsafe(),这段代码报错的核心原因是 Unsafe.getUnsafe() 存在严格的访问权限限制,JVM 不允许普通应用类直接调用这个方法。只有由 ** 引导类加载器(Bootstrap ClassLoader)** 加载的类(比如 JDK 核心类库),才能直接调用该方法。你的TestUnSafe类是由 ** 应用类加载器(App ClassLoader)** 加载的,所以调用Unsafe.getUnsafe()会触发SecurityException,导致静态初始化失败(ExceptionInInitializerError)。

2.解决Unsafe的安全限制:反射

java 复制代码
package cn.tx.cas;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class TestUnSafe1 {

    static final Unsafe unsafe;
    static final long stateOffset;
    private volatile long state = 0;
    static {
        try {
            //使用反射获取Unsafe的成员变量theUnsafe的元信息
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            //突破私有限制
            field.setAccessible(true);
            //获取静态变量的值
            unsafe = (Unsafe) field.get(null);
            //获取state在TestUnSafe中的汇编语言偏移量
            stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.getDeclaredField("state"));
        } catch (Exception ex) {
            System.out.println(ex.getLocalizedMessage());
            throw new Error(ex);
        }
    }

    public static void main(String[] args) {
        //创建实例,并且设置state值为1(2.2.5)
        TestUnSafe1 test = new TestUnSafe1();
        // (2.2.6)
        
        Boolean sucess = unsafe.compareAndSwapInt(test, stateOffset, 0, 1);
        System.out.println(sucess+"------>"+unsafe.getIntVolatile(test, stateOffset));

        Boolean sucess1 = unsafe.compareAndSwapInt(test, stateOffset, 1, 10);
        System.out.println(sucess1+"------>"+unsafe.getIntVolatile(test, stateOffset));

        Boolean sucess2 = unsafe.compareAndSwapInt(test, stateOffset, 9, 20);
        System.out.println(sucess2+"------>"+unsafe.getIntVolatile(test, stateOffset));
    }
}

六、cas的优缺点

1.优点

一种非阻塞的轻量级的乐观锁,相比synchronized重量锁,synchronized会进行比较复杂的加锁,解锁和唤醒操作,性能比较低,而cas则不会有这方面的性能开销。

2.缺点

2.1ABA问题

所谓的 ABA 问题,就是一个值原来是 A,变成了 B,又变回了 A。这个时候使用 CAS 是检查不出变化的,但实际上却被更新了两次。

ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。从 JDK 1.5 开始,JDK 的 atomic 包里提供了一个类AtomicStampedReference类来解决 ABA 问题。

这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果二者都相等,才使用 CAS 设置为新的值和标志。

java 复制代码
//预期引用值,新的引用值,预期标记,新的标记
public boolean compareAndSet(V   expectedReference,
                              V   newReference,
                              int expectedStamp,
                              int newStamp) {
    Pair<V> current = pair;
    return
        expectedReference == current.reference &&
        expectedStamp == current.stamp &&
        ((newReference == current.reference &&
          newStamp == current.stamp) ||
          casPair(current, Pair.of(newReference, newStamp)));
}

下面举个具体的例子:

java 复制代码
package cn.tx.cas.aba;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABAAtomic1 {


    private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<Integer>(100, 0);

    public static void main(String[] args) {
        Thread refT1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                atomicStampedRef.compareAndSet(100, 101, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
                System.out.println("thread refT1:" + atomicStampedRef.getReference());
                atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
                System.out.println("thread refT1:" + atomicStampedRef.getReference());
            }
        });

        Thread refT2 = new Thread(new Runnable() {
            @Override
            public void run() {
                int stamp = atomicStampedRef.getStamp();
                System.out.println("before sleep : stamp = " + stamp);    // stamp = 0
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("after sleep : stamp = " + atomicStampedRef.getStamp());//stamp = 1
                boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);
                System.out.println("thread refT2:" + atomicStampedRef.getReference() + ",c3 is " + c3);        //true
            }
        });
        refT1.start();
        refT2.start();
    }
}
时间节点 线程 refT2(线程 2) 线程 refT1(线程 1) atomicStampedRef 状态(引用值,stamp)
0 秒 启动,执行: 1. 获取初始 stamp=0,打印before sleep : stamp = 0 2. 调用sleep(2秒),进入休眠 启动,执行: 1. 调用sleep(1秒),进入休眠 (100, 0)
1 秒 仍在休眠(剩余 1 秒) 休眠结束,执行: 1. compareAndSet(100, 101, 0, 1): ✅ 预期引用 = 100、预期 stamp=0,匹配当前状态,修改为 (101, 1) 2. 打印thread refT1:101 3. compareAndSet(101, 100, 1, 2): ✅ 预期引用 = 101、预期 stamp=1,匹配当前状态,修改为 (100, 2) 4. 打印thread refT1:100 (100, 2)
2 秒 休眠结束,执行: 1. 打印after sleep : stamp = 2(当前最新 stamp) 2. compareAndSet(100, 101, 0, 1): ❌ 预期引用 = 100(匹配),但预期 stamp=0≠当前 stamp=2,CAS 失败 3. 打印thread refT2:100,c3 is false 执行完毕 (100, 2)
2.2长时间自旋

CAS 多与自旋结合。如果自旋 CAS 长时间不成功,会占用大量的 CPU 资源。

解决思路是让 JVM 支持处理器提供的pause 指令。

pause 指令能让自旋失败时 cpu 睡眠一小段时间再继续自旋,从而使得读操作的频率降低很多,为解决内存顺序冲突而导致的 CPU 流水线重排的代价也会小很多。

2.3多个共享变量的原子操作

CPU 的 cmpxchg 指令只能对一个内存地址执行 "比较 + 交换" 的原子操作。当对一个共享变量执行操作时,CAS 能够保证该变量的原子性。但是对于多个共享变量,CAS 就无法保证操作的原子性,这时通常有两种做法:

  • 使用AtomicReference类保证对象之间的原子性,把多个变量放到一个对象里面进行 CAS 操作;
  • 使用锁。锁内的临界区代码可以保证只有当前线程能操作。
相关推荐
油丶酸萝卜别吃2 小时前
Mapbox GL JS 表达式 (expression) 条件样式设置 完全指南
开发语言·javascript·ecmascript
尤老师FPGA2 小时前
使用ZYNQ芯片和LVGL框架实现用户高刷新UI设计系列教程(第四十五讲)
android·java·ui
爱吃大芒果2 小时前
Flutter for OpenHarmony前置知识:Dart 语法核心知识点总结(下)
开发语言·flutter·dart
Ulyanov2 小时前
从桌面到云端:构建Web三维战场指挥系统
开发语言·前端·python·tkinter·pyvista·gui开发
星火开发设计2 小时前
C++ 函数定义与调用:程序模块化的第一步
java·开发语言·c++·学习·函数·知识
cypking2 小时前
二、前端Java后端对比指南
java·开发语言·前端
钟离墨笺3 小时前
Go语言--2go基础-->map
开发语言·后端·golang
lsx2024063 小时前
DOM CDATA
开发语言
Tony Bai3 小时前
Go 语言的“魔法”时刻:如何用 -toolexec 实现零侵入式自动插桩?
开发语言·后端·golang