您当前的位置: 首页 > 

庄小焱

暂无认证

  • 1浏览

    0关注

    805博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

并发编程——Unsafe、Atomic原理

庄小焱 发布时间:2020-10-08 13:57:31 ,浏览量:1

摘要

Atomic类是JDK处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。

一、Atomic底层实现原理

Java并发编程中synchronized关键字原理的时候我们曾多次谈到过CAS这个概念,那么它究竟是什么?实际上我们在之前往往为了解决多线程并行执行带来的线程安全问题去利用加锁的机制去将多线程并行执行改变为单线程的串行执行,而实则还有另一种手段能够去避免此类问题的发生,而这种方案和之前我们所分析的synchronized关键字互斥的原理大相径庭,它实则是利用一种自旋的无锁形式去解决线程安全问题。

当一个线程想要执行被synchronized修饰的代码/方法,为了避免操作共享资源时发生冲突,每次都需要执行加锁策略,而无锁则总是假设对共享资源的访问没有冲突,线程可以不停执行,无需加锁,无需等待,而万事难遂人愿,一旦发现冲突,无锁策略中难道真的不处理吗?并不然,无锁策略采用一种称为CAS的技术来保证线程执行的安全性,这项CAS技术就是无锁策略实现的关键。

1.1 CAS机制无锁原理

无锁策略听起来很完美,但是当真正的需要使用时又该如果落地实现呢?而CAS机制则是我们无锁策略的落地实现者,CAS全称Compare And Swap(比较并交换),而Java中的CAS实现最终也是依赖于CPU的原子性指令实现(我们稍后会分析),在CAS机制中其核心思想如下:

CAS(V,E,N) 

  • V:需要操作的共享变量
  • E:预期值
  • N:新值

如果V值等于E值,则将N值赋值给V。反则如果V值不等于E值,则此时说明在当前线程写回之前有其他线程对V做了更改,那么当前线程什么都不做。简单的来说就是当一个线程对V需要做更改时,先在操作之前先保存当前时刻共享变量的值,当线程操作完成后需要写回新值时先重新去获取一下最新的变量值与操作开始之前的保存的预期值比对,如果相同说明没有其他线程改过,那么当前线程就执行写入操作。但如果期望值与当前线程操作之前保存的不符,则说明该值已被其他线程修改,此时不执行更新操作,但可以选择重新读取该变量再尝试再次修改该变量,也可以放弃操作,示意图如下:

由于CAS操作属于乐观派,每次线程操作时都认为自己可以成功执行,当多个线程同时使用CAS操作一个变量时,只有一个会成功执行并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS机制即使没有锁,同样也能够得知其他线程对共享资源进行了操作并执行相应的处理措施。同时CAS由于无锁操作中并没有锁的存在,因此不可能出现死锁的情况,所以也能得出一个结论:“CAS天生免疫死锁”,因为CAS本身没有加锁。

难道多个线程在同时做CAS操作时不会出现安全问题造成不一致呢?

因为CAS操作通过我们上面的阐述得知并不是一步到位的,而是也分为多个步骤来执行,有没有可能在判断V和E相同后,正要赋值时,切换了线程,更改了值?答案非常确定:不可能,因为在刚刚在前面我提到过Java中的CAS机制的最终实现是依赖于CPU原子性指令实现,CAS是一种操作系统原语范畴的指令,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS对于CPU来说是一条的原子指令,不会造成所谓的数据不一致问题。

1.2 Java中的Unsafe类

Unsafe类位于sun.misc包中,中文翻译过来就是不安全的意思,当我们第一次看到这个类时,我们可能会感到惊讶,为什么在JDK中会有一个类的名称被命名为“不安全”,但是你详细去研究不难发现这个类的神奇之处,提供的功能十分强大,但是确实存在些许不安全。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,当我们能够通过这个类做到和C的指针一样直接操作内存时也就凸显出此类的不安全性,意味着:

  • 不受JVM管理,也就代表着无法被GC,需要我们手动释放内存,当你使用这个类做了一些操作稍有不慎就会出现内存泄漏
  • Unsafe类中的不少方法中必须提供原始地址(内存地址)和被替换对象的地址,偏移量要自己计算,一旦出现问题就是JVM崩溃级别的错误,会导致整个Java程序崩溃,表现为应用进程直接crash掉。

