五、JAVA多线程夯实基础--C A S(Compare and swap)

1.C A S基本概念

C A S(Compare and swap)或者C A S(Compare and set或者比较交换,是一种无锁原子算法,映射到操作系统就是一条cmpxchg硬件汇编指令(保证原子性),其作用是让C P U将内存值更新为新值,但是有个条件,内存值必须与期望值相同,并且C A S操作无需用户态与内核态切换直接在用户态对内存进行读写操作意味着不会阻塞/线程上下文切换。

一次C A S的操作:包含3个参数C A S(V,E,N)V表示待更新的内存值,E表示预期值,N表示新值,当 V值等于E值时,才会将V值更新成N值,如果V值和E值不等,不做更新。

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

CAS 是基于乐观锁的思想:

最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试。

synchronized 是基于悲观锁的思想:

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

CAS 体现的是无锁并发、无阻塞并发, 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

C A S需要额外给出一个期望值,也就是你认为这个变量现在应该是什么样子的,如果变量不是你想象的那样,说明它已经被别人修改过了,你只需要重新读取,设置新期望值,再次尝试修改就好了。

2.C A S保证原子性

为了保证C A S的原子性,C P U提供了下面两种方式

  • 总线锁定

  • 缓存锁定

2.1总线锁定

总线(B U S)是计算机组件间的传输数据方式,也就是说C P U与其他组件连接传输数据,就是靠总线完成的,比如C P U对内存的读写。

总线锁定:是指C P U使用了总线锁,所谓总线锁就是使用C P U提供的LOCK#信号,当C P U在总线上输出LOCK#信号时,其他C P U的总线请求将被阻塞。

 

2.2缓存锁定 

总线锁定方式虽然保证了原子性,但是在锁定期间,会导致大量阻塞,增加系统的性能开销,所以现代C P U为了提升性能,通过锁定范围缩小的思想设计出了缓存行锁定(缓存行是C P U高速缓存存储的最小单位)。所谓缓存锁定是指C P U缓存行进行锁定,当缓存行中的共享变量回写到内存时,其他C P U会通过总线嗅探机制感知该共享变量是否发生变化,如果发生变化,让自己对应的共享变量缓存行失效,重新从内存读取最新的数据,缓存锁定是基于缓存一致性机制来实现的,因为缓存一致性机制会阻止两个以上C P U同时修改同一个共享变量(现代C P U基本都支持和使用缓存锁定机制)。(在六、Java内存模型也有详细的介绍)

3.C A S的缺点

C A S和锁都解决了原子性问题,和锁相比没有阻塞、线程上下文切换、死锁,所以C A S要比锁拥有更优越的性能,但是C A S同样存在缺点。

C A S的问题如下

  • 只能保证一个共享变量的原子操作(由cpu缓存一致性机制决定的)

  • 自旋时间太长(建立在自旋锁的基础上)

  • ABA问题

3.1只能保证一个共享变量的原子性操作

C A S只能针对一个共享变量使用,如果多个共享变量就只能使用锁了。

3.2自时间长

当一个线程获取锁时失败,不进行阻塞挂起,而是间隔一段时间再次尝试获取,直到成功为止,这种循环获取的机制被称为自旋锁(spinlock)。

自旋锁好处:持有锁的线程在短时间内释放锁,那些等待竞争锁的线程就不需进入阻塞状态(无需线程上下文切换/无需用户态与内核态切换),它们只需要等一等(自旋),等到持有锁的线程释放锁之后即可获取,这样就避免了用户态和内核态的切换消耗。

自旋锁缺点:线程在长时间内持有锁,等待竞争锁的线程一直自旋,CPU一直空转,资源浪费在毫无意义的地方,所以一般会限制自旋次数。

3.3ABA问题

ABA:C A S需要检查待更新的内存值有没有被修改,如果没有则更新,但是存在这样一种情况,如果一个值原来 是 A,变成了B,然后又变成了A,在C A S检查的时候会发现没有被修改。

假设有两个线程,线程1读取到内存值A,线程1时间片用完,切换到线程2,线程2也读取到了内存值A,并把它修改为B值,然后再把B值还原到A值,简单说,修改次序是A->B->A,接着线程1恢复运行,它发现内存值还是A,然后执行C A S操作,这就是著名的ABA问题,但是好像又看不出什么问题。

只是简单的数据结构,确实不会有什么问题,如果是复杂的数据结构可能就会有问题了(使用AtomicReference可以把C A S使用在对象上),

以链表数据结构为例,两个线程通过C A S去删除头节点,假设现在链表有A->B节点

  • 线程1删除A节点,B节点成为头节点,正要执行C A S(A,A,B)时,时间片用完,切换到线程2

  • 线程2删除A、B节点

  • 线程2加入C、A节点,链表节点变成A->C

  • 线程1重新获取时间片,执行C A S(A,A,B)

  • 丢失C节点

要解决A B A问题只要追加版本号即可,每次改变时加1,即A —> B —> A,变成1A —> 2B —> 3A,在Java中提供了AtomicStampedRdference可以实现这个方案。

4.Java中提供的J.U.C 并发包。

import java.util.concurrent.atomic

4.1原子整数

public class Test34 {
    public static void main(String[] args) {

        AtomicInteger i = new AtomicInteger(5);

        /*System.out.println(i.incrementAndGet()); // ++i   1
        System.out.println(i.getAndIncrement()); // i++   2

        System.out.println(i.getAndAdd(5)); // 2 , 7
        System.out.println(i.addAndGet(5)); // 12, 12*/

        //             读取到    设置值
//        i.updateAndGet(value -> value * 10);

        System.out.println(updateAndGet(i, p -> p / 2));

//        i.getAndUpdate()
        System.out.println(i.get());
    }

    public static int updateAndGet(AtomicInteger i, IntUnaryOperator operator) {
        while (true) {
            int prev = i.get();
            int next = operator.applyAsInt(prev);
            if (i.compareAndSet(prev, next)) {
                return next;
            }
        }
    }
}

4.2原子引用

package cn.itcast.test;

import lombok.extern.slf4j.Slf4j;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

@Slf4j(topic = "c.Test35")
public class Test35 {
    public static void main(String[] args) {
        DecimalAccount.demo(new DecimalAccountCas(new BigDecimal("10000")));
    }
}

class DecimalAccountCas implements DecimalAccount {
    private AtomicReference<BigDecimal> balance;

    public DecimalAccountCas(BigDecimal balance) {
//        this.balance = balance;
        this.balance = new AtomicReference<>(balance);
    }

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

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

interface DecimalAccount {
    // 获取余额
    BigDecimal getBalance();

    // 取款
    void withdraw(BigDecimal amount);

    /**
     * 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
     * 如果初始余额为 10000 那么正确的结果应当是 0
     */
    static void demo(DecimalAccount account) {
        List<Thread> ts = new ArrayList<>();
        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());
    }
}

4.2.1 AtomicStampedReference 解决ABA问题

4.3原子数组

package cn.itcast.test;

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 Test39 {

    public static void main(String[] args) {
    //不安全的数组
        demo(
                () -> new int[10],
                (array) -> array.length,
                (array, index) -> array[index]++,
                array -> System.out.println(Arrays.toString(array)));
    //安全的数组
        demo(
                () -> new AtomicIntegerArray(10),
                (array) -> array.length(),
                (array, index) -> array.getAndIncrement(index),
                array -> System.out.println(array));
    }

    /**
     * 参数1,提供数组、可以是线程不安全数组或线程安全数组
     * 参数2,获取数组长度的方法
     * 参数3,自增方法,回传 array, index
     * 参数4,打印数组的方法
     */
     <<Supplier、Consumer、BiConsumer是Java8中的函数式接口>>
    // supplier 提供者 无中生有 ()->结果
    // function 函数 一个参数一个结果 (参数)->结果 , BiFunction (参数1,参数2)->结果
    // consumer 消费者 一个参数没结果 (参数)->void, BiConsumer (参数1,参数2)->
    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();
        int 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 -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }); // 等所有线程结束
        printConsumer.accept(array);
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值