Java并发编程基础盘点3 - CAS

简介: Java并发编程基础盘点3 - CAS

CAS(Compare and Swap),中文可以理解为比较并替换,是一种实现并发算法时常用到的技术。它是一种无锁原子算法,CAS是一种乐观锁的实现方式,在操作时是抱着乐观的态度进行的,它总是认为可以成功完成操作。

让我们先直观的理解一下CAS的大体思路:

CAS(V,E,N)

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

先从一个简单例子开始:

public class CasTest0 {
    private static volatile int m = 0;
    public static void increase1() {
        m++;
    }
    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 1000; i++) {
           new Thread(() -> {
                CasTest0.increase1();
            }).start();
        }
        TimeUnit.SECONDS.sleep(3);
        System.out.println(m);
    }
}

运行这个例子,最后打印出的值可能是任意小于1000的正整数。在之前的文章中讲过,volatile可以保证可见性和有序性,但是无法保证原子性。而m++这一操作又不是原子操作,可以分为三个步骤:

image.png

获取静态变量m的值并入栈,int型常量值1入栈

栈顶int型数值相加,并将结果压入栈顶

为静态变量m赋值

从上述分析可得,自增操作并不具有原子性,所以在多线程环境下,运行得到的结果必定小于等于1000。

换成JUC包下的原子类操作试一下:

public class CasTest1 {
    private static AtomicInteger atomicI = new AtomicInteger(0);  
    public static void increase2() {
        atomicI.incrementAndGet(); 
    }
    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                CasTest1.increase2();
            }).start();
        }
        TimeUnit.SECONDS.sleep(3);
        System.out.println(atomicI.get());
    }
}

这样运行结果会始终返回1000,这就取决于原子Integer类AtomicInteger发挥了作用。

反编译看一下实际执行的指定,这里是仅以一条指令完成了自增的操作

image.png

看一下AtomicInteger类的实现:

image.png

在AtomicInteger中使用了Unsafe类,这个类可以说是Java提供的一个后门类,可以用来直接操作内存地址;

valueOffset,变量value的地址偏移量,具体赋值是在下面的静态代码块中进行的;

value就是需要修改的值,相当于i++操作中的i。

incrementAndGet最终调用Unsafe类中的方法:

//获取内存地址为obj+offset的变量值, 并将该变量值加上delta
public final int getAndAddInt(Object obj, long offset, int delta) {
    int v;
    do {
        //通过对象和偏移量获取变量的值
        //由于volatile的修饰, 所有线程看到的v都是一样的
        v= this.getIntVolatile(obj, offset);
    } while(!this.compareAndSwapInt(obj, offset, v, v + delta));
    return v;
}

具体流程:

1、while循环中的compareAndSwapInt()方法尝试修改v的值, 该方法会通过obj和offset获取变量的值;

2、如果这个值和v不一样,说明其他线程修改了obj+offset地址处的值,此时compareAndSwapInt()返回false,继续循环;

3、如果这个值和v一样,说明没有其他线程修改obj+offset地址处的值,此时可以将obj+offset地址处的值改为v+delta, compareAndSwapInt()返回true,退出循环。

compareAndSwapInt是一个native方法,调用了C++中的方法,后续调用链为调用汇编中的cmpxchg指令,最终通过二进制硬件支持实现了这一原子操作。

那么,CAS都有什么应用场景呢?典型场景就是电商中对于货物的库存的管理。首先从数据库中读取库存,在卖出货物后更新库存时,判断库存数量是否还和自己取出时相同,如果相同则更新,不同则进行自旋直到执行成功。

说了这么多,那么CAS就是完美的吗,很遗憾并不是,CAS仍然存在经典的ABA问题

按照我们之前的理解,CAS需要检查操作值有没有发生改变,如果没有发生改变则更新。但是存在这样一种情况:如果一个值原来是A,变成了B,然后又变成了A,那么在CAS检查的时候会发现没有改变,但是实质上它已经发生了改变,这就是所谓的ABA问题。

public class CasTest2 {
    private static AtomicInteger atomicI = new AtomicInteger(100);
    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+":"+atomicI.compareAndSet(100, 110));
        },"thread1");
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println(Thread.currentThread().getName()+":"+atomicI.compareAndSet(110, 100));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"thread2");
        t2.start();
        Thread t3 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName()+":"+atomicI.compareAndSet(100, 90));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"thread3");
        t3.start();
    }
}

运行结果:

image.png

三个线程的运行结果都为true,但是thread3执行时候检查的值是已经被中途修改过的,而不是初始值了。

应对ABA问题,其解决方案是加上版本号,即在每个变量都加上一个版本号,每次改变时加1。

即将原来的:A —> B —> A

变成:1A —> 2B —> 3A

这里引入AtomicStampedReference这个类,它内部不仅维护了对象值,还维护了一个int类型的stamp值,可以将其理解为时间戳或版本号。当AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还必须要更新这个stamp的值。并且当AtomicStampedReference设置对象值时,对象值以及stamp值都必须满足期望,写入才会成功。因此,即使对象值被反复读写,写回原值,只要stamp的值发生变化,就能防止不恰当的写入。

