侧边栏壁纸
博主头像
孔子说JAVA博主等级

成功只是一只沦落在鸡窝里的鹰,成功永远属于自信且有毅力的人!

  • 累计撰写 285 篇文章
  • 累计创建 125 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

Java并发编程之原子操作类AtomicMarkableReference实战详解

孔子说JAVA
2022-06-12 / 0 评论 / 0 点赞 / 77 阅读 / 10,207 字 / 正在检测是否收录...

并发编程原子类完整教程:Java并发编程之原子操作类实战教程

AtomicMarkableReference 是java.util.concurrent(简写为JUC)包下的类,和 AtomicStampedReference 等类是一样(这些类也是JUC下的),也是基于CAS无锁理论实现的,AtomicMarkableReference 原子类也可以解决A-B-A问题,与 AtomicStampedReference 不同的是它不是维护一个版本号,而是维护一个boolean类型的标记,用法没有AtomicStampedReference灵活。因此也只是在特定的场景下使用。该类提供了对象引用的非阻塞原子性读写操作,并且提供了其他一些高级的用法。

  • 因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。这就是A-B-A问题。
  • AtomicMarkableReference 作⽤是对”对象”进⾏原⼦操作,提供了⼀种读和写都是原⼦性的对象引⽤变量。原⼦意味着多个线程试图改变同
    ⼀个 AtomicMarkableReference (例如⽐较和交换操作) 不会使 AtomicMarkableReference 处于不⼀致的状态。
  • AtomicStampedReference用法参考:Java并发编程之原子操作类AtomicStampedReference实战详解

1、A-B-A问题

ABA问题描述

因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,就是说一个线程把数据A变为了B,然后又重新变成了A。此时另外一个线程读取的时候,使用CAS进行检查时会发现它的值没有发生变化,就误以为是原来的那个A,但是实际上却变化了。这就是有名的A-B-A问题。

ABA问题的后果

ABA问题会带来什么后果呢?我们来举个例子。

一个小偷,把别人家的钱偷了之后又还回去了,钱还是原来的钱吗?且小偷也做了违法的事情。ABA问题也一样,如果不好好解决就会带来大量的问题。最常见的就是资金问题,也就是别人如果挪用了你的钱,在你发现之前又还了回来。但是别人却已经触犯了法律。

再举个例子:

你倒了一杯水放桌子上,干了点别的事,然后同事把你水喝了又给你重新倒了一杯水,你回来看水还在,拿起来就喝,如果你不管水中间是否被人喝过,只关心水还在,这就是ABA问题。如果你是一个讲卫生讲文明的人,不但关心水在不在,还关心在你离开的时候水有没有被人动过,因为你是程序员,所以就想起了放了张纸在旁边,写上初始值0,别人喝水前麻烦先做个累加才能喝水。

ABA问题的解决思路

ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。AtomicStampedReference 就是类似的原理。

2、AtomicMarkableReference 主要方法

2.1 构造方法

AtomicMarkableReference 在构建的时候需要一个布尔类型的修改标识,每一次针对共享数据的变化都会导致该 initialMark 的变化,因为布尔类型的值只有2个,所以用法没有 AtomicStampedReference 灵活。因此也只是在特定的场景下使用。

/**
 *  初始化,构造成一个 Pair 对象,由于 pair 是用 volatile 修饰的所以在构造是线程安全的
 * @param initialRef 初始化变量引用
 * @param initialMark 修改标识
 */
public AtomicMarkableReference(V initialRef, boolean initialMark);

2.2 主要方法

// 以原子方式获取当前引用值
public V getReference()
 
// 以原子方式获取当前标记值
public int isMarked()
 
// 以原子方式获取当前引用值和标记值
public V get(boolean[] markHolder)
 
// 以原子的方式同时更新引用值和标记值
// 当期望引用值不等于当前引用值时,操作失败,返回false
// 当期望标记值不等于当前标记值时,操作失败,返回false
// 在期望引用值和期望标记值同时等于当前值的前提下
// 当新的引用值和新的标记值同时等于当前值时,不更新,直接返回true
// 当新的引用值和新的标记值不同时等于当前值时,同时设置新的引用值和新的标记值,返回true
public boolean weakCompareAndSet(V  expectedReference,
                                 V  newReference,
                                 boolean expectedMark,
                                 boolean newMark)
