Java并发编程(十三)Java 中的原子性操作

逆流者 2021年02月21日 89次浏览

此文为读书笔记,欢迎评论,谈论问题,共同进步!


原子性操作,是指执行一系列操作时,这些操作要么全部执行,要么全部不执行,不存在只执行其中一部分的情况。在设计计数器时一般都先读取当前值,然后+1,再更新。这个过程是读—改一写的过程,如果不能保证这个过程是原子性的,那么就会出现线程安全问题。

Java 中的 CAS操作

CAS(Compare and Swap),其是 JDK提供的非阻塞原子性操作,它通过硬件保证了比较一更新操作的原子性。JDK里面的Unsafe类提供了一系列的 compareAndSwap*方法,下面以compareAndSwapLong方法为例进行简单介绍。

  • boolean compareAndSwapLong(Object obj, long valueOffset, long expect, long update)方法∶
    其中compareAndSwap的意思是比较并交换。
    CAS有四个操作数,分别为∶对象内存位置、对象中的变量的偏移量、变量预期值和新的值。
    其操作含义是,如果对象 obj 中内存偏移量为valueOffset 的变量值为expect,则使用新的值update替换旧的值 expect。这是处理器提供的一个原子性指令。

关于CAS 操作有个经典的ABA问题:
假如线程1使用 CAS 修改初始值为A的变量X,那么线程1会首先去获取当前变量X的值(为A),然后使用CAS操作尝试修改X的值为B,如果使用CAS操作成功了,那么程序运行一定是正确的吗?其实未必,这是因为有可能在线程1获取变量X的值A后,在执行CAS前,线程2使用CAS修改了变量X的值为B,然后又使用CAS修改了变量X的值为A。所以虽然线程1执行CAS时X的值是A,但是这个A已经不是线程1获取时的A了。这就是ABA问题。

ABA 问题的产生是因为变量的状态值产生了环形转换,就是变量的值可以从A到B,然后再从B到A。如果变量的值只能朝着一个方向转换,比如A到B,B到C,不构成环形,就不会存在问题。
JDK 中的AtomicStampedReference类给每个变量的状态值都配备了一个时间戳,从而避免了 ABA 问题的产生。

Unsafe 类

Unsafe 类中的重要方法

JDK 的rt.jar包中的 Unsafe类提供了硬件级别的原子性操作,Unsafe类中的方法都是 native方法,它们使用JNI 的方式访问本地 C++ 实现库。

  • long objectFieldOffset(Field field)方法∶
    返回指定的变量在所属类中的内存偏移地址,该偏移地址仅仅在该 Unsafe 函数中访问指定字段时使用。
    如下代码使用 Unsafe类获取变量 value 在 AtomicLong 对象中的内存偏移。
static { 
	try {
			valueOffset = unsafe.objectFieldoffset(AtomicLong.class.getDeclaredField("value")); 
		} catch (Exception ex) {
			throw new Error(ex);
		}
}
  • int arrayBaseOffset(Class arrayClass)方法∶
    获取数组中第一个元素的地址。
  • int arrayIndexScale(Class arrayClass)方法∶
    获取数组中一个元素占用的字节。
  • boolean compareAndSwapLong(Object obj,long offset,long expect,long update)方法∶
    比较对象 obj中偏移量为ofset的变量的值是否与expect相等,相等则使用update值更新,然后返回 true,否则返回 false。
  • public native long getLongvolatile(Object obj,long offset)方法∶
    获取对象 obj中偏移量为 offst 的变量对应 volatile 语义的值。
  • void putLongvolatile(Object obj long ofset,long value)方法∶
    设置obj对象中offet偏移的类型为 long 的 field 的值为value,支持 volatile 语义。
  • void putOrderedLong(Object obj,long offst, long value)方法∶
    设置obj对象中offset偏移地址对应的long型 field 的值为value。这是一个有延迟的putLongvolatile方法,并且不保证值修改对其他线程立刻可见。
    只有在变量使用volatile修饰并且预计会被意外修改时才使用该方法。
  • void park(boolean isAbsolute,long time)方法∶
    阻塞当前线程,其中参数isAbsolute等于false且 time 等于0表示一直阻塞。time大于0表示等待指定的 time后阻塞线程会被唤醒,这个time是个相对值,是个增量值,也就是相对当前时间累加time后当前线程就会被唤醒。
    如果 isAbsolute 等于 true,并且 time大于0,则表示阻塞的线程到指定的时间点后会被唤醒,这里 time是个绝对时间,是将某个时间点换算为 ms 后的值。另外,当其他线程调用了当前阻塞线程的 interrupt方法而中断了当前线程时,当前线程也会返回,而当其他线程调用了unPark方法并且把当前线程作为参数时当前线程也会返回。
  • void unpark(Object thread)方法∶
    唤醒调用 park后阻塞的线程下面是 JDK8 新增的函数,这里只列出 Long类型操作。
  • long getAndSetLong(Object obj,long offset,long update)方法∶
    获取对象 obj 中偏移量为 offset 的变量 volatile 语义的当前值,并设置变量 volatile 语义的值为 update。
