java学习记录---CAS乐观锁

CAS,全称 Compare And Swap (比较与交换),是一种乐观锁,一样是锁相比 synchronized 性能却要高很多,由于是 synchronized 阻塞的,而CAS是非阻塞的。CAS主要有3个操做数,内存值V,预期的旧值A,须要修改的新值B,能够这样理解:在V的地址把A改为B,当V位置的值与预期的旧值A相同时候,则修改为B,不然不更新。java

下面看个图简单理解一下CAS:当线程1和线程2同时操做内存V,线程1想要把内存V的变量值从A(2)改为B(1)而线程2想要把V的变量值从A(2)改为B(3)。假设这个时候是线程1优先抢到资源因此线程1先进行CAS操做,这个时候预期旧值2是相等的则执行了更新,更新完后内存V的变量值就变成1,这个时候线程2才进入比较预期的A值与V中实际的变量值已经不相同了,因此更新失败。ide

这个图看上去是Compare And Swap 是同时操做,但其实是分2部执行:1.比较(compare),2.交换(swap),它的原子性是经过硬件实现的,而不是咱们java代码实现函数

java提供的CAS操做类性能

咱们随便找其中一个Atomic类学习学习

当V的值与A相等则更新成功测试

public static void main(String[] args) {
    // 定义IntegerCAS    AtomicInteger AI = new AtomicInteger();
    // 设置初始化值
    AI.set(1);
    //  1 替换成 2 并返回是否替换成功,该函数第一个参数为预期的旧值(A),第二个参数是须要修改的值(B)
    boolean b = AI.compareAndSet(1, 2);
    // 打印是否替换成功
    System.out.println(b);
    // 打印最新值
    System.out.println(AI);
}

打印结果:this

当V的值与A不相等则更新失败spa

public static void main(String[] args) {
    // 定义IntegerCAS    AtomicInteger AI = new AtomicInteger();
    // 设置初始化值
    AI.set(1);
    //  1 替换成 2 并返回是否替换成功,该函数第一个参数为预期的旧值(A),第二个参数是须要修改的值(B)
    boolean b = AI.compareAndSet(2, 2);
    // 打印是否替换成功
    System.out.println(b);
    // 打印最新值
    System.out.println(AI);
}

打印结果:线程

这2个打印结果的结论也证明了最开始那个图的原理,只有V的变量值与A相同时候,才会修改为B ,3d

接下来看看原子性

普通的int类型

public class AmIntegerTest implements Runnable {

    private static volatile int I = 0;

    public void run() {
        // 每一个线程自增100000次,
        for (int i = 0; i++ < 100000; add()) {}
        // 线程执行完以后结果
        System.out.println(Thread.currentThread().getName() + ":" + I);
    }

    public static void add(){
        I++;
    }

    public static void main(String[] args) {
        AmIntegerTest ait = new AmIntegerTest();
        Thread t1 = new Thread(ait);
        Thread t2 = new Thread(ait);
        t1.start();
        t2.start();
    }
}

打印结果显示,普通的int没有原子性

除非加上synchronized 关键字,接下来改造add()添加synchronized其余代码不变

public synchronized static void add(){
    I++;
}

不管执行多少次最终结果都是200000,能够保证原子性

 

咱们再看看java提供的CAS操做类

public class AmIntegerTest implements Runnable {

    // 定义IntegerCAS    private static AtomicInteger AI = new AtomicInteger();

    public void run() {
        // 每一个线程自增100000次,
        for (int i = 0;i++ < 100000; AI.incrementAndGet()) {}
        // 线程执行完以后结果
        System.out.println(Thread.currentThread().getName() + ":" + AI.get());
    }

    public static void main(String[] args) {
        AmIntegerTest ait = new AmIntegerTest();
        Thread t1 = new Thread(ait);
        Thread t2 = new Thread(ait);
        t1.start();
        t2.start();
    }
}

不管执行多少次最终结果都是200000,因此是具备原子性的,可是它的原子性是其余语言实现的,这里就不讨论它的实现原理了

AtomicInteger.value是一个volatile 修饰的变量(内存锁定,同一时刻只有一个线程能够修改内存值)

private volatile int value;

AtomicInteger.incrementAndGet()这是一个自增函数,实现了自旋锁(无限循环),下面看看incrementAndGet()的源代码,它调用了Unsafe 类的getAndAddInt()。而getAndAddInt()是无限循环,直到值修改为功才结束,不然一直循环

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
private static final Unsafe unsafe = Unsafe.getUnsafe();
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;
}

getAndAddInt()实现的自旋锁原理就是内存V的变量值与A不一致时候,再从新获取V的变量值,直到V的变量值与A一致时候,才更新成B并结束,这样有个缺点就是若是自旋次数太多,会形成很大的资源消耗

在Atomic包中的CAS操做都是基于如下3个方法实现,Unsafe类里面的全部方法都是 native 声明的,说明是调用其余语言实现的