但是我们通过Unsafe类直接操作内存,也意味着其速度会比普通Java程序更快,在高并发的条件之下能够很好地提高效率,因此,从上面几个角度来看,虽然在一定程度上提升了效率但是也带来了指针的不安全性,Unsafe名副其实。关于Unsafe类的主要功能点如下:

  • 类(Class)相关:提供Class和它的静态域操纵方法
  • 信息(Info)相关:返回某些低级别的内存信息
  • 数组(Arrays)相关:提供数组操纵方法
  • 对象(Objects)相关:提供Object和它的域操纵方法
  • 内存(Memory)相关:提供直接内存访问方法(绕过JVM堆直接操纵本地内存)
  • 同步(Synchronization)相关:提供低级别同步原语、线程挂起/放下等操纵方法
1.2.1 Unsafe类内存管理
//分配内存指定大小的内存
public native long allocateMemory(long bytes);

//根据给定的内存地址address设置重新分配指定大小的内存
public native long reallocateMemory(long address, long bytes);

//用于释放allocateMemory和reallocateMemory申请的内存
public native void freeMemory(long address);

//将指定对象的给定offset偏移量内存块中的所有字节设置为固定值
public native void setMemory(Object o, long offset, long bytes, byte value);

//设置给定内存地址的值
public native void putAddress(long address, long x);

//获取指定内存地址的值
public native long getAddress(long address);

//设置给定内存地址的long值
public native void putLong(long address, long x);

//获取指定内存地址的long值
public native long getLong(long address);

//设置或获取指定内存的byte值
public native byte  getByte(long address);
public native void  putByte(long address, byte x);
//其他基本数据类型(long,char,float,double,short等)的操作与putByte及getByte相同

//操作系统的内存页大小
public native int pageSize();
1.2.2 Unsafe类提供创建对象

在之前我们创建类对象实例时无非通过两种形式:new以及反射机制创建,但是无论是new还是反射的形式创建都会调用对象的构造方法来完成对象的初始化,而Unsafe类提供创建对象实例新的途径如下:

//传入一个对象的class并创建该实例对象,但不会调用构造方法
public native Object allocateInstance(Class cls) throws InstantiationException;
1.2.3 Unsafe类提供类、实例对象以及变量操纵
//获取字段f在实例对象中的偏移量
public native long objectFieldOffset(Field f);

//静态属性的偏移量,用于在对应的Class对象中读写静态属性
public native long staticFieldOffset(Field f);

//返回值就是f.getDeclaringClass()
public native Object staticFieldBase(Field f);


//获得给定对象偏移量上的int值,所谓的偏移量可以简单理解为指针指向该变量的内存地址,
//通过偏移量便可得到该对象的变量,进行各种操作
public native int getInt(Object o, long offset);

//设置给定对象上偏移量的int值
public native void putInt(Object o, long offset, int x);

//获得给定对象偏移量上的引用类型的值
public native Object getObject(Object o, long offset);

//设置给定对象偏移量上的引用类型的值
public native void putObject(Object o, long offset, Object x);
//其他基本数据类型(long,char,byte,float,double)的操作与getInthe及putInt相同

//设置给定对象的int值,使用volatile语义,即设置后立马更新到内存对其他线程可见
public native void  putIntVolatile(Object o, long offset, int x);

//获得给定对象的指定偏移量offset的int值,使用volatile语义,总能获取到最新的int值。
public native int getIntVolatile(Object o, long offset);

//其他基本数据类型(long,char,byte,float,double)的操作与putIntVolatile
//及getIntVolatile相同,引用类型putObjectVolatile也一样。

