深刻浅出 Java Concurrency (5): 原子操做 part 4 CAS操做

 

在JDK 5以前Java语言是靠synchronized关键字保证同步的,这会致使有锁(后面的章节还会谈到锁)。html

锁机制存在如下问题:java

(1)在多线程竞争下,加锁、释放锁会致使比较多的上下文切换和调度延时,引发性能问题。算法

(2)一个线程持有锁会致使其它全部须要此锁的线程挂起。数据结构

(3)若是一个优先级高的线程等待一个优先级低的线程释放锁会致使优先级倒置,引发性能风险。多线程

 

volatile是不错的机制,可是volatile不能保证原子性。所以对于同步最终仍是要回到锁机制上来。post

独占锁是一种悲观锁,synchronized就是一种独占锁,会致使其它全部须要锁的线程挂起,等待持有锁的线程释放锁。而另外一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操做,若是由于冲突失败就重试,直到成功为止。性能

CAS 操做this

上面的乐观锁用到的机制就是CAS,Compare and Swap。spa

CAS有3个操做数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改成B,不然什么都不作。.net

非阻塞算法 (nonblocking algorithms)

一个线程的失败或者挂起不该该影响其余线程的失败或挂起的算法。

现代的CPU提供了特殊的指令,能够自动更新共享数据,并且可以检测到其余线程的干扰,而 compareAndSet() 就用这些代替了锁定。

拿出AtomicInteger来研究在没有锁的状况下是如何作到数据正确性的。

private volatile int value;

首先毫无觉得,在没有锁的机制下可能须要借助volatile原语,保证线程间的数据是可见的(共享的)。

这样才获取变量的值的时候才能直接读取。

public final int get() {
        return value;
    }

而后来看看++i是怎么作到的。

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

在这里采用了CAS操做,每次从内存中读取数据而后将此数据和+1后的结果进行CAS操做,若是成功就返回结果,不然重试直到成功为止。

compareAndSet利用JNI来完成CPU指令的操做。

public final boolean compareAndSet(int expect, int update) {   
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

总体的过程就是这样子的,利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操做都是利用相似的特性完成的。

而整个J.U.C都是创建在CAS之上的所以对于synchronized阻塞算法,J.U.C在性能上有了很大的提高。参考资料的文章中介绍了若是利用CAS构建非阻塞计数器、队列等数据结构。

CAS看起来很爽,可是会致使“ABA问题”。

CAS算法实现一个重要前提须要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会致使数据的变化。

好比说一个线程one从内存位置V中取出A,这时候另外一个线程two也从内存中取出A,而且two进行了一些操做变成了B,而后two又将V位置的数据变成A,这时候线程one进行CAS操做发现内存中仍然是A,而后one操做成功。尽管线程one的CAS操做成功,可是不表明这个过程就是没有问题的。若是链表的头在变化了两次后恢复了原值,可是不表明链表就没有变化。所以前面提到的原子操做AtomicStampedReference/AtomicMarkableReference就颇有用了。这容许一对变化的元素进行原子操做。

 

 

参考资料:

(1)非阻塞算法简介

(2)流行的原子

相关文章
相关标签/搜索