【JUC并发编程系列】深入理解Java并发机制:CAS算法与原子类在Java中的实践应用(二、CAS)

文章目录

  • 【JUC并发编程系列】深入理解Java并发机制:CAS算法与原子类在Java中的实践应用(二、CAS)
    • 1. 同步之原子类(Atomic类)
    • 2. 使用atomicInteger计数
    • 3. 使用atomicInteger底层原理
    • 3. compareAndSet原理分析
      • 3.1 手写AtomicInteger
      • 3.2 手写Lock锁
      • 3.3 CAS aba的问题
      • 3.4 AtomicMarkableReference
    • 4. CAS 优点
    • 5. 悲观锁与乐观锁
      • 5.1 悲观锁
      • 5.2 乐观锁

【JUC并发编程系列】深入理解Java并发机制:CAS算法与原子类在Java中的实践应用(二、CAS)

CAS: Compare and Swap,翻译成比较并交换。底层会执行函数CAS(V,E,N)

CAS有3个操作数,内存值V,旧的预期值E,要修改的新值N。当且仅当预期值E和内存值V相同时,将内存值V修改为N,否则什么都不做。

保证线程安全性的问题 原子性

(V,E,N)===乐观锁

V:全局共享变量 原子类中 value值 ===0

E:旧的预期值

N:修改的新值

CAS:无锁机制----自旋控制乐观锁

缺点:自旋会消耗的cpu的资源 cas 死循环 空转问题 cpu飙高

底层基于unsafe类实现

【JUC并发编程系列】深入理解Java并发机制:CAS算法与原子类在Java中的实践应用(二、CAS)_第1张图片

里面的 unsafe.getAndAddInt(this, valueOffset, 1)中的

  • this===atomicInteger 对象;
  • valueoffset ==值偏移量;
  • 1
public final int getAndAddInt(Object paramObject, long paramLong, int paramInt) {
    //循环目的 就是为了cas的重试
    while (true) {
        //paramObject=atomicInteger 对象
        //Valueoffset ==值偏移量;
        //根据 this 和 valueOffset 获取当前的 atomicInteger 中 value值
        int i = getIntVolatile(paramObject, paramLong);//i=0
        if (compareAndSwapInt(paramObject, paramLong, i, i + paramInt))
            return i;
    }
}

1. 同步之原子类(Atomic类)

属于Java并发包类库(原子类 cas)

比如我们所学习的:原子类同步之原子类(Atomic类)

  1. 原子更新基本类型类 cas

    • AtomicBoolean: 原子更新布尔类型。
    • AtomicInteger: 原子更新整型。
    • AtomicLong: 原子更新长整型。
  2. 原子更新数组

    • AtomicIntegerArray: 原子更新整型数组里的元素。
    • AtomicLongArray: 原子更新长整型数组里的元素。
    • AtomicReferenceArray: 原子更新引用类型数组里的元素。
  3. 原子更新引用类型

    • AtomicReference: 原子更新引用类型。
    • AtomicReferenceFieldUpdater: 原子更新引用类型的字段。
    • AtomicMarkableReferce: 原子更新带有标记位的引用类型,可以使用构造方法更新一个布尔类型的标记位和引用类型。
  4. 原子更新字段类

    • AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。
    • AtomicLongFieldUpdater: 原子更新长整型字段的更新器。
    • AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型。
  5. JDK8新增原子类

    • DoubleAccumulator:DoubleAccumulator 是一个用于累积双精度浮点数的原子类。它允许通过一个给定的组合函数来原子地累加值。这个类通常用于并行计算场景,例如并行流处理中的数值累积。

    • LongAccumulator:类似于 DoubleAccumulatorLongAccumulator 用于累积长整型数值。它也支持通过一个组合函数原子地累加值,适用于需要在无锁情况下对长整型数据进行并行累加的场景。

    • DoubleAdder:DoubleAdder 是一个专门用于原子地累加双精度浮点数的类。与 DoubleAccumulator 不同,DoubleAdder 默认使用加法作为组合函数,因此它简化了累加操作。它适用于需要快速并行累加双精度值的场景。

    • LongAdder:LongAdderDoubleAdder 类似,但用于长整型数值。它提供了原子的累加操作,适合在高并发环境下快速累加长整型数据。

    Java 层面是由 Unsafe 类实现的