//与putIntVolatile一样,但要求被操作字段必须有volatile修饰
public native void putOrderedInt(Object o,long offset,int x);
1.2.4 Unsafe类的获取
public static Unsafe getUnsafe() {
  Class cc = sun.reflect.Reflection.getCallerClass(2);
  if (cc.getClassLoader() != null)
      throw new SecurityException("Unsafe");
  return theUnsafe;
}
public class UnSafeDemo {
    public  static  void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InstantiationException {
        // 通过反射得到theUnsafe对应的Field对象
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        // 设置该Field为可访问
        field.setAccessible(true);
        // 通过Field得到该Field对应的具体对象,传入null是因为该Field为static的
        Unsafe unsafe = (Unsafe) field.get(null);
        System.out.println(unsafe);

        //通过allocateInstance直接创建对象
        Demo demo = (Demo) unsafe.allocateInstance(Demo.class);

        Class demoClass = demo.getClass();
        Field str = demoClass.getDeclaredField("str");
        Field i = demoClass.getDeclaredField("i");
        Field staticStr = demoClass.getDeclaredField("staticStr");

        //获取实例变量str和i在对象内存中的偏移量并设置值
        unsafe.putInt(demo,unsafe.objectFieldOffset(i),1);
        unsafe.putObject(demo,unsafe.objectFieldOffset(str),"Hello Word!");

        // 返回 User.class
        Object staticField = unsafe.staticFieldBase(staticStr);
        System.out.println("staticField:" + staticStr);

        //获取静态变量staticStr的偏移量staticOffset
        long staticOffset = unsafe.staticFieldOffset(userClass.getDeclaredField("staticStr"));
        //获取静态变量的值
        System.out.println("设置前的Static字段值:"+unsafe.getObject(staticField,staticOffset));
        //设置值
        unsafe.putObject(staticField,staticOffset,"Hello Java!");
        //再次获取静态变量的值
        System.out.println("设置后的Static字段值:"+unsafe.getObject(staticField,staticOffset));
        //调用toString方法
        System.out.println("输出结果:"+demo.toString());

        long data = 1000;
        byte size = 1; //单位字节

        //调用allocateMemory分配内存,并获取内存地址memoryAddress
        long memoryAddress = unsafe.allocateMemory(size);
        //直接往内存写入数据
        unsafe.putAddress(memoryAddress, data);
        //获取指定内存地址的数据
        long addrData = unsafe.getAddress(memoryAddress);
        System.out.println("addrData:"+addrData);

        /**
         * 输出结果:
         sun.misc.Unsafe@0f18aef2
         staticField:class com.demo.Demo
         设置前的Static字段值:Demo_Static
         设置后的Static字段值:Hello Java!
         输出USER:Demo{str='Hello Word!', i='1', staticStr='Hello Java!'}
         addrData:1000
         */

    }
}

class Demo{
    public Demo(){
        System.out.println("我是Demo类的构造函数,我被人调用创建对象实例啦....");
    }
    private String str;
    private int i;
    private static String staticStr = "Demo_Static";

    @Override
    public String toString() {
        return "Demo{" +
            "str = '" + str + '\'' +
            ", i = '" + i +'\'' +
            ", staticStr = " + staticStr +'\'' +
        '}';
    }
}
1.2.5 Unsafe类提供直接获取数组元素内存位置
//获取数组第一个元素的偏移地址
public native int arrayBaseOffset(Class arrayClass);

//数组中一个元素占据的内存空间,arrayBaseOffset与arrayIndexScale配合使用,可定位数组中每个元素在内存中的位置
public native int arrayIndexScale(Class arrayClass);
1.2.6 Unsafe类提供Java中的CAS操作支持
//第一个参数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);
1.2.7 JDK8之后新增的基于原有CAS方法的方法
 //1.8新增,给定对象o,根据获取内存偏移量指向的字段,将其增加delta,
 //这是一个CAS操作过程,直到设置成功方能退出循环,返回旧值
 public final int getAndAddInt(Object o, long offset, int delta) {
     int v;
     do {
         //获取内存中最新值
         v = getIntVolatile(o, offset);
       //通过CAS操作
     } while (!compareAndSwapInt(o, offset, v, v + delta));
     return v;
 }

