Java并发编程之CAS原理

什么是CAS?

CAS:Compare and Swap,即比较再交换。
CAS有3个操作数:
① 内存值V;
② 旧的预期值A;
③ 要修改的新值B。
当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
CAS是一种无锁算法(切记CAS不是锁,而是可以利用它构建一系列高并发的锁)。

Java的对象偏移量是什么?

在学习使用JDK的CAS前,我们需要学习Unsafe类的使用,当然们需要了解一些必要的概念和Java基础知识,例如:

对象头:由于Java面向对象的思想,在JVM中需要大量存储对象,存储时为了实现一些额外的功能,需要在对象中添加一些标记字段用于增强对象功能,这些标记字段组成了对象头。

对象属性偏移量:简称 ”偏移量“,是一种用来计算该属性在某一对象实例下,属性值位于对象存储字节位置的一种表示。(CAS离不开它)
调用Unsafe的CAS方法时,实际上操作的是对象实际内存地址中的某一片段或全部,这在Java中属于危险操作,所以不要为了装X而使用哦。

好了,知道了偏移量干嘛用的,那就进入正题吧。。

如何使用JDK的CAS类?

在Java中CAS操作是由底层类库Unsafe(sun.misc.Unsafe)类提供的,我们先看下这个类怎么构建:

    private static final Unsafe theUnsafe;

    private Unsafe() { // 私有构造
    }

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

Unsafe是一个final类,其构造方法为private,是一个单例类,它提供了一个获取实例的方法getUnsafe()。
当然,拿后脑勺想想可以这么干:

private static final Unsafe unsafe = Unsafe.getUnsafe();

对不起,这和后脑勺没关系。。。。
由于Java限制了Unsafe的使用范围,对其进行了检查,所以当我们JDK外这么使用会报错。
那咋整呢,先上答案吧(嘿嘿):

## 利用反射获取Unsafe类下的theUnsafe字段值

    // JDK外 使用Unsafe方法
    private static Unsafe getUnsafeInstance() {
        Field theUnsafeInstance;
        try {
            theUnsafeInstance = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafeInstance.setAccessible(true);
            return (Unsafe) theUnsafeInstance.get(Unsafe.class);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
        throw new RuntimeException("theUnsafe Exception");
    }

这样,你就可以在你的类中使用它了!!等待,还没完,你再往下看看。。。

  1. 首先,我们需要一个并发操作的属性值,且用volatile关键字来声明;
  2. 需要对每一个并发操作的属性值额外声明一个long类型的偏移量,来记录每一个并发属性值在对象中的偏移量,由于每个属性的偏移量在该类加载或者之前就已经确定了(则该类的不同实例的同一属性偏移量相等),所以最好 static+final,防止被更改;
  3. 采用static块方式对偏移量进行初始化。

好了,气氛到了,上代码:

/**
 * CAS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题。
 * 1. 循环时间长开销很大,利用CAS构建的锁适用于读多写少操作;
 * 2. 每次只能保证一个变量的原子操作;
 * 3. ABA问题。
 */
public class TestCAS {

    private static final Unsafe U = getUnsafeInstance();

    // 如果使用 CAS方法对其直接更改则无法修改,
    // 初步估略原因为unsafe是对jvm内存的直接操作,所以必须用偏移量来比对
    private volatile int t_val = 0;

    // 如果想获取一个对象的属性的值,我们一般通过getter方法获得,
    // 而sun.misc.Unsafe却不同,我们可以把一个对象实例想象成一块内存,
    // 而这块内存中包含了一些属性,如何获取这块内存中的某个属性呢?
    // 那就需要该属性在该内存的偏移量了,每个属性在该对象内存中valueOffset偏移量不同,
    // 每个属性的偏移量在该类加载或者之前就已经确定了(则该类的不同实例的同一属性偏移量相等),
    // 所以sun.misc.Unsafe可以通过一个对象实例和该属性的偏移量用原语获得该对象对应属性的值;
    private static final long valueOffset;

    static {
        try {
            valueOffset = U.objectFieldOffset
                    (TestCAS.class.getDeclaredField("t_val"));
        } catch (Exception ex) {
            throw new Error(ex);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TestCAS testCAS = new TestCAS();
        testCAS.testCas();
    }

    // 测试 cas 操作
    // compareAndSwapInt(操作对象, 原始值, 期望值, 修改值)
    public void testCas() throws InterruptedException {
        Runnable runnable = () -> {
            int f_val;
            int u_val;
            do {
                f_val = t_val;
                u_val = f_val + 1;
                // System.out.println(t_val + " " + f_val + " " + u_val);
            } while (!U.compareAndSwapInt(this, valueOffset, f_val, u_val));

//            synchronized (this){
//                t_val = t_val + 1;
//            }
        };

        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(runnable);
            thread.start();
        }
        Thread.sleep(500); // 主线程比子线程执行的快,导致输出不正确
        System.out.println(t_val);
    }

    // 使用Unsafe方法
    private static Unsafe getUnsafeInstance() {
        Field theUnsafeInstance;
        try {
            theUnsafeInstance = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafeInstance.setAccessible(true);
            return (Unsafe) theUnsafeInstance.get(Unsafe.class);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
        throw new RuntimeException("theUnsafe Exception");
    }
}

应该在座的都能看懂,我就再强调一下compareAndSwap的方法参数(包含三个方法):

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

这三个方法是Unsafe类提供的,都具有4个形参,各代表:
第一个,操作对象,将要操作的实例对象;
第二个,偏移量,对象的属性偏移量;
第三个,期望值,期望值与内存现有值相同才会修改;
第四个,修改值,将要修改的值;

OK,开始伸展吧!!!

CAS做了什么操作?

我先提醒一点:在这里,还没有涉及到任何锁的概念,CAS是底层CPU提供的支持,利用CPU指令级的操作来保证原子性。

如果要谈这个话题,那就可深可浅了。
往深了说,那就到CPU的机制,往浅了说,到这你就结束吧,我就不留你了 /(ㄒoㄒ)/~~

其实,本质还是并发三个要素:原子性、可见性、有序性。
这是我看过觉得不错的一个博客,写的毕竟详细,给大家附上链接:并发三要素

那这里我就简单讲讲代码层面,详细的就点上面链接吧(打字也是很累的,copy我又感觉没脸,哈哈)

原子性方面:我们的Unsafe大牛的CAS方法就可以帮你解决,在保证可见性和有序性的情况下,他必然是原子性的。

可见性方面:我们使用了值的volatile关键字,保证修改的值会立即被更新到主存,保证其他线程会从主存直接读取。

有序性方面:volatile可以保证一部分内存屏障,同时java也具有happens-before法则来保证有序性。

可见,三个性质相互,缺一不可。

CAS的开销及缺点

CAS有哪些消耗?这个可以了解一下cache miss,CPU的缓存命中问题

  1. 由于CAS是一种无锁算法,在基于CAS实现锁时,多数采用的是do-while乐观锁方式,所以在当并发量很高的情况下对值进行修改,虽提升了吞吐率,但会导致CAS更新值命中低,cpu负载加剧等新问题,所以乐观锁更适合读多写少的高并发场景。
  2. 由于CAS是期望值和内存值的比较,当并发线程较多,但值却变化范围较小时,容易造成ABA问题,所以需要能够容忍ABA问题才能使用CAS构建的锁。

ABA解释:线程1本该把期望值从A变为C,结果线程2 (在线程1获取到期望值后改变值之前) 把期望值A改为B又改回A,线程1继续变为C。这个操作称为A-B-A问题。

你可能感兴趣的:(Java并发编程之CAS原理)