AtomicMarkableReference、AtomicStampedReference 深入源码解析

  正如《CAS 完全解析》所讲述一样,CAS可以使用无锁的方式在多线程环境下保证数据操作的安全性,但同时会引入ABA问题,什么是ABA问题呢,大概意思就是:

  假设这里有一个变量V,有两个线程T1和T2。目前V的值是100,T1和T2并发对V进行修改,可能由于某种资源调度方面的原因,T1获得的资源比较充足,T1将V变更为200,然后又变更会100。此时,T2才获得CPU资源,T2得到V的值,T2发现V的值还是100,认为其没有变化,CAS比较通过,进行数据修改。这里就引发了经典的ABA的问题。V的值实际是经过100-200-100的变化过程的,但是T2无法识别这个过程。当然这在某些应用场景下是没有影响的,但是在另外一些场景下,是需要刻意避免的,比如关于账户的金额等敏感信息。

  JUC包内提供了两个类AtomicMarkableReference和AtomicStampedReference来解决这个问题,两个类实现思路是一样的,都是为对象附加一个版本戳,对象改变时,版本戳也随之改变,达到标识对象变化的目的。

  AtomicMarkableReference和AtomicStampedReference区别:

  · AtomicMarkableReference使用boolean类型版本戳,AtomicStampedReference使用int类型版本戳。

  · AtomicMarkableReference想要表达的含义是对象是否被更改过,而不关心被更改的次数,无论一次或多次。AtomicStampedReference想要表达的含义是对象被更改过几次。

  演示示例:

  首先,新建一个线程类,负责模拟多线程环境下,对AtomicMarkableReference和AtomicStampedReference变量进行操作。

package com.securitit.serialize.atomics;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicMarkableReference;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicReferenceArray;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import java.util.concurrent.atomic.AtomicStampedReference;

public class AtomicMarkableStampedReferenceThread extends Thread {
	// AtomicMarkableReference实例.
	private AtomicMarkableReference<String> atomicMarkableReference = null;
	// AtomicStampedReference实例.
	private AtomicStampedReference<String> atomicStampedReference = null;
	// CountDownLatch可以进行线程计数,到达指定计数时,可唤醒调用await方法的线程.
	private CountDownLatch countDownLatch;

	public AtomicMarkableStampedReferenceThread(AtomicMarkableReference<String> atomicMarkableReference,
			AtomicStampedReference<String> atomicStampedReference, CountDownLatch countDownLatch) {
		this.atomicMarkableReference = atomicMarkableReference;
		this.atomicStampedReference = atomicStampedReference;
		this.countDownLatch = countDownLatch;
	}

	@Override
	public void run() {
		// 将atomicMarkableReference修改为200.
		if (atomicMarkableReference.compareAndSet("100", "200", false, true)) {
			System.out.println("AtomicMarkableReference 更新成功.");
		}
		// 将atomicStampedReference修改为200.
		if (atomicStampedReference.compareAndSet("100", "200", atomicStampedReference.getStamp(),
				atomicStampedReference.getStamp() + 1)) {
			System.out.println("AtomicStampedReference 更新成功.");
		}
		countDownLatch.countDown();
	}

}

  然后,我们写测试类,在测试类中开启4个线程同时进行操作:

package com.securitit.serialize.atomics;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicMarkableReference;
import java.util.concurrent.atomic.AtomicStampedReference;

public class AtomicMarkableStampedReferenceTester {
	// AtomicMarkableReference实例.
	private static AtomicMarkableReference<String> atomicMarkableReference = null;
	// AtomicStampedReference实例.
	private static AtomicStampedReference<String> atomicStampedReference = null;
	// CountDownLatch可以进行线程计数,到达指定计数时,可唤醒调用await方法的线程.
	private static CountDownLatch countDownLatch = new CountDownLatch(4);

	public static void main(String[] args) throws Exception {
		String src = "100";
		// 初始化atomicMarkableReference/atomicStampedReference.
		atomicMarkableReference = new AtomicMarkableReference<String>(src, false);
		atomicStampedReference = new AtomicStampedReference<String>(src, 1);
		// 启动4个线程.
		new AtomicMarkableStampedReferenceThread(atomicMarkableReference, atomicStampedReference, countDownLatch)
				.start();
		new AtomicMarkableStampedReferenceThread(atomicMarkableReference, atomicStampedReference, countDownLatch)
				.start();
		new AtomicMarkableStampedReferenceThread(atomicMarkableReference, atomicStampedReference, countDownLatch)
				.start();
		new AtomicMarkableStampedReferenceThread(atomicMarkableReference, atomicStampedReference, countDownLatch)
				.start();
		// 等待线程执行完毕.
		countDownLatch.await();
		// 输出当前结果.
		System.out.println("AtomicMarkableReference: " + atomicMarkableReference.getReference());
		System.out.println("AtomicStampedReference: " + atomicStampedReference.getReference());
	}

}

  输出结果:

AtomicMarkableReference 更新成功.
AtomicStampedReference 更新成功.
AtomicMarkableReference: 200
AtomicStampedReference: 200

  从上面的输出结果可以,在4个线程中,对于AtomicMarkableReference和AtomicStampedReference都只有一个线程修改成功,这种情况保证了即使在多线程环境下,也可以追踪数据的变换轨迹,在某些业务场景下,规避出现ABA问题。

  源码分析:

  实现基础:

  AtomicMarkableReference:

// 对象与版本戳存储.
private volatile Pair<V> pair;
// 绑定对象与版本戳内部类.
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, boolean mark) {
		return new Pair<T>(reference, mark);
	}
}

  AtomicStampedReference:

// 对象与版本戳存储.
private volatile Pair<V> pair;
// 绑定对象与版本戳内部类.
private static class Pair<T> {
	final T reference;
    // 版本戳.
	final int stamp;
	private Pair(T reference, int stamp) {
		this.reference = reference;
		this.stamp = stamp;
	}
    // 提供获取实例方法.
	static <T> Pair<T> of(T reference, int stamp) {
		return new Pair<T>(reference, stamp);
	}
}

  AtomicMarkableReference和AtomicStampedReference通过内部类Pair将数据与版本戳绑定在一起,AtomicMarkableReference绑定了boolean类型的版本标记,AtomicStampedReference绑定了int类型的版本戳。

​  基本方法:

  AtomicMarkableReference:

// 构造方法:传入对象和版本标记.
public AtomicMarkableReference(V initialRef, boolean initialMark);
// 获取对象.
public V getReference();
// 获取版本标记.
public boolean isMarked();
// 通过获得对象和版本标记.对象作为返回值,版本标记会写入数组中,调用处可获取.
public V get(boolean[] markHolder);
// 通过CAS方式更新对象和版本标记值.
// 需满足条件:
// · 期望值和Pair<T>中的对象值相等.
// · 期望标记和Pair<T>中的标记值相等.
// · 新值和Pair<T>中对象值相等&&新标记值和Pair<T>中的标记值相等.
//	 或.
//	 通过UNSAFE.compareAndSwapObject新值.
public boolean compareAndSet(V expectedReference, V newReference, boolean expectedMark,
boolean newMark);
// 直接调用方法compareAndSet.在这里两者是没有差别的.
public boolean weakCompareAndSet(V expectedReference, V newReference, boolean expectedMark, boolean newMark);
// 设置新对象值和标记值.
public void set(V newReference, boolean newMark);
// 如果当前引用 == 预期引用,则以原子方式将该标记的值设置为给定的更新值.
public boolean attemptMark(V expectedReference, boolean newMark);

​  AtomicStampedReference:

// 构造方法.传入对象和版本戳.
public AtomicStampedReference(V initialRef, int initialStamp);
// 获取对象.
public V getReference();
// 获取版本戳.
public int getStamp();
// 同时获取对象和版本戳.版本戳会写入数组中,调用处可获取.
public V get(int[] stampHolder);
// 通过CAS方式更新对象和版本戳.
// 需满足条件:
// · 期望值和Pair<T>中的对象值相等.
// · 期望版本戳和Pair<T>中的版本戳相等.
// · 新值和Pair<T>中对象值相等&&新版本戳和Pair<T>中的版本戳相等.
//	 或.
//	 通过UNSAFE.compareAndSwapObject新值.
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp);
// 直接调用方法compareAndSet.在这里两者是没有差别的.
public boolean weakCompareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp);
// 设置新对象值和标记值.
public void set(V newReference, int newStamp);
// 如果当前引用 == 预期引用,则以原子方式将该标记的值设置为给定的更新值.
public boolean attemptStamp(V expectedReference, int newStamp);

  AtomicMarkableReference和AtomicStampedReference源码不是很复杂,API相对来说比较少,两者除标记值和版本戳的其他内容差别不大。

  在源码学习的过程中,除了分析源码的来龙去脉,也要掌握源码的设计思想,比如标记值和版本戳的应用,可以在很多场景下。

​  注:文中源码均来自于JDK1.8版本,不同版本间可能存在差异。

  如果有哪里有不明白或不清楚的内容,欢迎留言哦!

猜你喜欢

转载自blog.csdn.net/securitit/article/details/106834664
今日推荐