浅谈CAS机制

这里只是浅谈一下CAS机制,有机会的话后续会深入

文章目录

  • CAS
      • 机制
      • 为什么具有原子性
      • 缺点
      • ABA问题
      • ABA问题的解决方案


CAS

背景
尽管Java1.6为Synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低。这个时候我们想到了CAS

CAS 是 Compare and Swap的简写,意思在于先比较再交换。比较啥,交换啥,以及能做啥就是我们要了解的。

我们都知道
i++这样的操作并不是原子操作,在多线程的情况下会丢失很多值。

那么我们可以用synchronized锁住整个方法,或者用原子类例如AtomicInteger来进行操作。

比如

    
    private AtomicInteger count = new AtomicInteger(0);
    count.incrementAndGet();  //效果等于count+1


jdk在java.util.concurrent.atomic.包底下给我们提供了很多诸如此类的原子类

AtomicLong / AtomicBoolean / AtomicMarkableReference / AtomicStampedReference …

机制

CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

假设线程1准备将变量count的值从10,变到11。 而在这时有个线程2抢先将变量count变为11。 这个时候线程1继续往下走,Compare了内存地址中的值 11 和旧的预期值10 不一致,更新失败不会将旧值更新为要修改的新值11。 线程1会从头获取内存里count的值11,要修改的新值为12,假设这次并没有别的线程的干扰,那么线程1对比内存里的值和旧的预期值一致,将count由11修改为12。 这个线程1自发重试的行为称之为“自旋”。

注意: 比较内存中旧值和将新值写进内存,这个compareAndSwap的整个过程是原子操作,从而保证了CAS机制。

看个代码的例子


import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class CasTest {

    private AtomicInteger atomic = new AtomicInteger(0);
    private int i = 0;
    public static void main(String[] args) {
        final CasTest cas = new CasTest();
        List<Thread> ts = new ArrayList<Thread>(600);
        long start = System.currentTimeMillis();
        for (int j = 0; j < 100; j++) {
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        cas.count();
                        cas.safeCount();
                    }
                }
            });
            ts.add(t);
        }

        for (Thread t : ts) {
            t.start();
        }
        // 等待所有线程执行完成
        for (Thread t : ts) {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(cas.i);
        System.out.println(cas.atomic.get());
        System.out.printf("耗时%s ms\n", System.currentTimeMillis() - start);

    }

    /**
     * 使用CAS实现线程安全计数器
     */

    private void safeCount() {
        for (; ; ) {
            int i = atomic.get();
            boolean suc = atomic.compareAndSet(i, ++i);
            if (suc) {
                break;
            }
        }
    }

    /**
     * 非线程安全计数器
     */

    private void count() {
        i++;
    }

}

测试结果应该是

980212
1000000
耗时80 ms

重点理解一下safeCount方法里面的代码

为什么具有原子性

原子类的底层是由unsafe实现的

什么是unsafe呢?Java语言不像C,C++那样可以直接访问底层操作系统,但是JVM为我们提供了一个后门,这个后门就是unsafe。unsafe为我们提供了硬件级别的原子操作。

至于valueOffset对象,是通过unsafe.objectFieldOffset方法得到,所代表的是AtomicInteger对象value成员变量在内存中的偏移量。我们可以简单地把valueOffset理解为value变量的内存地址。

之前提到CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

而unsafe的方法参数包括了这三个基本元素:valueOffset参数代表了V,expect参数代表了A,update参数代表了B。

正是unsafe的compareAndSwapxxx方法保证了Compare和Swap操作之间的原子性操作。

缺点

  1. CPU开销较大。在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
  2. 不能保证代码块的原子性。(只能保证单次操作的原子性)
  3. ABA问题

ABA问题

假设小明银行账户有100块钱。 今天的某一时刻小明正准备去取50块钱,然而因为不可抗力(网络波动,误操作,系统BUG等等)同时有两条线程发起了这个取钱的行为。 同时小明的妈妈也在给小明汇了50块钱。 按理说,最后的结果应该是小明取出50块钱,账户余额为100才对。 我们看看会发生什么…

线程名称 账户余额 期望余额 执行状态
小明(取钱正常) 100 50 成功
小明(取钱异常) 100 50 BLOCKED

然后这时候余额是50。
小明妈汇钱进来了。。。

线程名称 账户余额 期望余额 执行状态
妈(打钱正常) 50 100 成功
小明(取钱异常) 50 50 BLOCKED

然后这时候余额是100。也正在这时,异常的线程从阻塞中恢复

| 线程名称 | 账户余额 | 期望余额| 执行状态 |
|—: | —|
|小明(取钱异常)| 100 | 50 | 成功|

最终小明取出50块钱,余额为50… 小明妈白打了50块钱。

正确的操作应该是异常线程醒来后发现虽然当前钱是100,但我不能继续进行-50的操作。因为你这个-50的操作是上个版本的事情,已经过去了,应该被忽略

有没有解决方案呢?


ABA问题的解决方案

加个版本号之类的东西。

当我们执行compare的时候,不仅仅要比较内存里的值是否和旧的预期值相同,还要看版本号,比如当前值已经进行了N次修改版本号为N,而你这个线程手里握的版本号还是1… 那么即便内存里的值是否和旧的预期值相同,也要放弃这次操作。

应用实例 AtomicStampedReference 等。

总结一下:

  1. Java语言CAS底层如何实现?
    利用unsafe提供了原子性操作方法。
  1. 什么是ABA问题?怎么解决?
    当一个值从A更新成B,又更新会A,普通CAS机制会误判通过检测。
    利用版本号比较可以有效解决ABA问题。

    1. 第一步: A1 -> B2 小明取钱 100变50

    2. 第二步: B2 -> A3 妈给小明打了50,钱又变成了100

    3. 在第一步产生的异常线程还是A1, 然而现在实际的值是A3。版本号对不上,于是不会再误操作变成B(不会误操作由100元再变成50元)

https://www.jianshu.com/p/736c532869a3

你可能感兴趣的:(多线程,面试)