【并发编程】Atomic与CAS

多线程开发中确保这三大特性。首先,最简单的方式就是使用 synchronized 关键字或者其它加锁。这种方式最大的好处是–简单!不需要动脑子,在需要的地方加锁就好了。同步方式在并发时包治百病,但治病的手段却是让多线程程序转为串行执行,这相当于自毁武功,浪费资源。如果滥用同步,那么程序就是去了多线程的意义。因此,只有在必要的时候才使用同步。比如对共享资源的访问。而且尽量控制同步代码块的范围,不需要使用同步的代码,尽量不要放入同步代码块。所以java还提供了轻量级的实现,来解决特定的问题,虽然没有普遍性,只是针对某些特定的问题提供的实现。但是这样已经能够解决问题,还能提高代码效率。

原子性的轻量级实现-Atomic

Atomic 相关类在 java.util.concurrent.atomic 包中。针对不同的原生类型及引用类型,有 AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference 等。另外还有数组对应类型 AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray。由于 Atomic 提供的功能类似,就不一个个过了。我们以 AtomicInteger 为例,看看 Atomic 类型变量所能提供的功能。

我们首先举个例子来验证AtomicInteger 的原子性
首先我们看没有加这个字段修饰的代码以及运行结果


public class T01_AtomicInterger {
    Integer count = new Integer(0);

     void m() {
        for (int i = 0; i < 10000; i++) {
            count++; //count++
        }
    }

    public static void main(String[] args) {
        T01_AtomicInterger t = new T01_AtomicInterger();

        List<Thread> threads = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(t::m, "thread-" + i));
        }

        threads.forEach((o) -> o.start());

        threads.forEach((o) -> {
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        System.out.println(t.count);
    }

}

这段代码时启动了十个线程,每个线程执行 10000次对count的加一操作,最后的结果理论上是100000。但是结果却不是

 运算逻辑是对变量 count 的累加。假如 count 为 int 类型,多个线程并发时,可能各自读取到了同样的值,也可能 A 线程读到 2,但由于某种原因更新晚了,count 已经被其它线程更新为了 4,但是线程 A 还是继续执行了 count+1 的操作,count 反而被更新为更小的值 3。现在的多线程程序是不安全的。故会造成上述的结果,其实我们有最简单的解决办法加锁,就是让加一操作由异步变成同步就行,我们看下边的代码:

    synchronized  void m() {
        for (int i = 0; i < 10000; i++) {
            count++; //count++
        }
    }

最后的运算结果如下:


 确实得到 了正确的答案,但是我上次说过了,加锁是需要开销的。所以java提供了更加轻量级的实现--AtomicInteger

我么需要将count用 AtomicInteger修饰,加一的操作不需要用synchronized  修饰了,直接使用Atomic的incrementAndGet方法,也就是加一操作,这样就实现了原子性操作。

    AtomicInteger count = new AtomicInteger(0);

    /*synchronized */ void m() {
        for (int i = 0; i < 10000; i++) {
            count.incrementAndGet(); //count++
        }
    }

运算结果如下;

也是可以实现加一操作的,而且保证了安全性,并且没有加锁,提高了代码的运行效率。其实count=count+1 这行语句其实隐含了两步操作,第一步取得 count 的值,第二步为 count 加 1 。而在这两步操作中间,count 的值可能已经改变了。而 AtomicInteger 提供的 incrementAndGet () 方法,则把这两步操作作为一个原子性操作来完成,则不会出现线程安全问题。

而Atomic如何实现了原子性呢?其实使用了CAS算法。

CAS 算法

CAS 是 Compare and swap 的缩写,翻译过来就是比较替换。其实 CAS 是乐观锁的一种实现。而 Synchronized 则是悲观锁。这里的乐观和悲观指的是当前线程对是否有并发的判断。

悲观锁–认为每一次自己的操作大概率会有其它线程在并发,所以自己在操作前都要对资源进行锁定,这种锁定是排他的。悲观锁的缺点是不但把多线程并行转化为了串行,而且加锁和释放锁都会有额外的开支。

乐观锁–认为每一次操作时大概率不会有其它线程并发,所以操作时并不加锁,而是在对数据操作时比较数据的版本,和自己更新前取得的版本一致才进行更新。乐观锁省掉了加锁、释放锁的资源消耗,而且在并发量并不是很大的时候,很少会发生版本不一致的情况,此时乐观锁效率会更高。

Atomic 变量在做原子性操作时,会从内存中取得要被更新的变量值,并且和你期望的值进行比较,期望的值则是你要更新操作的值。如果两个值相等,那么说明没有其它线程对其更新,本线程可以继续执行。如果不等,说明有线程已经先于此线程进行了更新操作。那么则继续取得该变量的最新值,重复之前的逻辑,直至操作成功。这保证了每个线程对 Atomic 变量操作是线程安全的。

CAS缺点 : ABA 问题

假如本线程更新前取得期望值为 A,和更新操作之间的这段时间内,其它线程可能把 value 改为了 B 又改回了 A。 而本线程更新时发现 value 和期望值一样还是 A,认为其没有变化,则执行了更新操作。但其实此时的 A 已经不是彼时的 A 了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值