java并发编程第三天---实现原子操作

902 阅读6分钟

原子操作

所谓原子操作,就是指不可中断的一个或一系列操作。

例如 i++,就不是原子操作,它包括 读取、更新、写入 三步原子操作。

非原子操作在多线程环境下会导致线程安全问题,Java 中可以通过 监视器锁 或 CAS 来实现原子操作。


synchronized 实现原子操作

public class AtomicityTest {
    public int count = 0;

    public synchronized void increase() {
        count++;
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicityTest test = new AtomicityTest();
        for (int i=0;i<10;i++){
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    test.increase();
                }
            }).start();
        }

        Thread.sleep(2000); // 保证前面的线程执行完
        System.out.println("count的值:" + test.count);
    }
}

给非原子操作所在的方法加上监视器锁,保证了同一时刻只有一个线程能进入到方法中执行,保证了原子性和可见性。但是显然大大降低了并发性,并且监视器锁带来的上下文切换也会影响性能。有没有更好的方法呢?


CAS

简介

CAS 即 Compare And Swap ,是 JDK 提供的非阻塞原子性操作,利用了处理器提供的 CMPXCHG 指令保证了 比较-更新 操作的原子性。

JDK 的 Unsafe 类提供了一系列 compareAndSwap* 方法。例如下面的 compareAndSwapInt。

// CAS 有四个操作数
// 如果对象 obj 中 内存偏移量为 valueOffset 的变量(假设为 value )值为 expect ,则用新的值 update 替换 value 的值 ,否则什么也不干
public final native boolean compareAndSwapInt(Object obj, long valueOffset, int expect, int update);

想要学好 CAS,先得对 Unsafe 类有一点的了解。


Unsafe 类

sun.misc.Unsafe 类提供了硬件级别的原子性操作,Unsafe 类中的方法都是 native 方法。

我们来手动获取 Unsafe 类的实例,并实现 CAS 操作。


public class TestUnsafe {
    static Unsafe unsafe = Unsafe.getUnsafe(); // 获取 Unsafe 类的实例
    
    // 记录变量 state 在类 TestUnsafe 中的偏移量
    static long stateOffset;
    
    // 用 volatile修饰,保证了内存的可见性
    private volatile long state;