public class CasTest3 {
    private static AtomicStampedReference asr = new AtomicStampedReference(100, 1);
    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("1:" + asr.compareAndSet(100, 110, asr.getStamp(), asr.getStamp() + 1) );
            System.out.println("stamp:"+asr.getStamp()+"  value:"+asr.getReference());
            System.out.println("2:" + asr.compareAndSet(110, 100, asr.getStamp(), asr.getStamp() + 1) );
            System.out.println("stamp:"+asr.getStamp()+"  value:"+asr.getReference());
        });
        Thread t2 = new Thread(() -> {
            int stamp = asr.getStamp();
            try {
                TimeUnit.SECONDS.sleep(4);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("3:" + asr.compareAndSet(100, 110, stamp, stamp + 1) );
            System.out.println("stamp:"+asr.getStamp()+"  value:"+asr.getReference());
        });
        t1.start();
        t2.start();
    }
}

运行结果:

image.png

Thread2期待的stamp值为1,Reference的值为100。Thread1在每次自增的同时,stamp值加1,所以在经过Thread1两次修改Reference值后,即使与期望的Reference值相同,但stamp值不同,仍然不做任何修改。

CAS缺点:

最后对CAS的缺点进行一下总结,CAS虽然高效地解决了原子操作问题,但是还是存在一些缺陷的,主要表现在三个方面:

1.循环时间太长

如果CAS一直不成功的情况发生,会一直进行自旋操作,会造成大量CPU执行开销。在JUC中有些地方就限制了CAS自旋的次数,例如BlockingQueue的SynchronousQueue。

2.只能保证一个共享变量原子操作

看了CAS的实现过程,可以得出CAS只能针对一个共享变量,如果是多个共享变量情况,只能使用锁来保证原子性了。

3.ABA问题

CAS需要检查操作值有没有发生改变,如果没有发生改变则更新,但是之前提到的ABA问题会造成一定影响,这时只要加上版本号对其进行限定就可以了。

相关文章
|
2天前
|
安全 Java 调度
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第12天】 在现代软件开发中,多线程编程是提升应用程序性能和响应能力的关键手段之一。特别是在Java语言中,由于其内置的跨平台线程支持,开发者可以轻松地创建和管理线程。然而,随之而来的并发问题也不容小觑。本文将探讨Java并发编程的核心概念,包括线程安全策略、锁机制以及性能优化技巧。通过实例分析与性能比较,我们旨在为读者提供一套既确保线程安全又兼顾性能的编程指导。
|
2天前
|
数据采集 安全 Java
Java并发编程学习12-任务取消(上)
【5月更文挑战第6天】本篇介绍了取消策略、线程中断、中断策略 和 响应中断的内容
29 4
Java并发编程学习12-任务取消(上)
|
16小时前
|
存储 关系型数据库 MySQL
《MySQL 入门教程》第 05 篇 账户和权限,Java高并发编程详解深入理解pdf
《MySQL 入门教程》第 05 篇 账户和权限,Java高并发编程详解深入理解pdf
|
21小时前
|
NoSQL Dubbo Java
StringBoot编程式事务与声明式事务java工程师面试突击第一季
StringBoot编程式事务与声明式事务java工程师面试突击第一季
|
1天前
|
Java 程序员 C++
Java中CAS详解
Java中CAS详解
5 0
|
1天前
|
安全 Java 开发者
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第15天】本文将深入探讨Java并发编程的核心概念,包括线程安全和性能优化。我们将通过实例分析,理解线程安全的重要性,并学习如何通过各种技术和策略来实现它。同时,我们也将探讨如何在保证线程安全的同时,提高程序的性能。
|
2天前
|
Java 编译器 开发者
Java并发编程中的锁优化策略
【5月更文挑战第15天】 在Java的多线程编程中,锁机制是实现线程同步的关键。然而,不当的锁使用往往导致性能瓶颈甚至死锁。本文深入探讨了Java并发编程中针对锁的优化策略,包括锁粗化、锁消除、锁分离以及读写锁的应用。通过具体实例和性能分析,我们将展示如何有效避免竞争条件,减少锁开销,并提升应用程序的整体性能。
|
2天前
|
消息中间件 并行计算 Java
Java中的多线程编程:基础知识与实践
【5月更文挑战第15天】 在现代计算机编程中,多线程是一个复杂但必不可少的概念。特别是在Java这种广泛使用的编程语言中,理解并掌握多线程编程是每个开发者必备的技能。本文将深入探讨Java中的多线程编程,从基础概念到实际应用场景,为读者提供全面的理论支持和实践指导。
|
2天前
|
Java 程序员 调度
Java中的多线程编程:从理论到实践
【5月更文挑战第14天】在现代计算机技术中,多线程编程是一个重要的概念。它允许多个线程并行执行,从而提高程序的运行效率。本文将从理论和实践两个角度深入探讨Java中的多线程编程,包括线程的基本概念、创建和控制线程的方法,以及如何处理线程同步和通信问题。
|
2天前
|
Java 开发者
深入理解Java并发编程:从基础到高级
【5月更文挑战第13天】本文将深入探讨Java并发编程的各个方面,从基础知识到高级概念。我们将首先介绍线程的基本概念,然后深入讨论Java中的多线程编程,包括线程的创建和控制,以及线程间的通信。接下来,我们将探讨并发编程中的关键问题,如同步、死锁和资源竞争,并展示如何使用Java的内置工具来解决这些问题。最后,我们将讨论更高级的并发编程主题,如Fork/Join框架、并发集合和并行流。无论你是Java新手还是有经验的开发者,这篇文章都将帮助你更好地理解和掌握Java并发编程。