// 以原子的方式同时更新引用值和标记值
// 当期望引用值不等于当前引用值 或 期望标记值不等于当前标记值时,操作失败,返回false
// 如果当前引用 == 预期引用,并且当前标记值不等于预期标记值不,那么以原子方式将引用和标记的值设置为给定的更新值。
// 在期望引用值和期望标记值同时等于当前值的前提下,当新的引用值和新的标记值同时等于当前值时,不更新,直接返回true
// 当新的引用值和新的标记值不同时等于当前值时,同时设置新的引用值和新的标记值,返回true
public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             boolean expectedMark,
                             boolean newMark)
 
// 以原子方式设置引用的当前值为新值newReference
// 同时,以原子方式设置标记值的当前值为新值newMark
// 新引用值和新标记值只要有一个跟当前值不一样,就进行更新
public void set(V newReference, boolean newMark)
 
// 以原子方式设置标记值为新的值
// 前提:引用值保持不变
// 当期望的引用值与当前引用值不相同时,操作失败,返回fasle
// 如果当前引用 == 预期引用,则以原子方式将该标记的值设置为给定的更新值,操作成功,返回true
public boolean attemptMark(V expectedReference, boolean newMark)
 
// 使用`sun.misc.Unsafe`类原子地交换两个对象
private boolean casPair(Pair<V> cmp, Pair<V> val)

3、AtomicStampedReference 演示示例

3.1 演示示例1

本示例演示 AtomicMarkableReference 的基础用法。

import java.util.concurrent.atomic.AtomicMarkableReference;
import org.junit.Test;
 
/**
 * AtomicMarkableReference 用于标记一个对象的引用,通过静态内部类Pair的方式记录
 */
public class AtomicMarkableReferenceTest {
    /**
     * 构造函数,参数一为要标记的对象引用
     * 参数二为标记布尔值
     * toString 返回ObjectclassName@hashCode()
     * void
     * @throws
     */
    @Test
    public void testConstruct0()throws Exception{
        AtomicMarkableReferenceTest test=new AtomicMarkableReferenceTest();
        AtomicMarkableReference testObj=new AtomicMarkableReference(test,true);
        System.out.println(testObj.toString());
    }
    
    /**
     * 获取引用对象
     * @throws
     */
    @Test
    public void testGetReference(){
        AtomicMarkableReferenceTest test=new AtomicMarkableReferenceTest();
        AtomicMarkableReference testObj=new AtomicMarkableReference(test,true);
        System.out.println(testObj.getReference());
    }
    
    /**
     * 获取引用对象标记值
     * @throws
     */
    @Test
    public void testIsMarked(){
        AtomicMarkableReferenceTest test=new AtomicMarkableReferenceTest();
        AtomicMarkableReference testObj=new AtomicMarkableReference(test,true);
        System.out.println(testObj.isMarked());
    }
    
    /**
     * 获取引用对象标记值
     * @throws
     */
    @Test
    public void testGet(){
        AtomicMarkableReferenceTest test=new AtomicMarkableReferenceTest();
        AtomicMarkableReference testObj=new AtomicMarkableReference(test,false);
        boolean[] booleans=new boolean[]{true,false};
        System.out.println(testObj.get(booleans));
        System.out.println();
    }
        
    /**
     * CompareAndSet和WeakCompareAndSet相同
     * 将引用对象根据期望的值判断,true则设置成新值和新标记值
     * false则不设置
     * @throws
     */
    @Test
    public void testWeakCompareAndSet(){
        AtomicMarkableReferenceTest test=new AtomicMarkableReferenceTest();
        AtomicMarkableReferenceTest test1=new AtomicMarkableReferenceTest();
        AtomicMarkableReference testObj=new AtomicMarkableReference(test,true);
        System.out.println(testObj.weakCompareAndSet(test,test1,true,false));
        System.out.println(testObj.getReference());
        System.out.println(testObj.isMarked());
    }
        
