乐观锁,CAS算法

一、CAS的定义

一个线程失败或挂起并不会导致其他线程也失败或挂起,那么这种算法就被称为非阻塞算法。而CAS就是一种非阻塞算法实现,也是一种乐观锁技术,它能在不使用锁的情况下实现多线程安全,所以CAS也是一种无锁算法。

CAS [公式] 比较并交换,是一种实现并发算法时常用到的技术,Java并发包中的很多类都使用了CAS技术。CAS具体包括三个参数:当前内存值V、旧的预期值A即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。CAS 有效地说明了“ 我认为位置 V 应该包含值 A,如果真的包含A值,则将 B 放到这个位置,否则,不要更改该位置,只告诉我这个位置现在的值(A)即可。 ”整个比较并交换的操作是原子操作

ABA问题:
ABA问题是CAS中的一个漏洞。CAS的定义,当且仅当内存值V等于就得预期值A时,CAS才会通过原子方式用新值B来更新V的值,否则不会执行任何操作。那么如果先将预期值A给成B,再改回A,那CAS操作就会误认为A的值从来没有被改变过,这时其他线程的CAS操作仍然能够成功,但是很明显是个漏洞,因为预期值A的值变化过了。如何解决这个异常现象?java并发包为了解决这个漏洞,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性,即在变量前面添加版本号,每次变量更新的时候都把版本号+1,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。

二、CAS算法解决的问题

使用CAS就可以不用加锁来实现线程安全。

原子更新的基本类型包括:

  • AtomicBoolean:原子更新布尔变量;
  • AtomicInteger:原子更新整型变量;
  • AtomicLong:原子更新长整型变量;

以AtomicInteger为例

 public class AtomicInteger extends Number implements java.io.Serializable {
     //返回当前的值
     public final int get() {
         return value;
     }
     //原子更新为新值并返回旧值
     public final int getAndSet(int newValue) {
         return unsafe.getAndSetInt(this, valueOffset, newValue);
     }
     //最终会设置成新值
     public final void lazySet(int newValue) {
         unsafe.putOrderedInt(this, valueOffset, newValue);
     }
     //如果输入的值等于预期值,则以原子方式更新为新值
     public final boolean compareAndSet(int expect, int update) {
         return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
     }
     //原子自增
     public final int getAndIncrement() {
         return unsafe.getAndAddInt(this, valueOffset, 1);
     }
     //原子方式将当前值与输入值相加并返回结果
     public final int getAndAdd(int delta) {
         return unsafe.getAndAddInt(this, valueOffset, delta);
     }
 }

为了说明AtomicInteger的原子性,这里代码演示多线程对一个int值进行自增操作,最后输出结果,代码如下:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerDemo {

    private static AtomicInteger atomicInteger = new AtomicInteger(0);

    public static void main(String[] args){
        for (int i = 0; i < 5; i++){
            new Thread(new Runnable() {
                public void run() {
                    //调用AtomicInteger的getAndIncement返回的是增加之前的值
                     System.out.println(atomicInteger.getAndIncrement());
                }
            }).start();
        }
        System.out.println(atomicInteger.get());
    }
}

输出:

    0
    1
    2
    3
    4
    5
    5

可见,在多线程的情况下,得到的结果是正确的,但是如果仅仅使用int类型的成员变量则可能得到不同的结果。这里的关键在于getAndIncrement是原子操作,那么是如何保证的呢?看源代码:

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

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = this.getIntVolatile(o, offset);
    } while(!this.compareAndSwapInt(o, offset, v, v + delta));
    return v;
}

public final native boolean compareAndSwapInt(Object o, long offset, int delta, int v);

通过方法调用,我们可以发现,getAndIncrement方法调用getAndAddInt方法,最后调用的是compareAndSwapInt方法,即本文的主角CAS。

getAndAddInt方法解析:拿到内存位置的最新值v,使用CAS尝试将内存位置的值v修改为目标值v+delta,如果修改失败,则获取该内存位置的新值v,然后继续尝试,直至修改成功,如果这是CAS一直失败,多次尝试后多没有成功,就会给CPU带来很大的开销。

扩:原子变量类
原子变量类(例如java并发包原子操作类 [公式] 中的 AtomicXXX)就使用了这种高效的CAS操作。

三、CAS的缺点:

CAS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题。

【1】循环时间长、开销很大。

当某一方法比如:getAndAddInt执行时,如果CAS失败,会一直进行尝试。如果CAS长时间尝试但是一直不成功,可能会给CPU带来很大的开销。

【2】只能保证一个共享变量的原子操作。

当操作1个共享变量时,我们可以使用循环CAS的方式来保证原子操作,但是操作多个共享变量时,循环CAS就无法保证操作的原子性,这个时候就需要用锁来保证原子性。

【3】存在ABA问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值