//1.8新增,方法作用同上,只不过这里操作的long类型数据
 public final long getAndAddLong(Object o, long offset, long delta) {
     long v;
     do {
         v = getLongVolatile(o, offset);
     } while (!compareAndSwapLong(o, offset, v, v + delta));
     return v;
 }

 //1.8新增,给定对象o,根据获取内存偏移量对于字段,将其 设置为新值newValue,
 //这是一个CAS操作过程,直到设置成功方能退出循环,返回旧值
 public final int getAndSetInt(Object o, long offset, int newValue) {
     int v;
     do {
         v = getIntVolatile(o, offset);
     } while (!compareAndSwapInt(o, offset, v, newValue));
     return v;
 }

// 1.8新增,同上,操作的是long类型
 public final long getAndSetLong(Object o, long offset, long newValue) {
     long v;
     do {
         v = getLongVolatile(o, offset);
     } while (!compareAndSwapLong(o, offset, v, newValue));
     return v;
 }

 //1.8新增,同上,操作的是引用类型数据
 public final Object getAndSetObject(Object o, long offset, Object newValue) {
     Object v;
     do {
         v = getObjectVolatile(o, offset);
     } while (!compareAndSwapObject(o, offset, v, newValue));
     return v;
 }
1.2.8 Unsafe类提供线程挂起与恢复操作支持

将一个线程进行挂起是通过park方法实现的,调用park后,线程将一直阻塞直到超时或者中断等条件出现。unpark可以终止一个挂起的线程,使其恢复正常。Java对线程的挂起操作被封装在LockSupport类中,LockSupport类中有各种版本pack方法,其底层实现最终还是使用Unsafe.park()方法和Unsafe.unpark()方法来实现。

//线程调用该方法,线程将一直阻塞直到超时,或者是中断条件出现。  
public native void park(boolean isAbsolute, long time);  

//终止挂起的线程,恢复正常.java.util.concurrent包中挂起操作都是在LockSupport类实现的,其底层正是使用这两个方法,  
public native void unpark(Object thread); 
1.2.9 内存屏障:等操作的内存屏障定义支持
//在该方法之前的所有读操作,一定在load屏障之前执行完成
public native void loadFence();

//在该方法之前的所有写操作,一定在store屏障之前执行完成
public native void storeFence();

//在该方法之前的所有读写操作,一定在full屏障之前执行完成,这个内存屏障相当于上面两个的合体功能
public native void fullFence();
//获取持有锁,已不建议使用
@Deprecated
public native void monitorEnter(Object var1);

//释放锁,已不建议使用
@Deprecated
public native void monitorExit(Object var1);

//尝试获取锁,已不建议使用
@Deprecated
public native boolean tryMonitorEnter(Object var1);

//获取本机内存的页数,这个值永远都是2的幂次方  
public native int pageSize();  

//告诉虚拟机定义了一个没有安全检查的类,默认情况下这个类加载器和保护域来着调用者类  
public native Class defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);  

//加载一个匿名类
public native Class defineAnonymousClass(Class hostClass, byte[] data, Object[] cpPatches);

//判断是否需要加载一个类
public native boolean shouldBeInitialized(Class c);

//确保类一定被加载 
public native  void ensureClassInitialized(Class c);
二、常用的Atomic源码分析

通过前面部分的分析我们应该基本理解CAS无锁思想以及对魔法类Unsafe有较全面的认知,而这些也是我们分析atomic包的基本条件,那么我们接下来再一步步分析CAS在Java中的应用。JDK5之后推出的JUC并发包中提供了java.util.concurrent.atomic原子包,在该包下提供了大量基于CAS实现的原子操作类,如以后不想对程序代码加锁但仍然想避免线程安全问题,那么便可以使用该包下提供的类,原子包提供的操作类主要可分为如下四种类型:

2.1 基本类型原子操作类

Atomic包中提供的对于基本类型的原子操作类分别为AtomicInteger、AtomicBoolean、AtomicLong三个,它们的底层实现方式及使用方式是一致的,所以我们只分析其中一个AtomicInteger用作举例,AtomicInteger主要是针对int类型的数据执行原子操作,它提供了原子自增方法、原子自减方法以及原子赋值方法等API:

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 {
           //通过unsafe类的objectFieldOffset()方法,获取value变量在对象内存中的偏移
           //通过该偏移量valueOffset,unsafe类的内部方法可以获取到变量value对其进行取值或赋值操作
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
   //当前AtomicInteger封装的int变量值 value
    private volatile int value;

    public AtomicInteger(int initialValue) {
        value = initialValue;
    }
    public AtomicInteger() {
    }
   //获取当前最新值,
    public final int get() {
        return value;
    }
    //设置当前值,具备volatile效果,方法用final修饰是为了更进一步的保证线程安全
    public final void set(int newValue) {
        value = newValue;
    }
    //最终会设置成newValue,使用该方法后可能导致其他线程在之后的一小段时间内可以获取到旧值,有点类似于延迟加载
    public final void lazySet(int newValue) {
        unsafe.putOrderedInt(this, valueOffset, newValue);
    }
   //设置新值并获取旧值,底层调用的是CAS操作即unsafe.compareAndSwapInt()方法
    public final int getAndSet(int newValue) {
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }
   //如果当前值为expect,则设置为update(当前值指的是value变量)
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    //当前值自增加1,返回旧值,底层CAS操作
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    //当前值自减扣1,返回旧值,底层CAS操作
    public final int getAndDecrement() {
        return unsafe.getAndAddInt(this, valueOffset, -1);
    }
   //当前值增加delta,返回旧值,底层CAS操作
    public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }
    //当前值自增加1并返回自增后的新值,底层CAS操作
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    //当前值自减扣1并返回自减之后的新值,底层CAS操作
    public final int decrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
    }
   //当前值增加delta,返回新值,底层CAS操作
    public final int addAndGet(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
    }
   //省略一些不常用的方法....
}

从上面的源码中我们可以得知,AtomicInteger原子类的所有方法中并没有使用到任何互斥锁机制来实现同步,而是通过我们前面介绍的Unsafe类提供的CAS操作来保障的线程安全

基本类型的原子操作类用法

public class AtomicIntegerDemo {
    // 创建共享变量 atomicI
    static AtomicInteger atomicI = new AtomicInteger();

    public static class AddThread implements Runnable{
        public void run(){
           for(int i = 0; i < 10000; i++)
               atomicI.incrementAndGet();
        }

    }
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        //开启5条线程同时执行atomicI的自增操作
        for(int i = 1; i  fieldt = field.getType();
        // 判断是否为int类型
        if (fieldt != int.class)
            throw new IllegalArgumentException("Must be integer type");
        // 判断是否被volatile修饰
        if (!Modifier.isVolatile(modifiers))
            throw new IllegalArgumentException("Must be volatile type");

        this.cclass = (Modifier.isProtected(modifiers) &&
                       caller != tclass) ? caller : null;
        this.tclass = tclass;
        // 获取该字段的在对象内存的偏移量,通过内存偏移量可以获取或者修改该字段的值
        offset = unsafe.objectFieldOffset(field);
    }
}

从AtomicIntegerFieldUpdaterImpl源码中不难看出其实字段更新器都是通过反射机制+Unsafe类实现的,看看AtomicIntegerFieldUpdaterImpl中自增方法incrementAndGet实现:

public int incrementAndGet(T obj) {
    int prev, next;
    do {
        prev = get(obj);
        next = prev + 1;
        // 调用下面的compareAndSet方法完成cas操作
    } while (!compareAndSet(obj, prev, next));
    return next;
}