public final long getAndsetLong(Object obj,long offset,long update) {
	long l; 
	do {
		l = getLongvolatile (obj, offset);// (1)
	} while (!compareAndSwapLong(obj,offset,l,update)); 
	return l;
}

上面代码: (1)处的 getLongvolatile 获取当前变量的值,然后使用 CAS 原子操作设置新值。这里使用 while循环是考虑到,在多个线程同时调用的情况下 CAS失败时需要重试。

  • long getAndAddLong(Object obj,long offset, long addValue)方法∶获取对象 obj 中偏移量为 offset 的变量 volatile 语义的当前值,并设置变量值为原始值 +addValue。
public final long getAndAddLong(Object obj,long offset,long addValue) {
	long l; 
	do {
		l = getLongvolatile (obj,offset);
	while(!compareAndSwapLong(obj,offset,l,l + addValue)); 
	return l;
}

类似getAndSetLong 的实现,只是这里进行CAS操作时使用了原始值+传递的增量参数 addValue 的值。

如何使用 Unsafe 类

先简单测试下:

public class UnSafeTest {

    /** unsafe 实例 */
    static final Unsafe unsafe = Unsafe.getUnsafe();

    /** 记录变量state 在类 UnSafeTest 中的偏移量 */
    static long stateOffset;

    /** 变量state */
    private volatile long state = 0;

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

    public static void main(String[] args) {
        // 创建实例,设置state 为 1
        UnSafeTest test = new UnSafeTest();
        boolean success = unsafe.compareAndSwapInt(test, stateOffset, 0, 1);
        System.out.println(success);
    }
}
Exception in thread "main" java.lang.ExceptionInInitializerError
Caused by: java.lang.SecurityException: Unsafe
	at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
	at base.thread.book.UnSafeTest.<clinit>(UnSafeTest.java:12)

发现getUnsafe 实例报错了, 查看源码:

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

!VM.isSystemDomainLoader(var0.getClassLoader())这个判断直接抛出的异常, 这是为什么?
因为Unsafe类可以直接操作内存, 是不安全的, 所以JDK开发人员特意做了这个限制, 不让技术人员在正规渠道中使用Unsafe类, 这个类是rt.jar中提供的, 这个jar包是使用Bootstrap 类加载器加载的, 而在启动main所在的类是使用AppClassLoader加载的, 所以会报错.

那怎么可以尝试用一下Unsafe类的 ?

可以使用反射来获取Unsafe实例:

public class UnSafeTest2 {

    /** unsafe 实例 */
    static Unsafe unsafe;

    /** 记录变量state 在类 UnSafeTest 中的偏移量 */
    static long stateOffset;

    /** 变量state */
    private volatile long state = 0;

    static {
        try {
            // 使用反射获取Unsafe的成员变量theUnsafe
            Field field = Unsafe.class.getDeclaredField("theUnsafe");

            // 设置为可存取
            field.setAccessible(true);

            unsafe = (Unsafe) field.get(null);
            // 获取state变量 在类 UnSafeTest 中的偏移量
            stateOffset = unsafe.objectFieldOffset(UnSafeTest2.class.getDeclaredField("state"));
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        // 创建实例,设置state 为 1
        UnSafeTest2 test = new UnSafeTest2();
        boolean success = unsafe.compareAndSwapInt(test, stateOffset, 0, 1);
        System.out.println(success); // true
    }
}