基于CAS操作的Java非阻塞同步机制

原文链接:https://www.jianshu.com/p/8c94e1a41e7e

前言

Java非阻塞同步机制广泛应用于乐观锁、阻塞队列、线程池内部的实现。相比独占锁(synchronized),它不会阻塞线程,因此不需要调度线程,具有更高的性能;代码层面可控粒度更细,灵活度高,伸缩性更强。

硬件层支持

多处理器针对并发而设计了一些特殊CPU指令,用于管理共享数据的并发访问。

早期的处理器支持原子的测试并设置(Test-and-Set),获取并递增(Fetch-and-Increment),交换(Swap)等指令,用来实现各种互斥体,通过互斥体又可以实现更复杂的并发对象。

现在,几乎所有处理器都支持以某种形式对原子读-改-写,例如接下来要说的比较与交换(Compare And Swap)。操作系统和JVM用该指令实现乐观锁和并发数据结构。这些指令在Java 5.0及以后引入JVM。

CAS操作

非阻塞同步机制的实现依赖于硬件层的CAS操作。CAS操作包括三个操作数:需要读写的内存位置V,进行比较的值A,拟写入的新值B。当CAS操作要将V赋值为B,首先判断V是否为A,如果是则赋值成功。如果在某个线程的CAS操作中发现V已经不是A,CAS操作内部会检测到这个错误并返回失败。

CAS操作的关键是“判断内存V的值等于A”和“将内存V赋值为B”这两步骤必须是原子性的。在X86平台,通过"
lock cmpxchg"汇编指令完成操作。

lock指令说明:

在所有的 X86 CPU 上都具有锁定一个特定内存地址的能力,当这个特定内存地址被锁定后,它就可以阻止其他的系统总线读取或修改这个内存地址。这种能力是通过 LOCK 指令前缀再加上下面的汇编指令来实现的。当使用 LOCK 指令前缀时,它会使 CPU 宣告一个 LOCK# 信号,这样就能确保在多处理器系统或多线程竞争的环境下互斥地使用这个内存地址。当指令执行完毕,这个锁定动作也就会消失。

有了lock指令保证原语性,接下来执行cmpxchg指令,该指令的作用是比较与交换:

static inline int atomic_cmpxchg(atomic_t *ptr, int old, int new)

比较old和原子变量ptr中的值,如果相等,那么就把new值赋给原子变量。返回旧的原子变量ptr中的值。

JVM对CAS的支持

Java 5.0前如果不编写明确的代码无法执行CAS,5.0及以后可以通过UnSafe类来执行CAS操作。在支持CAS的平台,运行时CAS操作被编译为多条机器指令。如果平台不支持CAS,那么JVM将使用自旋锁。

扫描二维码关注公众号,回复: 2072729 查看本文章

CAS缺陷

ABA问题

假设第一个线程执行CAS操作,将内存V位置上的A改为C,同时第二个线程先于第一个线程通过CAS将V改为B,第三个线程又在第二个线程之后第一个线程之前执行完CAS将V上的B改为A,那么轮到第一个线程执行时会认为这是正确的,并将V上的A改为C。

某些特殊场景不能容忍ABA问题,遇到ABA问题应该将其视为无效的操作。举例:
有一个栈,A->B->C,A为栈顶。
线程1 通过CAS要将HEADER由A设置为B,将A出栈。
线程2 通过CAS先于线程1将A出栈,又通过CAS将B出栈,最后通过CAS将A入栈。此时堆栈结构为A->C。
等待线程1执行时发现栈顶内存位置任然是A,因此将A出栈,将栈顶设置为B。此时堆栈的结构为B。