// 最终调用的还是Unsafe类中compareAndSwapInt()方法完成
public boolean compareAndSet(T obj, int expect, int update) {
        if (obj == null || obj.getClass() != tclass || cclass != null) fullCheck(obj);
        return unsafe.compareAndSwapInt(obj, offset, expect, update);
}
2.5 CAS的ABA问题的解决方案

那么当我们出现这类问题并且需要我们去关注时又该如何解决呢?在我们通过其他形式去实现乐观锁时通常会通过version版本号来进行标记从而避免并发带来的问题,而在Java中解决CAS的ABA问题主要有两种方案:

  • AtomicStampedReference:时间戳控制,能够完全解决
  • AtomicMarkableReference:维护boolean值控制,不能完全杜绝

AtomicStampedReference是一个带有时间戳的对象引用,内部通过包装Pair对象键值对的形式来存储数据与时间戳,在每次更新时,先对数据本身和时间戳进行比对,只有当两者都符合预期值时才调用Unsafe的compareAndSwapObject方法进行写入。当然,AtomicStampedReference不仅会设置新值而且还会记录更改的时间戳。这就解决了之前CAS机制带来的ABA问题。简单案例如下:

public class ABAIssue {
    // 定义原子计数器,初始值 = 100
    private static AtomicInteger atomicI = new AtomicInteger(100);
    // 定义AtomicStampedReference:初始化时需要传入一个初始值和初始时间
    private static AtomicStampedReference asRef = new AtomicStampedReference(100, 0);

    /**
     * 未使用AtomicStampedReference线程组:TA TB
     */
    private static Thread TA = new Thread(() -> {
        System.err.println("未使用AtomicStampedReference线程组:[TA TB] >>>>");
        // 更新值为101
        boolean flag = atomicI.compareAndSet(100, 101);
        System.out.println("线程TA:100 -> 101.... flag:" + flag + ",atomicINewValue:" + atomicI.get());
        // 更新值为100
        flag = atomicI.compareAndSet(101, 100);
        System.out.println("线程TA:101 -> 100.... flag:" + flag + ",atomicINewValue:" + atomicI.get());
    });
    private static Thread TB = new Thread(() -> {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        boolean flag = atomicI.compareAndSet(100, 888);
        System.out.println("线程TB:100 -> 888.... flag:" + flag + ",atomicINewValue:" + atomicI.get() + "\n\n");
    });

    /**
     *  使用AtomicStampedReference线程组:T1 T2
     */
    private static Thread T1 = new Thread(() -> {
        System.err.println("使用AtomicStampedReference线程组:[T1 T2] >>>>");
        // 更新值为101
        boolean flag = asRef.compareAndSet(100, 101, asRef.getStamp(), asRef.getStamp() + 1);
        System.out.println("线程T1:100 -> 101.... flag:" + flag + ".... asRefNewValue:" + asRef.getReference() + ".... 当前Time:" + asRef.getStamp());
        // 更新值为100
        flag = asRef.compareAndSet(101, 100, asRef.getStamp(), asRef.getStamp() + 1);
        System.out.println("线程T1:101 -> 100.... flag:" + flag + ".... asRefNewValue:" + asRef.getReference() + ".... 当前Time:" + asRef.getStamp());
    });
    private static Thread T2 = new Thread(() -> {
        int time = asRef.getStamp();
        System.out.println("线程休眠前Time值:" + time);
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        boolean flag = asRef.compareAndSet(100, 888, time, time + 1);

        System.out.println("线程T2:100 -> 888.... flag:" + flag + ".... asRefNewValue:" + asRef.getReference() + ".... 当前Time:" + asRef.getStamp());
    });

    public static void main(String[] args) throws InterruptedException {
        TA.start();
        TB.start();
        TA.join();
        TB.join();

        T1.start();
        T2.start();
    }
}

