CAS 无锁式同步机制

计算机系统中,CPU 和内存之间是经过总线进行通讯的,当某个线程占有 CPU 执行指令的时候,会尽量的将一些须要从内存中访问的变量缓存在本身的高速缓存区中,而修改也不会当即映射到内存。java

而此时,其余线程将看不到内存中该变量的任何改动,这就是咱们说的内存可见性问题。连续的文章中,咱们总共提出了两种解决办法。git

其一是使用关键字 volatile 修饰共享的全局变量,而 volatile 的实现原理大体分两个步骤,任何对于该变量的修改操做都会由虚拟机追加一条指令立马将该变量所在缓存区中的值回写内存,接着将失效该变量在其余 CPU 缓存区的引用。也就意味着,其余 CPU 若是再想要使用该变量,缓存中是没有的,进而逼迫去访问内存拿最新的数据。github

其二是使用关键字 synchronized 并借助对象内置锁实现数据一致性,主要思路是,若是一个线程由于竞争某个锁失败而被阻塞了,那么它就认为别的线程正在工做,极可能会改了某些共享变量的数据,进而在得到锁后第一时间从新刷内存中的数据,同时一个线程走出同步代码块以前会同步数据到内存。算法

其实咱们也不多会使用第二种方法来解决内存可见性问题,着实有点大材小用的感受,使用 volatile 关键字算是一个比较经常使用的方式。可是 volatile 是有特定的适用场景的,也具备它的局限性,咱们一块儿来看。数组

volatile 的局限性

废话很少说,先看一段代码:缓存

public class MainTest {
    private static volatile int count;

    @Test
    public void testVolatile() throws InterruptedException {
        Thread1[] thread1s = new Thread1[100];
        for (int i = 0; i < 100; i++){
            thread1s[i] = new Thread1();
            thread1s[i].start();
        }

        for (int j = 0; j < 100; j++){
            thread1s[j].join();
        }
        System.out.println(count);
    }
    //每一个线程随机自增 count
    private class Thread1 extends Thread{
        @Override
        public void run(){
            try {
                Thread.sleep((long) (Math.random() * 500));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count++;
        }
    }
}
复制代码

咱们将变量 count 使用 volatile 进行修饰,而后建立一百个线程并启动,按照咱们以前的理解,变量 count 的值一旦被修改就能够被其余线程立马看到,不会缓存在本身的工做内存。可是结果却不是这样。安全

屡次运行,结果不尽相同bash

94微信

96并发

98

....

其实缘由很简单,咱们只说过 volatile 会在变量值被修改后回写内存并失效其余 CPU 缓存中该变量的引用迫使其余线程从主存中从新去获取该变量的值。

可是 count++ 这个操做并非原子操做,以前咱们说过这一点,这个操做会使得 CPU 作如下几件事情:

  • 从 CPU 缓存读出变量的值放入寄存器 A 中
  • 为 count 加一并将值保存在另外一个寄存器 B 中
  • 将寄存器 B 中的数据写到缓存并经过缓存锁回写内存

而若是第一步刚执行结束,或第二步刚执行结束,但没有执行第三步的时候,其余的某个线程更改了该变量的值并失效了当前 CPU 中缓存中该变量的引用,那么第三步会因为缓存失效而先去内存中读一个值过来,而后用寄存器 B 中的值覆盖缓存并刷到内存中。

这就意味着,在此以前其余线程的修改被覆盖,进而咱们得不到咱们预期的结果。结论就是,volatile 关键字具备可见性而不具备原子性。

原子类型变量

JDK1.5 之后由 Doug Lea 大神设计的 java.util.concurrent.atomic 包中包含了原子类型相关的全部类。

image

其中,

  • AtomicBoolean:对应的 Boolean 类型的原子类型
  • AtomicInteger:对应的 Integer 类型的原子类型
  • AtomicLong:相似
  • AtomicIntegerArray:对应的数组类型
  • AtomicLongArray:相似
  • AtomicReference:对应的引用类型的原子类型
  • AtomicIntegerFieldUpdater:字段更新类型

剩余的几个类的做用,咱们稍后再详细介绍。

针对基本类型所对应的原子类型,咱们以 AtomicInteger 这个类为例,看看它的源码实现状况。

AtomicInteger 相关实现

image

内部定义了一个 int 类型的变量 value,而且 value 修饰为 volatile,表示 value 这个字段值的任何修改都对其余线程当即可见。

而构造函数容许你传入一个初始的 value 数值,不传的话就会致使 value 的值为零。

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
复制代码

这个方法就是原子的「i++」操做,咱们跟进去看:

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}
复制代码

