关于多线程:AtomicStampedReference源码分析

50次阅读

共计 4487 个字符,预计需要花费 12 分钟才能阅读完成。

    欢送大家搜寻“小猴子的技术笔记”关注我的公众号,文章实时同步。有问题能够及时和我交换。

    之前的文章曾经介绍过 CAS 的操作原理,它尽管可能保证数据的原子性,但还是会有一个 ABA 的问题。

    那么什么是 ABA 的问题呢?假如有一个共享变量“num”, 有个线程 A 在第一次进行批改的时候把 num 的值批改成了 33。批改胜利之后,紧接着又立即把“num”的批改回了 22。另外一个线程 B 再去批改这个值的时候并不能感知到这个值被批改过。

    换句话说,他人把你账户外面的钱拿进去去投资,在你发现之前又给你还了回去,那这个钱还是原来的那个钱吗?你老婆出轨之后又回到了你身边,还是你原来的那个老婆吗?

    为了模仿 ABA 的问题,我启动了两个线程拜访一个共享的变量。将上面的代码拷贝到编译器中,运行进行测试:

public class ABATest {private final static AtomicInteger num = new AtomicInteger(100);

    public static void main(String[] args) {new Thread(() -> {num.compareAndSet(100, 101);
            num.compareAndSet(101, 100);
            System.out.println(Thread.currentThread().getName() + "批改 num 之后的值:" + num.get());
        }).start();

        new Thread(() -> {
            try {TimeUnit.SECONDS.sleep(3);
                num.compareAndSet(100, 200);
                System.out.println(Thread.currentThread().getName() + "批改 num 之后的值:" + num.get());
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }).start();}
}


    第一个线程先进行批改把数值从 100 批改为 101,而后在从 101 批改回 100,这个过程其实是发成了 ABA 的操作。第二个线程期待 3 秒(为了是让第一个线程执行结束,第二个线程在执行)之后进行值从 100 批改为 200。依照咱们的了解,第一个线程曾经批改过原来的值了,那么第二个线程就不应该批改胜利。然而如果你运行上面的测试用例的话,你会发现它是能够进行批改胜利的,请看运行后果:


Thread-0 批改 num 之后的值:100
Thread-1 批改 num 之后的值:200

    尽管后果是合乎咱们的预期的:数值被胜利地进行了批改,然而批改的过程却是不合乎咱们的预期的。

    为了解决这个问题,咱们能够在批改的时候附加上一个版本号,也就是第几次批改。每次批改的时候把版本号带上,如果版本号可能对应的上的话就进行批改,如果对应不上的话就不容许进行批改。

    所以如果批改的时候带上的版本号不统一的话是不可能进行胜利批改的。咱们能够依照下面的原理本人进行版本号的封装,但兴许会比拟麻烦。因而咱们能够应用 JDK 给咱们提供的一个曾经封装好的类“AtomicStampedReference”来进行咱们数据的更新。咱们来看看上面的这些例子:

public class AtomicStampedReferenceTest {private final static AtomicStampedReference<Integer> stamp = new AtomicStampedReference<>(100, 1);

    public static void main(String[] args) {new Thread(() -> {System.out.println(Thread.currentThread().getName() + "第 1 次版本号:" + stamp.getStamp());
            stamp.compareAndSet(100, 200, stamp.getStamp(), stamp.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "第 2 次版本号:" + stamp.getStamp());
            stamp.compareAndSet(200, 100, stamp.getStamp(), stamp.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "第 2 次版本号:" + stamp.getStamp());
        }).start();

        new Thread(() -> {
            try {TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName() + "第 1 次版本号:" + stamp.getStamp());
                stamp.compareAndSet(100, 400, stamp.getStamp(), stamp.getStamp() + 1);
                System.out.println(Thread.currentThread().getName() + "获取到的值:" + stamp.getReference());
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }).start();}
}
Thread-0 第 1 次版本号:1
Thread-0 第 2 次版本号:2
Thread-0 第 2 次版本号:2
Thread-1 第 1 次版本号:2
Thread-1 获取到的值:200

