并发编程原子类完整教程: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。
评论区