    /**
     * CompareAndSet和WeakCompareAndSet相同
     * 将引用对象根据期望的值判断,true则设置成新值和新标记值
     * false则不设置
     * @throws
     */
    @Test
    public void testCompareAndSet(){
        AtomicMarkableReferenceTest test=new AtomicMarkableReferenceTest();
        AtomicMarkableReferenceTest test1=new AtomicMarkableReferenceTest();
        AtomicMarkableReference testObj=new AtomicMarkableReference(test,true);
        System.out.println(testObj.compareAndSet(test,test1,true,false));
        System.out.println(testObj.getReference());
        System.out.println(testObj.isMarked());
    }
    
    /**
     * 将引用对象设置成新值和新标记值
     * @throws
     */
    @Test
    public void testSet(){
        AtomicMarkableReferenceTest test=new AtomicMarkableReferenceTest();
        AtomicMarkableReferenceTest test1=new AtomicMarkableReferenceTest();
        AtomicMarkableReference testObj=new AtomicMarkableReference(test,true);
        testObj.set(test1,false);
        System.out.println(testObj.isMarked());
    }
 
    /**
     * 改变引用对象标记值
     * @throws
     */
    @Test
    public void testAttemptMark(){
        AtomicMarkableReferenceTest test=new AtomicMarkableReferenceTest();
        AtomicMarkableReference testObj=new AtomicMarkableReference(test,true);
        testObj.attemptMark(test,false);
        System.out.println(testObj.isMarked());
    }
}

3.2 演示示例2

本示例演示 AtomicMarkableReference 的基础用法。

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicMarkableReference;

public class AtomicMarkableReferenceTest {
    private static AtomicMarkableReference atomicMarkableReference = new AtomicMarkableReference(100, false);

    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicMarkableReference.compareAndSet(100, 101, atomicMarkableReference.isMarked(), !atomicMarkableReference.isMarked());
            atomicMarkableReference.compareAndSet(101, 100, atomicMarkableReference.isMarked(), !atomicMarkableReference.isMarked());
        });

        Thread thread1 = new Thread(() -> {
            boolean marked = atomicMarkableReference.isMarked();
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean b = atomicMarkableReference.compareAndSet(100, 101, atomicMarkableReference.isMarked(), !marked);
            // true 实际应该返回false
            System.out.println(b);
        });
        thread.start();
        thread1.start();
    }
}

4、源码分析

AtomicMarkableReference 的源码比较简单,定义了一个内部类Pair,主要用来存放引用值和修改标识。下面对 AtomicMarkableReference 部分源码进行解析。

4.1 实现基础 - 内部类的定义

AtomicMarkableReference 是一个泛型类,并且没有继承任何方法和实现任何接口,通过一个内部类Pair对象来存放引用值和修改标识。

public class AtomicMarkableReference<V>{
    // 定义了一个内部类Pair,主要用来存放引用值和版本号stamp
    private static class Pair<T> {
        final T reference;  // 变量引用
        final boolean mark;  // 修改标识
        private Pair(T reference, boolean mark){
            this.reference = reference;
            this.mark = mark;
        }
        static <T> Pair<T> of(T reference, mark){
            return new Pair<T>(reference, mark);
        }
    }
}

4.2 实现基础 - 主要字段

// 当前对象,包括对象引用和版本号
private volatile Pair<T> pair;

// Unsafe是所有并发工具的基础工具类,设置为使用Unsafe.compareAndSwapInt进行更新,方法中多处需要使用unsafe变量
private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();

// 利用unsafe的本地方法来获取对象在内存的偏移地址
private static final long pairOffset = objectField(UNSAFE, "pair", AtomicMarkableReference.class);

4.3 实现基础 - 构造方法

构造方法利用 initialRef 和 initialMark 生成当前对象,包括对象引用和修改标识。由于 pair 是用 volatile 修饰的,所以构造是线程安全的。

/**
 * 构造方法
 * @param initialRef  初始变量引用
 * @param initialMark  修改标识
 */
public AtomicMarkableReference(V initialRef, boolean initialMark) {
   pair = Pair.of(initialRef, initialMark);
}

4.4 实现基础 - 主要方法

4.4.1 工具方法

