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

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

注:【问题提出】的乐观锁方案看不懂没关系,这正是本文要讨论的内容

文章目录

  • Java多线程共享模型之乐观锁(CAS与Atomic原子类)
    • 问题提出
    • CAS分析
      • 为什么无锁(CAS)效率高
      • CAS特点
    • JUC_Atomic原子类
      • ABA问题
      • ABA解决方案-AtomicStampedReference
      • 原子数组AtomicIntegerArray
      • 字段更新器AtomicIntegerFieldUpdater(了解)
      • 原子类常见操作(了解)

问题提出

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

账户接口

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->{
            try {
                t.join(); //同步下面的打印语句
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(account.getBalance());
    }
}

线程不安全的实现

 public  class TestAtomicRef implements DecimalAccount {

    BigDecimal balance ;

    public TestAtomicRef(BigDecimal balance) {
        this.balance = balance; 
    }

    @Override
    public BigDecimal getBalance() {
        return balance ;
    }

     /*不安全实现*/
        @Override
        public void withdraw(BigDecimal amount) {
        BigDecimal balance = this.getBalance() ;
        this.balance = balance.subtract(amount) ;
    }
}

//调用函数
 class Test{
     public static void main(String[] args) {
         TestAtomicRef ref = new TestAtomicRef(new BigDecimal(10000))  ;

         DecimalAccount.demo(ref);
     }

}

预期结果为0,产生结果大概率不为0,故线程不安全

解决方案一:加锁

 //重量锁
    private final Object lock = new Object();

/*安全实现 - 重量级锁*/
     @Override
     public void withdraw(BigDecimal amount) {

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

保证取款操作完全同步

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

  //CAS
     AtomicReference<BigDecimal> ref ;
     
         public TestAtomicRef(BigDecimal balance) {
        ref = new AtomicReference<>(balance)  ;
    }
     
         @Override
    public BigDecimal getBalance() {
        return  ref.get() ;
    }

     /*安全实现 - CAS*/
     @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 的说法),它必须是原子操作

Java多线程共享模型之乐观锁(CAS与Atomic原子类)_第1张图片

注意:

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

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

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

《计组-总线嗅探机制》

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

–valatile相关内容参见:

[Java线程]volatile关键字小结

Valatile原理-内存屏障

为什么无锁(CAS)效率高

  • 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇(while),而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。

打个比喻

  • 线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速… 恢复到高速运行,代价比较大

  • 但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换

CAS特点

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

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

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

  • CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思

  • 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一

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

JUC_Atomic原子类

ABA问题

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

package com.Thread;

import java.util.concurrent.atomic.AtomicReference;

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() ;
        AtoBtoA();
        Thread.sleep(1000);
        //change ->C

        boolean c = ref.compareAndSet(prev, "C");
        System.out.println("change A->C"+c);
    }

    private static void AtoBtoA() throws InterruptedException {

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

        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

ABA解决方案-AtomicStampedReference

主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又 改回 A 的情况,如果主线程

希望:

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

AtomicStampedReference

package com.Thread;

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

public class SolveABA {
    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 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如: A -> B -> A -> C ,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次

原子数组AtomicIntegerArray

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

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

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

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

package com.Thread;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

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++) {
            //每个线程对数组作1000次操作
            ts.add(new Thread(()->{
                for (int j = 0; j < 10000; j++) {
                    putConsumer.accept(array,j%length);//取模是为了均摊在数组的每个元素上
                }
            }));
        }
        ts.forEach(t -> t.start()); // 启动所有线程
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }); // 等所有线程结束
        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]

字段更新器AtomicIntegerFieldUpdater(了解)

该更新器可对类的属性做出安城安全的操作

package com.Thread;

import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

public class TestFieldUpdater {

    private volatile int field ;

    public static void main(String[] args) {
    AtomicIntegerFieldUpdater fieldUpdater
    = AtomicIntegerFieldUpdater.newUpdater(TestFieldUpdater.class,"field") ;

    TestFieldUpdater updater = new TestFieldUpdater();
    fieldUpdater.compareAndSet(updater,0,10);

        // 修改成功 field = 10
        System.out.println(updater.field);
        // 修改成功 field = 20
        fieldUpdater.compareAndSet(updater,10,20);
        System.out.println(updater.field);
        // 修改成功 field = 20
        fieldUpdater.compareAndSet(updater,10,30);
        System.out.println(updater.field);

    }
}

原子类常见操作(了解)

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));
    }
}

你可能感兴趣的:(Java并发编程,java,多线程,并发编程,CAS,Atomic原子类)