ABA问题的解决:
Java中的AtomicStampedReference引入了版本号解决上述问题,当线程2通过AtomicStampedReference完成一系列CAS操作,虽然最后栈顶的位置上的引用还指向A,但是版本号确经过了3次更新。线程1再通过AtomicStampedReference执行CAS操作时发现最初传入的版本号与最新的版本号不一致,所以返回失败。

    /**
     * Atomically sets the value of both the reference and stamp
     * to the given update values if the
     * current reference is {@code ==} to the expected reference
     * and the current stamp is equal to the expected stamp.
     *
     * @param expectedReference the expected value of the reference
     * @param newReference the new value for the reference
     * @param expectedStamp the expected value of the stamp
     * @param newStamp the new value for the stamp
     * @return {@code true} if successful
     */
    public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

AtomicStampedReference的本质是对两个内存位置执行CAS操作,即一个“引用-版本号”二元组,类似的,AtomicMarkableReference更新一个“引用-布尔值”二元组。

自旋时间过长

如果循环执行CAS操作长时间不成功,线程不会挂起,从而对CPU资源浪费。所以使用CAS操作进行并发操作时,也要考虑业务的场景是否合适。

UnSafe类

在对线程池源码分析时,发现底层用到了sun.misc.Unsafe这个类,实际上JDK就是通过它间接调用底层的JNI方法完成CAS操作。

获取UnSafe实例

在JDK的源码分析中,常见:

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

其实现为:

  public static Unsafe getUnsafe()
  {
    SecurityManager sm = System.getSecurityManager();
    if (sm != null)
      sm.checkPropertiesAccess();
    return unsafe;
  }

getUnsafe方法做了安全检查,防止在不信任的代码中调用它。

但我们还是能够通过反射获取它:

 Field f = Unsafe.class.getDeclaredField("theUnsafe");
 f.setAccessible(true);

常见方法

public native boolean compareAndSwapInt(Object obj, long offset,int expect, int update);  
public native boolean compareAndSwapLong(Object obj, long offset, long expect, long update); 
public native boolean compareAndSwapObject(Object obj, long offset,Object expect, Object update);

上述三个CAS方法分别对应int、long、引用类型的操作。


public native void putOrderedInt(Object obj, long offset, int value);  
 public native void putOrderedLong(Object obj, long offset, long value);  
public native void putOrderedObject(Object obj, long offset, Object value); 

有序或者有延迟的将int、long、引用类型改为目标值。但不保证修改后对其他线程立即可见,除非变量用volatile修饰。


public native long objectFieldOffset(Field field); 

取得静态字段的内存地址。该地址对于Field是唯一的。


public native void putIntVolatile(Object obj, long offset, int value);
public native void putLongVolatile(Object obj, long offset, long value); 
public native void putObjectVolatile(Object obj, long offset, Object value);  
public native int getIntVolatile(Object obj, long offset);  
public native long getLongVolatile(Object obj, long offset);  
public native Object getObjectVolatile(Object obj, long offset);  
...

以volatile的语义更新地址上的值,并以volatile的语义读取地址上的值。


public native void park(boolean isAbsolute, long time);  

阻塞一个线程,直到对其调用unpark。isAbsolute为true,time的单位是新纪元后的毫秒,否则time的单位为纳秒。time即超时时间,当它等于0时永不超时。

 public native void unpark(Thread thread);  

解除阻塞中的线程。

原子类

AtomicBoolean
AtomicInteger
AtomicIntegerArray
AtomicIntegerFieldUpdater
AtomicLong
AtomicLongArray
AtomicLongFieldUpdater
AtomicMarkableReference
AtomicReference
AtomicReferenceArray
AtomicReferenceFieldUpdater
AtomicStampedReference
DoubleAccumulator
DoubleAdder
LongAccumulator
LongAdder

小结

本篇讲述了Java非阻塞同步机制的实现原理,了解了CAS操作的汇编指令以及它的缺陷。最后认识了JDK中调用CAS操作的方法,通过UnSafe类不但可以执行CAS方法,也可以阻塞或唤醒线程。

猜你喜欢

转载自blog.csdn.net/wuyangyang555/article/details/80984661
今日推荐