//利用unsafe的CAS方法来对对象进行交换和设置,AtomicMarkableReference 内部很多方法基于此方法实现的
private boolean casPair(Pair<V> cmp, Pair<V> val){
    return PAIR.compareAndSet(this, cmp, val);
}

4.4.2 get和set方法

  • getReference()方法, 以原子方式获取当前引用值。
  • isMarked()方法, 返回标记的当前值。
  • get(boolean[] markHolder)方法,以原子方式返回引用和标记的当前值。
  • set(V newReference, boolean newMark)方法,无条件地设置引用和标记的值。
// 以原子方式获取当前引用值
public V getReference(){
    return pair.reference;
}

// 返回当前标记的布尔值
public boolean isMarked(){
    return pair.mark;
}

// 以原子方式返回引用和标记的当前值
public V get(boolean[] markHolder){
    Pair<V> pair = this.pair;
    markHolder[0] = pair.mark;
    return pair.reference;
}

// 无条件地设置引用和标记的值。
public void set(V newReference, int newStamp){
   Pair<V> current = pair;
     if (newReference != current.reference || newMark != current.mark)
        this.pair = Pair.of(newReference, newMark);
}

4.4.3 比较设置方法

比较设置方法有 compareAndSet 和 weakCompareAndSet。

  • 原子的将引用值设置为 newValue,如果旧值 == expectedValue,设置成功返回 true,否则返回 false。这个方法是支持CAS操作的,同一个时间只有一个线程调用 compareAndSet()方法。
  • compareAndSet方法,cas更新当前值和标记值。如果当前引用是==预期引用并且当前标记等于预期标记,则自动将引用和标记的值设置为给定的更新值,然后返回true,否则返回false.
  • weakCompareAndSet() 方法与 compareAndSet() 类似,但 weakCompareAndSet() 不会插入内存屏障,不能保障volatile的原子性
// cas更新当前值和标记值
public boolean compareAndSet(V expectedReference, V newReference, boolean expectedMark, boolean newMark){
    Pair<V> current = pair;
    return expectedReference == current.reference &&
        expectedMark == current.mark  &&
        ((newReference == current.reference &&
         newMark == current.mark) || 
         casPair(current, Pair.of(newReference, newMark)));
}

// cas更新当前值和标记值
public boolean weakCompareAndSet(V expectedReference, V newReference, boolean expectedMark, boolean newMark){
    return compareAndSet(expectedReference, newReference, expectedMark, newMark);
}

unsafe是通过Unsafe.getUnsafe()返回的一个Unsafe对象。通过Unsafe的CAS函数对Object引用类型数组的元素进行原子操作。如compareAndSetRaw()就是调用Unsafe的CAS函数。

4.4.4 获取标记型布尔值

该方法,主要是原子性地获取标记型布尔值,首先会比较当前引用的对象是不是expectedReference,如果是则更新版本号的值,并且返回true,否则返回false。

public boolean attemptMark(V expectedReference, boolean newMark){
    Pair<V> current = pair;
    return expectedReference == current.reference &&
        ((newMark == current.mark || casPair(current, Pair.of(expectedReference, newMark)));
}

5、总结

AtomicMarkableReference 与 AtomicStampedReference 一样也可以解决 ABA的问题,两者唯一的区别是,AtomicStampedReference 是通过 int 类型的版本号,而 AtomicMarkableReference 是通过 boolean 型的标识来判断数据是否有更改过。AtomicMarkableReference 可以理解为 AtomicStampedReference 的简化版,就是不关心修改过几次,仅仅关心是否修改过。因此变量mark是boolean类型,仅记录值是否有过修改。

  • AtomicStampedReference:带版本戳的原子引用类型,版本戳为int类型。
  • AtomicMarkableReference:带版本戳的原子引用类型,版本戳为boolean类型。
  • AtomicMarkableReference 的版本号只有true或者false两种。如果A和B两个线程,A把版本号由true修改为false,然后又由false修改为true,那么最开始B线程拿到的版本号是true,认为其他线程并没有修改资源,导致后续逻辑处理有问题。所以AtomicMarkableReference只是降低了ABA发生的概率,但不能完全避免ABA问题,优先建议选择AtomicStampedReference。
0

评论区