    static {
        try {
            // 获取 state 变量在类 TestUnsafe 中的偏移量
            stateOffset = unsafe.objectFieldOffset(TestUnsafe.class.getDeclaredField("state"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    TestUnsafe testUnsafe = new TestUnsafe();
        int expect = 0;
        int update = 10;
        // 如果对象 testUnsafe 中 内存偏移量为 stateOffset 的变量( state )值为 expect ,则用新的值 update 替换 state ,否则什么也不干
        Boolean success = unsafe.compareAndSwapInt(testUnsafe,stateOffset,expect,update);
        System.out.println(success); // CAS 操作是否成功
        System.out.println("state:" + testUnsafe.state);
        System.out.println("expect:" + expect); // expect 的值不变,value 的值变
    }
}

此时程序会报错

因为普通的开发人员不能直接获取 Unsafe 实例 (JDK 开发组做的限制, 防止开发人员直接操作内存),通过判断类加载器来限制 Unsafe 类的使用。

@CallerSensitive
public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass(); // 获取调用getUnsafe方法的class对象,这里是TestUnsafe.class
    if (!VM.isSystemDomainLoader(var0.getClassLoader())) { 
        throw new SecurityException("Unsafe");
    } else {
        return theUnsafe;
    }
}

// 判断TetstUnsafe.class是否为BootstraclassLoader所加载
public static boolean isSystemDomainLoader(ClassLoader var0) { 
    return var0 == null;
}

这时候万能的反射就派上用场了,可以通过反射来获取 Unsafe 类的实例。

public class TestUnsafe {
    static Unsafe unsafe;
	
    // 记录变量 state 在类 TestUnsafe 中的偏移量
    static long stateOffset;
    
    // 通过 volatile 修饰,保证了内存的可见性
    private volatile long state;

    static {
        try {
            // 通过反射获取 Unsafe 类的实例
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
            // 获取 state 变量在类 TestUnsafe 中的偏移值
            stateOffset = unsafe.objectFieldOffset(TestUnsafe.class.getDeclaredField("state"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    TestUnsafe testUnsafe = new TestUnsafe();
        int expect = 0;
        int update = 10;
        // 如果对象 testUnsafe 中 内存偏移量为 stateOffset 的变量( state )值为 expect ,则用新的值 update 替换 state ,否则什么也不干
        Boolean success = unsafe.compareAndSwapInt(testUnsafe,stateOffset,expect,update);
        System.out.println(success); // CAS 操作是否成功
        System.out.println("state:" + testUnsafe.state);
        System.out.println("expect:" + expect); // expect 的值不变,value 的值变
    }
}

CAS 操作成功,state 的值被设为 10。


JDK 内置原子操作类

JUC 并发包中包含有 AtomiclntegerAtomicLongAtomicBoolean 等原子性操作类

它们的原理类似,底层原理都是 CAS,我们来研究一下 AtomicInteger 类。

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // 获取 Unsafe 类实例
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    
    // 记录变量 value 在类 AtomicInteger 中的偏移值
    private static final long valueOffset;

    static {
        try {
            // 获取变量 value 在类 AtomicInteger 中的偏移值
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
	
    // 变量的实际值,声明为 volatile,保证了内存的可见性
    private volatile int value;
    
    
    // 构造函数
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }
    
    // 返回 value 更新后的值
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    
    // 返回 value 更新前的值
    public final int getAndIncrement() {        
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    
    ...
}

因为 AtomicInteger 类是由 Bootstrap 类加载器加载的,所以可以通过 Unsafe.getUnsafe() 方法直接获取 Unsafe 类的实例。


// 原子性设置 value 值为原始值 +1,返回值为原始值
public final int getAndAddInt(Object obj, long valueOffset, int delta) {
    int expect;
    do {
        // 获取对象 obj(AtomicInter 实例) 中内存偏移量为 valueOffset 的变量(value)的值
        expect = this.getIntVolatile(obj, valueOffset);
    } while(!this.compareAndSwapInt(obj, valueOffset, expect, expect + delta));
   	// 如果对象 obj(AtomicInter 实例)中内存偏移量为 valueOffset 的变量(value)值为 expect ,
    // 则用新的值 expect + delta,替换 value的值(此时 expect 不变) ,否则继续循环
	
    // 返回 value 原始值
    return expect;
}

这段代码能保证多线程环境下的原子性,当有多个线程同时修改变量 value 时,每次自增操作都要比较当前 value 值是否是内存中最新的值,如果是才更新,否则继续 CAS 自旋,直到成功。

例如: 有两个线程 A 和 B 同时对 value 自增,某一时刻 value 的原始值为 5,线程 A 通过 getIntVolatile 方法取得内存中最新的 value 值 5,然后让出 CPU 调度,线程 B 也通过 getIntVolatile 方法取得内存中最新的 value 值 5,然后进行 compareAndSwapInt, 将 value 值设为 6。此时线程 A 恢复,执行 compareAndSwapInt,发现自己手里的值 5 和内存中的最新值 6 不一样,所以 CAS 操作失败,继续循环,直到成功。因为 value 被 volatile 修饰,所以某个线程对 value 的修改,对其他线程立即可见。


用原子操作类实现线程安全的递增

public class AtomicityTest{
    public AtomicInteger count = new AtomicInteger();

    public void increase() {
        count.incrementAndGet();
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicityTest test = new AtomicityTest();
        for (int i=0;i<10;i++){
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    test.increase();
                }
            }).start();
        }

        Thread.sleep(2000); // 保证前面的线程执行完
        System.out.println("count的值:" + test.count);
    }
}

count 的值一定为10000。


CAS的缺点

ABA问题

CAS 在操作值的时候,需要检查值有没有发生变化,如果没有发生变化则更新。

ABA 问题的解决思路就是使用版本号,在变量前面加上版本号,每次变量更新的时候将版本号 +1,那么 A -> B -> A 就会变成 1A -> 2B->3A,这两个 A 就属于不同的 A 了。

JDK 中提供了 AtomicStampedReference 类来解决 ABA 问题。

AtomicStampedReference 内部维护了一个时间戳,每次检验要同时检验值和时间戳。


高并发下开销大

CAS 只能保证一个共享变量的原子操作,在高并发情况下,多线程对一个变量 CAS 争夺失败后进行自旋,如果 CAS 长时间不成功,会给 CPU 带来非常大的执行开销。 解决方案: JDK 1.8 提供的 LongAdder 类,后面再详解这个类。