/**
 * 未使用AtomicStampedReference线程组:[TA TB] >>>>
 * 线程TA:100 -> 101.... flag:true,atomicINewValue:101
 * 线程TA:101 -> 100.... flag:true,atomicINewValue:100
 * 线程TB:100 -> 888.... flag:true,atomicINewValue:888
 *
 *
 * 使用AtomicStampedReference线程组:[T1 T2] >>>>
 * 线程休眠前Time值:0
 * 线程T1:100 -> 101.... flag:true.... asRefNewValue:101.... 当前Time:1
 * 线程T1:101 -> 100.... flag:true.... asRefNewValue:100.... 当前Time:2
 * 线程T2:100 -> 888.... flag:false.... asRefNewValue:100.... 当前Time:2
 */

我们观察如上Demo中AtomicInteger与AtomicStampedReference的测试结果可以得知,AtomicStampedReference确实能够解决我们在之前阐述的ABA问题,那么AtomicStampedReference究竟是如何ABA问题的呢?我们接下来再看看它的内部实现:

public class AtomicStampedReference {
    // 通过Pair内部类存储数据和时间戳
    private static class Pair {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static  Pair of(T reference, int stamp) {
            return new Pair(reference, stamp);
        }
    }
    // 存储数值和时间的内部类
    private volatile Pair pair;

    // 构造方法:初始化时需传入初始值和时间初始值
    public AtomicStampedReference(V initialRef, int initialStamp) {
        pair = Pair.of(initialRef, initialStamp);
    }
}
public boolean compareAndSet(V expectedReference,
                         V newReference,
                         int expectedStamp,
                         int newStamp) {
    Pair current = pair;
    return
        expectedReference == current.reference &&
        expectedStamp == current.stamp &&
        ((newReference == current.reference &&
          newStamp == current.stamp) ||
         casPair(current, Pair.of(newReference, newStamp)));
}

// 最终实现调用Unfase类中的compareAndSwapObject()方法
private boolean casPair(Pair cmp, Pair val) {
    return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}

在compareAndSet方法源码实现中我们不难发现,它其中同时对当前数据和当前时间进行比较,只有两者都相等是才会执行casPair()方法,而casPair()方法也是一个原子性方法,最终实现调用的还是Unsafe类中的compareAndSwapObject()方法。

AtomicMarkableReference与我们之前所探讨的AtomicStampedReference并不相同,AtomicMarkableReference只能在一定程度上减少ABA问题的出现,它并不能完全的杜绝ABA问题。因为AtomicMarkableReference内部维护的是boolean类型的标识,也就代表着它并不像之前的AtomicStampedReference内部的维护的时间戳一样值会不断的递增,而AtomicMarkableReference内部因为维护的是boolean,也就代表着只会在true与false两种状态之间来回切换,所以还是存在ABA问题出现的概念

锁升级膨胀过程中也多次提到。而无锁也在有些地方被称作自旋锁,自旋是一种如果出现线程竞争,但是此处的逻辑线程执行起来非常快,某些没有获取到资源(数值等)的线程也能在不久后的将来获取到资源并执行时,OS让没有获取到资源的线程执行几个空循环的操作等待资源的情况。而自旋这种操作也的确可以提升效率,因为自旋锁只是在逻辑上阻塞了线程,在用户态的情况下让线程停止了执行,并没有真正意义上在内核态中挂起对应的内核线程,从而可以减少很多内核挂起/放下线程耗费的资源。但问题是当线程越来越多竞争很激烈时,占用CPU的时间变长会导致性能急剧下降,因此Java虚拟机内部一般对于自旋锁有一定的次数限制,可能是50或者100次循环后就放弃,直接让OS挂起内核线程,让出CPU资源。

博文参考

《深入理解JVM虚拟机》

《Java并发编程之美》

《Java高并发程序设计》

《亿级流量网站架构核心技术》

《Java并发编程实战》

Java中atomic包中的原子操作类总结 - 掘金

关注
打赏
1657692713
查看更多评论
立即登录/注册

微信扫码登录

0.0403s