Java多线程与并发编程(4)_CAS与原子类

Java多线程共享模型之乐观锁(CAS与Atomic原子类)

乐观锁的概念是相对于轻量级锁、偏向锁、重量级锁而言的

乐观锁本身是一种有锁似无锁的状态

CAS需要配合valiate使用,即保证共享变量的可见性

背景

有个账户,有两个功能,取款和查询余额,如何保证多线程同时取款不会出现并发问题?

账户接口

interface DecimalAccount{
    //获取余额
    BigDecimal getBalance() ;
    //取款
    void withdraw(BigDecimal account) ;
	
    //模拟多线程取款
    static void demo(DecimalAccount account){
        List<Thread> ts = new ArrayList<>() ;
        /**
         * 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
         * 如果初始余额为 10000 那么正确的结果应当是 0
         */
        for (int i = 0; i < 1000; i++) {
            ts.add(new Thread(()->{
                account.withdraw(BigDecimal.TEN);
            }));
        }
        ts.forEach(Thread::start);
        ts.forEach(t->{
               t.join(); 
        System.out.println(account.getBalance());
    }
}

线程不安全的实现:

    public void withdraw(BigDecimal amount) {
        BigDecimal balance = this.getBalance() ;
        this.balance = balance.subtract(amount) ;
    }
}

解决方案一:重量锁

        synchronized (lock){
            BigDecimal balance = this.getBalance() ;
            this.balance = balance.subtract(amount) ;
        }

解决方案二:采用CAS操作(不加锁/乐观锁)

  //CAS
    AtomicReference<BigDecimal> ref ;