几个参数简单说一下,var1 是咱们的 AtomicInteger 实例引用,var2 是一个字段偏移量,经过它咱们能够定位到其中的 value 字段。var4 这里固定为一。

代码的逻辑也是简单的,取出内部 value 字段的值并暂存在变量 value5 中,而后再次判断,若是 value 字段的值依然等于 value5,那么将原子操做式将 value 修改成 value4 + value5,本质上就是加一。

不然,说明在当前线程上次访问后,又有其余线程修改了这个 value 字段的值,因而咱们从新获取这个字段的值,直到没有人修改成止并自增它。

这个 compareAndSwapInt 方法咱们通常把它叫作『CAS』,底层有系统指令作支撑,是一个比较并修改的原子指令,若是值等于 A 则将它修改成 B,不然返回。

AtomicInteger 中的其他方法大体相似,都是依赖这个『CAS』方法实现的。

  • int getAndAdd(int delta):自增 delta 并获取修改以前的值
  • int incrementAndGet():自增并获取修改后的值
  • int decrementAndGet():自减并获取修改后的值
  • int addAndGet(int delta):自增 delta 并获取修改后的值

基于这一点,咱们重构上述的线程不安全的 demo:

//构建一个原子类型变量 aCount
private static volatile AtomicInteger aCount = new AtomicInteger(0);
@Test
public void testAtomic() throws InterruptedException {
    Thread2[] threads = new Thread2[100];
    for (int i = 0; i < 100; i++){
        threads[i] = new Thread2();
        threads[i].start();
    }
    for (int i = 0; i < 100; i++){
        threads[i].join();
    }
    System.out.println(aCount.get());
}

private class Thread2 extends Thread{
    @Override
    public void run(){
        try {
            Thread.sleep((long) (500 * Math.random()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //原子自增
        aCount.getAndIncrement();
    }
}
复制代码

修改后的代码不管运行多少次,总会获得结果 100 。有关 AtomicLong、AtomicReference 的相关内容大体相似,都是依赖咱们这个『CAS』方法,这里再也不赘述。

FieldUpdater 是基于反射来原子修改变量的值,这里很少说了,下面咱们看看『CAS』的一些问题。

CAS 的局限性

ABA 问题

CAS 有一个典型问题就是「ABA 问题」,咱们知道 CAS 工做的基本原理是,先读取目标变量的值,而后调用原子指令判断该值是否等于咱们指望的值,若是等于就认为没有被别人改过,不然视做数据脏了,从新去读变量的值。

可是问题是,若是变量 a 的值为 100,咱们的 CAS 方法也读到了 100,接着来了一个线程将这个变量改成 999,以后又来一个线程再改了一下,改为 100 。而轮到咱们的主线程发现 a 的值依然是 100,它视做没有人和它竞争修改 a 变量,因而修改 a 的值。

这种状况,虽然 CAS 会更新成功,可是会存在潜在的问题,中途加入的线程的操做对于后一个线程根本是不可见的。而通常的解决办法是为每一次操做加上加时间戳,CAS 不只关注变量的原始值,还关注上一次修改时间。

循环时间长开销大

咱们的 CAS 方法通常都定义在一个循环里面,直到修改为功才会退出循环,若是在某些并发量较大的状况下,变量的值始终被别的线程修改,本线程始终在循环里作判断比较旧值,效率低下。

因此说,CAS 适用于并发量不是很高的状况下,效率远远高于锁机制。

只能保证一个变量的原子操做

CAS 只能对一个变量进行原子性操做,而锁机制则不一样,得到锁以后,就能够对全部的共享变量进行修改而不会发生任何问题,由于别人没有锁不能修改这些共享变量。

总结一下,锁实际上是一种悲观的思想,「我认为全部人都会和我来竞争某些资源的使用,因此我获得资源以后把它锁上,用完再释放掉锁」,而 CAS 则是一种乐观的思想,「我觉得只有我一我的在使用这些资源,假若有人也在使用,那我再次尝试便可」。

CAS 是之后的各类并发容器的实现基石,是一种乐观的、非阻塞式的算法,将有助于提高咱们的并发性能。


文章中的全部代码、图片、文件都云存储在个人 GitHub 上:

(github.com/SingleYam/o…)

欢迎关注微信公众号:OneJavaCoder,全部文章都将同步在公众号上。

image
相关文章
相关标签/搜索