    也是启动了两个线程对共享变量进行批改,然而这次不同的是带着版本号对共享变量进行的批改。上面将下面的例子进行拆解剖析,钻研下“AtomicStampedReference”到底为咱们做了一些什么。

    首先剖析共享变量的创立:构建了一个“AtomicStampedReference”对象,并且显示的赋值了 100 和 1。

private final static AtomicStampedReference<Integer> stamp = new AtomicStampedReference<>(100, 1);

    结构函数调用了上面的源码:


public AtomicStampedReference(V initialRef, int initialStamp) {pair = Pair.of(initialRef, initialStamp);
}

    ”initialRef” 是初始值,也就是咱们定义的 100,“initialStamp”是咱们显示申明的一个整形类型的版本号。只有在 int 的范畴内即可,然而不要太大了,毕竟是 int 如果超了就会失落精度问题。

    而后调用了“Pair.of(initialRef, initialStamp)”,持续跟进源码查看:

    通过观察源码能够发现类“Pair”是“AtomicStampedReference”类的一个动态外部类,有两个参数的构造函数,而后把咱们传递进来的初始值和版本号进行赋值给“Pair”对象。能够留神到“pair”被关键字“volatile”润饰,也就保障了内存的可见性和禁止指令的重排序。因而如果“pair”产生了变动,那么所有持有其援用的信息都会进行相应的数据更新。

    到此为止,“AtomicStampedReference”对象初始化结束,外部蕴含了一个“reference”值为 100,“stamp”为 1 的“pair”动态外部类。

“stamp.getStamp()”目标是为了获取以后的版本号,咱们在初始化的时候显示设置了一个值 1,因而第一次获取到的版本号就是 1。


 public int getStamp() {return pair.stamp;}

“stamp.compareAndSet(100, 200, stamp.getStamp(), stamp.getStamp() + 1);”是进行第一次 CAS 更新数据,这次更新的时候就带着版本号去更新了。

new Thread(() -> {System.out.println(Thread.currentThread().getName() + "第 1 次版本号:" + stamp.getStamp());
    stamp.compareAndSet(100, 200, stamp.getStamp(), stamp.getStamp() + 1);
    System.out.println(Thread.currentThread().getName() + "第 2 次版本号:" + stamp.getStamp());
    stamp.compareAndSet(200, 100, stamp.getStamp(), stamp.getStamp() + 1);
    System.out.println(Thread.currentThread().getName() + "第 2 次版本号:" + stamp.getStamp());
}).start();

    还记得吗?之前的 CAS 比拟是须要传递一个期望值和更新的值(内存中的值,底层的办法会给咱们封装好):

num.compareAndSet(100, 101);

    而带着版本号的 CAS 须要咱们传递四个值,一个是期望值,一个是更新的值,还有两个就是冀望的工夫戳和须要更新的工夫戳:


V   expectedReference // 示意预期值
V   newReference,     // 示意要更新的值
int expectedStamp,    // 示意预期的工夫戳
int newStamp          // 示意要更新的工夫戳 

    之后进行了预期值的判断,预期工夫戳的判断,要更新的值和以后的值如果一样的话,并且要更新的版本号和以后的版本号一样的话就返回胜利。


private boolean casPair(Pair<V> cmp, Pair<V> val) {return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}

    这里咱们会发现在“compareAndSet”办法中最初还调用了“casPair”办法,从名字就能够看到,次要是应用 CAS 机制更新新的值 reference 和工夫戳 stamp。而最终调用的底层是一个本地的办法对数据进行的批改。

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

    对于须要本人进行 CAS 解决的中央,咱们能够应用“AtomicStampedReference<V>”来进行数据的解决。它既反对泛型,同时还能够防止传统 CAS 中 ABA 的问题,使数据更加平安。

    欢送大家搜寻“小猴子的技术笔记”关注我的公众号,文章实时同步。有问题能够及时和我交换。

正文完
 0