21. 原子类:无锁工具类的典范 - 并发工具类

public class Test {
	AtomicLong count = new AtomicLong(0);

	void add10K() {
		int idx = 0;
		while (idx++ < 10000) {
			count.getAndIncrement();
		}
	}
}

互斥锁方案为了保证互斥性,需要执行加锁、解锁操作,而加锁、解锁操作本身就消耗性能;同时拿不到锁的线程还会进入阻塞状态,进而触发线程切换,线程切换对性能的消耗也很大。 相比之下,无锁方案则完全没有加锁、解锁的性能消耗,同时还能保证互斥性,既解决了问题,又没有带来新的问题.

1. 无锁方案的实现原理

其实原子类性能高的秘密很简单,硬件支持而已。CPU 为了解决并发问题,提供了 CAS 指令(CAS,全称是 Compare And Swap,即“比较并交换”)。CAS 指令包含 3 个参数:共享变量的内存地址 A、用于比较的值 B 和共享变量的新值 C;并且只有当内存中地址 A 处的值等于 B时,才能将内存中地址 A 处的值更新为新值 C。作为一条 CPU 指令,CAS 指令本身是能够保证原子性的

模拟代码:只有当目前count的值和期望值expect相等时,才会将count更新为newValue。

class SimulatedCAS {
	int count;

	synchronized int cas(int expect, int newValue) {
		// 读目前 count 的值
		int curValue = count;
		// 比较目前 count 值是否 == 期望值
		if (curValue == expect) {
			// 如果是, 则更新 count 的值
			count = newValue;
		} // 返回写入前的值
		return curValue;
	}
}

使用CAS来解决并发问题,一般都会伴随着自旋,而所谓自旋,其实就是循环尝试。如果(2)返回值不等于count,表明count被其他线程改变。

class SimulatedCAS {
	int count;

	// 实现 count+=1
	addOne(){
		do{
			newValue = count+1;	 //①
	} while(count !=cas(count,newValue)) //②

	synchronized int cas(int expect, int newValue) {
		// 读目前 count 的值
		int curValue = count;
		// 比较目前 count 值是否 == 期望值
		if (curValue == expect) {
			// 如果是, 则更新 count 的值
			count = newValue;
		} // 返回写入前的值
		return curValue;
	}
}

// TODO
存在的ABA的问题:???

2. 看 Java 如何实现原子化的 count += 1

原子类 AtomicLong 的 getAndIncrement() 方法内部就是基于 CAS 实现
的。Java 1.8 版本中,getAndIncrement() 方法会转调unsafe.getAndAddLong() 方法。这里 this 和valueOffset 两个参数可以唯一确定共享变量的内存地址。

 public final long getAndIncrement() {
        return unsafe.getAndAddLong(this, valueOffset, 1L);
    }

unsafe.getAndAddLong()方法的源码如下,该方法首先会在内存中读取共享变量的值,之后循环调用compareAndSwapLong()方法来尝试设置共享变量的值,直到成功为止。compareAndSwapLong()是一个native方法,只有当内存中共享变量的值等于expected时,才会将共享变量的值更新为x,并且返回true;否则返回fasle。compareAndSwapLong的语义和CAS指令的语义的差别仅仅是返回值不同而已。

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;
	} 
	// 原⼦性地将变量更新为 x
	// 条件是内存中的值等于 expected
	// 更新成功则返回 true
	native boolean compareAndSwapLong(Object o, long offset, long expected, long x);

CAS 使用的经典范例

do {
// 获取当前值
oldV = xxxx;
// 根据当前值计算新值
newV = ...oldV...
}while(!compareAndSet(oldV,newV);

3. 原子类概览

有五个类别:原子化的基本数据类型、原子化的对象引用类型、原子化数组、原子化对象属性更新器和原子化的累加器。
在这里插入图片描述

3.1 原子化的基本数据类型

AtomicBoolean、AtomicInteger和AtomicLong。

getAndIncrement()	//	原⼦化	i++
getAndDecrement()	//	原⼦化的	i--
incrementAndGet()	//	原⼦化的	++i
decrementAndGet()	//	原⼦化的	--i
//	当前值	+=delta,返回	+=	前的值
getAndAdd(delta)	
//	当前值	+=delta,返回	+=	后的值
addAndGet(delta)
//CAS	操作,返回是否成功
compareAndSet(expect,	update)
//	以下四个⽅法
//	新值可以通过传⼊	func	函数来计算
getAndUpdate(func)
updateAndGet(func)
getAndAccumulate(x,func)
accumulateAndGet(x,func)

3.2 原子化的对象引用类型

相关实现有 AtomicReference、AtomicStampedReference和AtomicMarkableReference,利用它们可以实现对象引用的原子化更新。AtomicReference 提供的方法和原子化的基本数据类型差不多,这里不再赘述。不过需要注意的是,对象引用的更新需要重点关注 ABA 问题,AtomicStampedReference和AtomicMarkableReference这两个原子类可以解决ABA问题。

解决ABA问题,增加版本号,每一次CAS,版本号都递增。AtomicStampedReference 实现的 CAS 方法就增加了版本号参数,
方法签名如下:

boolean compareAndSet(V expectedReference,V newReference,
int expectedStamp,int newStamp)

AtomicMarkableReference 的实现机制则更简单,将版本号简化成了一个 Boolean 值:

boolean compareAndSet(V expectedReference,V newReference,
boolean expectedMark,boolean newMark)

3.3 原子化数组

AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray,原子化地更新数组里面的每一个元素。

3.4 原子化对象属性更新器

AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和
AtomicReferenceFieldUpdater,利用它们可以原子化地更新对象的属性,这三个方法都是利用反射机制实现的,创建更新器的方法如下:

public static <U> AtomicXXXFieldUpdater<U>
	newUpdater(Class<U> tclass,String fieldName)

需要注意的是,对象属性必须是 volatile 类型的,只有这样才能保证可见性

3.5 原子化的累加器

DoubleAccumulator、DoubleAdder、LongAccumulator和LongAdder,这四个类仅仅用来执行累加操作,相比原子化的基本数据类型,速度更快,但是不支持compareAndSet()方法。如果你仅仅需要累加操作,使用原子化的累加器性能会更好。

发布了97 篇原创文章 · 获赞 3 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_39530821/article/details/102692312