    public BigDecimal getBalance() {
        return  ref.get() ;
    }
    @Override
    public void withdraw(BigDecimal amount) {
         while (true){
             BigDecimal prev = ref.get() ;
             BigDecimal next = prev.subtract(amount) ;
             if (ref.compareAndSet(prev,next)) break;
     }
     

CAS分析

前面看到的 AtomicInteger 的解决方法,内部并没有用锁来保护共享变量的线程安全。那么它是如何实现的呢?

主要是这一句

if (ref.compareAndSet(prev,next)) break;

  • compareAndSet 正是做这个检查,在 set 前,先比较 prev 与ref在内存中的最新值
  • 若不一致,next 作废,返回 false 表示失败比如,别的线程已经做了减法,当前值已经被减成了 990,那么本线程的这次 990 (next)就作废了,进入 while 下次循环重试
  • 若一致,以 next 设置为新值,返回 true 表示成功

compareAndSet,它的简称就是 CAS (也有 Compare And Swap 的说法),它必须是原子操作

注意:

CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性

  • lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态

在多核状态下,某个核执行到带 lock 的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的。

《计组-总线嗅探机制》

CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果

CAS在竟态条件不严重时效率高

synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞

  • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。

  • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。

结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。

  • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

_Atomic原子类

ABA问题

所谓ABA,就是线程1要将"A"改为"C",假定线程2在线程1执行完毕之前把“A”改为“B”又改为“A”。 此时线程1无法得知共享变量”A“被修改过,CAS执行仍会成功

public class TestABA {
    static AtomicReference<String> ref = new AtomicReference<>("A");

    public static void main(String[] args) throws InterruptedException {
        System.out.println("主线程开始");
        String prev = ref.get() ;
        //A->B->A
        AtoBtoA();
        Thread.sleep(1000);
        //change A -> C
        //prev = A1 实际上内存中是A2 CAS以为是prev是最新值
        boolean c = ref.compareAndSet(prev, "C");
        System.out.println("change A->C"+c);
    }

    private static void AtoBtoA() throws InterruptedException {
        //A->B
        new Thread(()->{
            String prev = ref.get();
            boolean b = ref.compareAndSet(prev, "B");
            System.out.println("change A->B"+b);
        }).start();
        Thread.sleep(500);
		//B->A
        new Thread(()->{
            String prev = ref.get();
            boolean a = ref.compareAndSet(prev, "A");
            System.out.println("change B->A"+a);
        }).start();
    }
}
//
主线程开始
change A->Btrue
change B->Atrue
change A->Ctrue

AtomicStampedReference版本号

希望:

只要有其它线程【动过了】共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再加一个版本号

AtomicStampedReference


public class SolveABA {
    //arg2(0) = 版本号
    static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A",0);

    public static void main(String[] args) throws InterruptedException {
        System.out.println("主线程开始");
        String prev = ref.getReference() ;
        //获取版本号
        int stamp = ref.getStamp();
        System.out.println("主版本号为"+stamp);
        AtoBtoA();
        Thread.sleep(1000);
        boolean c = ref.compareAndSet(prev, "C", stamp, stamp + 1);
        //change ->C
        System.out.println("change A->C"+c);
    }

    private static void AtoBtoA() throws InterruptedException {

        new Thread(()->{
            String prev = ref.getReference();
            boolean b = ref.compareAndSet(prev, "B", ref.getStamp(), ref.getStamp() + 1);
            System.out.println("change A->B"+b);
        }).start();
        Thread.sleep(500);
        new Thread(()->{
            String prev = ref.getReference();
            boolean a = ref.compareAndSet(prev, "A", ref.getStamp(), ref.getStamp() + 1);
            System.out.println("change B->A"+a);
        }).start();
    }
}

结果:

主版本号为0
change A->Btrue
change B->Atrue
change A->Cfalse

通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次

原子数组AtomicIntegerArray

原子整数只对单个值的共享变量有用,但并不保证集合、数组内元素的线程安全,可以使用AtomicIntegerArray保证集合或数组内各元素线程安全

以下代码可以测试数组是否线程安全,该方法将启动多个线程对数组内各个元素进行自增操作

如数组内十个元素初始值为0,十个线程将数组内十个元素自增10000次,线程安全的结果应该为

[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]

public class TestAtomicArray {
    /*测试数组安全性的通用方法,采用函数式编程提供参数即可*/
    /**
     参数1,提供数组、可以是线程不安全数组或线程安全数组
     参数2,获取数组长度的方法
     参数3,自增方法,回传 array, index
     参数4,打印数组的方法
     */
    private static <T> void demo(
        Supplier<T> arraySupplier,
        Function<T, Integer> lengthFun,
        BiConsumer<T, Integer> putConsumer,
        Consumer<T> printConsumer)
    {
        List<Thread> ts = new ArrayList<>();
        T array = arraySupplier.get() ;
        Integer length = lengthFun.apply(array);

        for (int i = 0; i < length ; i++) {
            //每个线程对数组作10000次操作
            ts.add(new Thread(()->{
                for (int j = 0; j < 10000; j++) {
                    //取模是为了均摊在数组的每个元素上
                    putConsumer.accept(array,j%length);
                }
            }));
        }
        ts.forEach(t -> t.start()); // 启动所有线程
        ts.forEach(t -> {
                t.join();
        });
        printConsumer.accept(array);
    }

不安全验证

    public static void main(String[] args) {
        demo(
                ()->new int[10],
                (array)->array.length,
                (array,index) -> array[index]++
                array-> System.out.println(Arrays.toString(array))
        );
    }
}

结果:

[9224, 9254, 9278, 9262, 9248, 9252, 9278, 9280, 9233, 9293]

安全验证

        //安全的
        demo(
                ()->new AtomicIntegerArray(10),
                (array)->array.length(),
                (array,index)->array.getAndIncrement(index),
                array-> System.out.println(array)
        );

结果:

[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]

原子类常见操作

package com.Thread;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicMarkableReference;

public class TestAutomic {

    public static void main(String[] args) {

        AtomicInteger i = new AtomicInteger(0) ;

        // 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
        System.out.println(i.getAndIncrement());
        // 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
        System.out.println(i.incrementAndGet());

        /* 获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0)
         其中函数中的操作能保证原子,但函数需要无副作用*/
        System.out.println(i.getAndUpdate(p->p+2));

        /* 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0)
         其中函数中的操作能保证原子,但函数需要无副作用*/
        System.out.println(i.updateAndGet(p -> p + 2));

        /*获取并计算/计算并获取*/
        //p = i ; x = 10 ;
        System.out.println(i.getAndAccumulate(10, (p, x) -> p + x));
        //p = i ; x = 10 ;
        System.out.println(i.accumulateAndGet(10, (p, x) -> p + x));
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值