//第一个参数o为给定对象,offset为对象内存的偏移量,经过这个偏移量迅速定位字段并设置或获取该字段的值,
//expected表示指望值,x表示要设置的值,下面3个方法都经过CAS原子指令执行操做。
public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);

public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);

public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);

咱们试试使用一下这些函数

public class CasTest {

    // 定义java CASUnsafe
    public static Unsafe U = getUnsafe();

    public static void main(String[] args) throws NoSuchFieldException {
        // java Unsafe类调用C++ 实现的CAS
        // 建立一个咱们的测试对象
        Cas cas = new Cas("1");
        // 获取对象内value存储对象的地址
        long offset = U.objectFieldOffset(cas.getClass().getDeclaredField("value"));
        // 经过Unsafe类的CAS函数进行修改
        System.out.println(U.compareAndSwapObject(cas, offset, "1", "2"));
        System.out.println(cas);
    }

    static class Cas{
        private String value;

        public Cas(String value) {
            this.value = value;
        }

        @Override
        public String toString() {
            return "Cas{" +
                    "value='" + value + '\'' +
                    '}';
        }
    }

    // 由于 Unsafe的构造函数是私有的,并且它提供的 getUnsafe()也只有系统类才能使用,
    // 因此咱们只能经过反射获取Unsafe实例了
    private static Unsafe getUnsafe() {
        Unsafe unsafe = null;
        try {
            Constructor<?> declaredConstructor = Unsafe.class.getDeclaredConstructors()[0];
            declaredConstructor.setAccessible(true);
            unsafe = (Unsafe) declaredConstructor.newInstance();
        } catch (Exception e) { e.printStackTrace(); }
        return unsafe;
    }

}

看一下main()的打印结果,修改cas对象的value值,从“1”改为“2”,执行成功

改一下main(),试一下从“2”改为“2”

public static void main(String[] args) throws NoSuchFieldException {
    // java Unsafe类调用C++ 实现的CAS
    // 建立一个咱们的测试对象
    Cas cas = new Cas("1");
    // 获取对象内value存储对象的地址
    long offset = U.objectFieldOffset(cas.getClass().getDeclaredField("value"));
    // 经过Unsafe类的CAS函数进行修改
    System.out.println(U.compareAndSwapObject(cas, offset, "2", "2"));
    System.out.println(cas);
}

这个时候修改是失败的,由于value的值为“1”,而CAS操做里 预期的旧值是“2”,因此没法成功执行从“2”改为“2”

下面2个函数都是Unsafe类里面的,都是私有的,并且它提供的 getUnsafe()也不能使用因此须要经过反射获取实例

private Unsafe() {
}

@CallerSensitive
public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
        throw new SecurityException("Unsafe");
    } else {
        return theUnsafe;
    }
}

ABA问题

ABA就是内存V的变量值从A变成B,再从B变成A,这个时候CAS只判断值是否相等,只要值相等就会认为这个值没改变过,但其实是已经变化了。咱们能够添加多一个标识(版本号、时间)判断这个值是否已经改变过,java也提供了相关的解决方案 AtomicStampedReference 类。

先看一下普通CAS的操做类

public static void main(String[] args) {
    // 定义IntegerCAS    AtomicInteger AI = new AtomicInteger(1);
    System.out.println("初始化值:" + AI);
    AI.compareAndSet(1,2);
    System.out.println("第一次CAS操做完成以后值:" + AI);
    AI.compareAndSet(2,1);
    System.out.println("第二次CAS操做完成以后值:" + AI);
    AI.compareAndSet(1,3);
    System.out.println("第二次CAS操做完成以后值:" + AI);
}

这种只须要V的值与A一致就能够修改,存在ABA问题

接下来看看CAS的标识引用类

public static void main(String[] args) {
    // 定义 AtomicStampedReference 类,第一个参数是咱们的初始化值,第二个是版本标识
    AtomicStampedReference<Integer> ASR = new AtomicStampedReference<Integer>(1,1);
    System.out.println("初始化值:" + ASR.getReference());
    ASR.compareAndSet(1,2, ASR.getStamp(), ASR.getStamp() + 1);
    System.out.println("第一次CAS操做完成以后值:" + ASR.getReference());
    // 获取stamp ,下面2次新增都使用这个 stamp    int stamp = ASR.getStamp();
    // 这一次能够修改为功,由于 stamp 的预期值一致
    ASR.compareAndSet(2,1, stamp, stamp + 1);
    System.out.println("第二次CAS操做完成以后值:" + ASR.getReference());
    // 这一次能够修改失败,由于 stamp 的预期值不一致
    ASR.compareAndSet(1,3, stamp, stamp + 1);
    System.out.println("第三次CAS操做完成以后值:" + ASR.getReference());
}

这种不紧V的值要与A相等,且记录的标识也须要一致才能完成修改

CAS是乐观锁,synchronized是悲观锁。二者各有优异,应对不一样场景选择合适的锁。

相关文章
相关标签/搜索