2. 使用atomicInteger计数

public class Test01 extends Thread {
    //value == 0
    private static AtomicInteger atomicInteger = new AtomicInteger(0);

    @Override
    public void run() {
        sum();
    }

    private static void sum() {
        for (int i = 0; i < 1000; i++) {
            //value++
            atomicInteger.incrementAndGet();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Test01 t1 = new Test01();
        Test01 t2 = new Test01();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(atomicInteger.get());
    }
}

atomicInteger 计数能够保证线程安全问题。

3. 使用atomicInteger底层原理

atomicInteger底层基于CAS实现

【JUC并发编程系列】深入理解Java并发机制:CAS算法与原子类在Java中的实践应用(二、CAS)_第2张图片

Unsafe类,全限定名是sun.misc.Unsafe,从名字中我们可以看出来这个类对普通程序员来说是“危险”的,一般应用开发者不会用到这个类。

Unsafe类是"final"的,不允许继承。且构造函数是private

传递三个参数 【thisatomicInteger thisvalueOffset(成员变量在内存中偏移量) ,1 】+1

Value=对象内存地址+内存偏移量

CAS前置知识Unsafe

Unsafe对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得 java9已经移除Unsafe

Cas底层 Unsafe类实现-----java层面

Cas 能够保证线程安全性 数据的原子性------cpu硬件支撑

public class Test04 {
    private static AtomicInteger atomicInteger = new AtomicInteger();

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        // 1.使用反射获取theUnsafe属性
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);//设置可以访问私有属性
        // 2.获取到unsafe
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);
        // 1.获取域的偏移地址
        long idOffset = unsafe.objectFieldOffset(Lock.class.getDeclaredField("state"));
        Lock lock = new Lock();
		// 获取 state 字段的偏移量
        long idOffset = unsafe.objectFieldOffset(Lock.class.getDeclaredField("state"));
        /**
         * Object o 想要修改的对象
         * long offset 需要修改字段的偏移值
         * int expected 旧的预期值
         * int x 拟修改的值
         */
        boolean result = unsafe.compareAndSwapInt(lock, idOffset, 0, 1);
        System.out.println(result);
    }

    static class Lock {
        int state;
    }
}
public final int getAndAddInt(Object paramObject, long paramLong, int paramInt) {
    while (true) {
        int i = getIntVolatile(paramObject, paramLong);
        if (compareAndSwapInt(paramObject, paramLong, i, i + paramInt))
            return i;
    }
}

从对象的内存处开始,获得原始字节偏移量,用于存储实力对象的内存

在这里偏移量的意思就像我们 new 一个数组,数组的地址就是数组地一个元素的地址,假如数组地址是 a,第二个元素就是a+1,其中+1就是偏移量。对应的对象的一个属性的偏移量就是其对象的地址开始增加,增加的数就是这个filed的偏移量。

对于value这个值我们知道了,他是AtomicInteger中的value属性对应的偏移量,就是对象地址+value = value的地址

循环:获取当前atomicInteger 原子类的 value=0 i=0

compareAndSwapInt(1,1,1+1);

【JUC并发编程系列】深入理解Java并发机制:CAS算法与原子类在Java中的实践应用(二、CAS)_第3张图片

AtomicInteger 底层如何实现:

  1. 创建了一个AtomicInteger
  2. 先定义一个 int类型value
  3. 底层都是基于unsafe类实现

AtomicInteger源码:

/**
 *就是对旧的预期值 +1
 */
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
//参数1 this new AtomicInteger 
//参数2  valueOffset
//参数3  1

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do { 
		// var5 获取最新的 value 值 即当前值 (旧预期值 E) AtomicInteger类中的 value 字段加上了volatile 关键字
        var5 = this.getIntVolatile(var1, var2);
		//this.compareAndSwapInt(var1, var2, var5, var5 + var4)
        //var1 this new AtomicInteger 对象
        //var2 value的值 offset
        //var5---当前value值=0 (旧预期值 E)
        //var5(0) + var4(1)---当前value值+1
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

3. compareAndSet原理分析

compareAndSet(e,n);

E==V 则将N的值赋值给V;

3.1 手写AtomicInteger

public class TestAtomicInteger {
    /**
     * 自动实现自增 并且不发生线程安全性问题
     */
    private static AtomicInteger atomicInteger = new AtomicInteger(0);

    public int incrementAndGet() {
        //返回新的 value
        return getAndAddInt() + 1;
    }

    public int getAndAddInt() {
        //对当前的全局变量值 value++
        //获取旧的预期值
        int expect = atomicInteger.get();
        //此段代码会引起 cpu 飙高的问题
        while (true) {
            if (atomicInteger.compareAndSet(expect, expect + 1)) {
                return expect;
            }
            try {
                //休眠 100 毫秒 避免 cpu 飙高的问题
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static void main(String[] args) {
//        boolean result = atomicInteger.compareAndSet(0, 2);
//        System.out.println(result+","+atomicInteger.get());
        TestAtomicInteger testAtomicInteger = new TestAtomicInteger();
        testAtomicInteger.incrementAndGet();
        testAtomicInteger.incrementAndGet();
        testAtomicInteger.incrementAndGet();
        System.out.println(atomicInteger.get());
    }
}

3.2 手写Lock锁

public class TestLock {
    /**
     * initialValue = 0 表示没有线程使用
     * initialValue = 1 表示有一个线程正在使用
     */
    private AtomicInteger lockState = new AtomicInteger(0);

    /**
     * 获取锁
     */
    public void lock() {
        /**
         * 将 initialValue 从 0 => 1
         * 如果有两个线程同时获取锁,最终只会有一个线程获取成功
         * 没有获取到锁会进行自旋
         */
        while (true) {
            if (lockState.compareAndSet(0, 1)) {
                return;
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * 释放锁
     */
    public void unlock() {
        while (true) {
            if (lockState.compareAndSet(1, 0)) {
                return;
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class Test05 implements Runnable {
    private TestLock testLock = new TestLock();
    private static int count = 100;

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            cal();
        }
    }

    public void cal() {
        try {
            //获取锁
            testLock.lock();
            if (count > 1) {
                count--;
                System.out.println(Thread.currentThread().getName() + "count=" + count);
            }
        } catch (Exception e) {

        } finally {
            //释放锁
            testLock.unlock();
        }
    }

    public static void main(String[] args) {
        Test05 test05 = new Test05();
        new Thread(test05).start();
        new Thread(test05).start();
    }
}

3.3 CAS aba的问题

Cas主要检查 内存值V与旧的预值值=E是否一致,如果一致的情况下,则修改。

这时候会存在ABA的问题:如果将原来的值A,改为了B,B又改为了A 发现没有发生变化,实际上已经发生了变化,所以存在Aba问题。

解决办法:通过版本号码,对每个变量更新的版本号码做+1

解决aba问题是否大:概念产生冲突,但是不影响结果,换一种方式 通过版本号码方式。

【JUC并发编程系列】深入理解Java并发机制:CAS算法与原子类在Java中的实践应用(二、CAS)_第4张图片

3.4 AtomicMarkableReference

  • 第一个参数expectedReference:表示预期值。

  • 第二个参数newReference:表示要更新的值。

  • 第三个参数expectedStamp:表示预期的时间戳。

  • 第四个参数newStamp:表示要更新的时间戳。

public class Test004 {
    // 注意:如果引用类型是Long、Integer、Short、Byte、Character一定一定要注意值的缓存区间!
    // 比如Long、Integer、Short、Byte缓存区间是在-128~127,会直接存在常量池中,而不在这个区间内对象的值则会每次都new一个对象,那么即使两个对象的值相同,CAS方法都会返回false
    // 先声明初始值,修改后的值和临时的值是为了保证使用CAS方法不会因为对象不一样而返回false
    private static final Integer INIT_NUM = 1000;
    private static final Integer UPDATE_NUM = 100;
    private static final Integer TEM_NUM = 200;
    private static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(INIT_NUM, 1);

    public static void main(String[] args) {
        new Thread(new Runnable() {
            public void run() {
                Integer value = (Integer) atomicStampedReference.getReference();
                int stamp = atomicStampedReference.getStamp();
                System.out.println(Thread.currentThread().getName() + " : 当前值为:" + value + " 版本号为:" + stamp);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // value 旧值 内存中的值   UPDATE_NUM 修改的值
                if (atomicStampedReference.compareAndSet(value, UPDATE_NUM, 1, stamp + 1)) {
                    System.out.println(Thread.currentThread().getName() + " : 当前值为:" + atomicStampedReference.getReference() + " 版本号为:" + atomicStampedReference.getStamp());
                } else {
                    System.out.println("版本号不同,更新失败!");
                }
            }
        }, "线程A").start();

    }
}

4. CAS 优点

CAS(Compare and Swap)或 Compare and Set 是一种无锁编程中常用的原子操作。它通常用于实现线程安全的数据结构和算法,而无需使用传统的互斥锁。CAS 操作的主要优点包括:

  1. 避免死锁
    CAS 减少了因传统锁机制而导致的死锁风险。在多线程环境中,如果线程获取锁的顺序不当,就可能产生死锁。CAS 操作不需要显式地获取和释放锁,从而降低了死锁的可能性。

  2. 提高并发性能
    CAS 操作可以并行执行,当多个线程尝试更新同一个数据时,其中一个线程会成功执行 CAS,而其他线程将失败并重试。这允许更多的线程同时进行工作,提高了系统的整体吞吐量。

  3. 低开销
    与锁相比,CAS 操作在硬件级别上实现,通常具有更低的系统开销。锁需要维护锁定状态、等待队列等,而 CAS 只需要简单的比较和交换指令。

  4. 非阻塞性
    使用 CAS 进行的更新是非阻塞的,这意味着即使一个线程在等待其 CAS 操作成功,它也不会阻止其他线程执行自己的 CAS 操作。这有助于保持系统的响应性和效率。

  5. 灵活性
    CAS 操作为设计高度优化的并发算法提供了基础。开发者可以直接控制并发行为,而不必依赖于更高层次的同步原语,如锁。

  6. 支持构建更复杂的同步结构
    利用 CAS 操作,可以构建更高级别的同步结构,如无锁队列、栈和其他数据结构,这些结构在高并发场景下表现优异。

然而,CAS 也并非没有缺点。例如,它可能导致 ABA 问题,在某些情况下需要额外的逻辑来解决。此外,过度的 CAS 失败和重试可能导致某些线程被饥饿,即无法获得更新数据的机会。在设计基于 CAS 的算法时,必须谨慎处理这些问题。

5. 悲观锁与乐观锁

5.1 悲观锁

悲观锁比较悲观,当多个线程对同一行数据实现写的操作的时候,最终只会有一个线程才能写成功,只要谁能够对获取该到行锁则其他线程时不能够对该行数据做任何修写操作,且是阻塞状态。 比如for update 或者事务开启了事务但是没有提交事务。

mysql存储引擎 InnoDB 事务

线程1 对userid=1`` update操作 开启事务 没有提交和回滚事务;

线程2 对 userid=1 update 操作 它会一直阻塞等待;

线程2 它会一直阻塞等待

  • 原因:线程1 开启了事务 没有提交和回滚事务 意味着其他的线程就不能够对该行数据做写的操作就会一直阻塞等待-----触发行锁机制

  • 目的:就是为了保证该行数据的线程安全性问题。

注意:mysql中的autocommit为自动提交。

Value的值为ON,表示autocommit开启。

OFF表示autocommit关闭

set autocommit=0;—关闭

  1. 用 BEGIN, ROLLBACK, COMMIT来实现

    • BEGIN 开始一个事务

    • ROLLBACK 事务回滚

    • COMMIT 事务确认

  2. 直接用 SET 来改变 MySQL 的自动提交模式:

  • SET AUTOCOMMIT=0 禁止自动提交

  • SET AUTOCOMMIT=1 开启自动提交

--查询当前线程对应的事务
select t.trx_mysql_thread_id from information_schema.innodb_trx t
--清理未提交的事务
kill 768;  

悲观锁比较悲观 —没有获取到锁 则会阻塞等待

Java synchronized 如果升级为重量级锁(变成悲观锁) => C++Monitor 对象

5.2 乐观锁

乐观锁比较乐观,通过预值或者版本号比较,如果不一致性的情况则通过循环控制修改,当前线程不会被阻塞,是乐观,效率比较高。

【JUC并发编程系列】深入理解Java并发机制:CAS算法与原子类在Java中的实践应用(二、CAS)_第5张图片

你可能感兴趣的:(JUC并发编程系列,java,JUC,并发编程,并发机制,CAS,后端,面试)