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)) //②
//	模拟实现	CAS,仅⽤来帮助理解
	synchronized int cas(int expect, int newValue) {
		// 读目前 count 的值
		int curValue = count;
		// 比较目前 count 值是否 == 期望值
		if (curValue == expect) {
			// 如果是, 则更新 count 的值
			count = newValue;
		} // 返回写入前的值
		return curValue;
	}
}

存在的ABA的问题

前面我们提到“如果 cas(count,newValue) 返回的值不等于count,意味着线程在执行完代码①处之后,执行代码②处之前,count的值被其他线程更新过”,那如果cas(count,newValue) 返回的值等于count,是否就能够认为count的值没有被其他线程更新过呢?显然不是的,假设count原本是A,线程 T1 在执行完代码①处之后,执行代码②处之前,有可能 count 被线程 T2 更新成了 B,之后又被 T3 更新回了 A,这样线程 T1 虽然看到的一直是 A,但是其实已经被其他线程更新过了,这就是 ABA 问题。

可能大多数情况下我们并不关心 ABA 问题,例如数值的原子递增,但也不能所有情况下都不关
心,例如原子化的更新对象很可能就需要关心 ABA 问题,因为两个 A 虽然相等,但是第二个 A
的属性可能已经发生变化了。所以在使用 CAS 方案的时候,一定要先check 一下。

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()方法。如果你仅仅需要累加操作,使用原子化的累加器性能会更好。

4.总结

无锁方案相对于互斥锁方案,优点非常多,首先性能好,其次是基本不会出现死锁问题(但可能出现饥饿和活锁问题,因为自旋会反复重试)。

Java提供的原子类能够解决一些简单的原子性问题,但你可能会发现,上面我们所有原子类的方
法都是针对一个共享变量的,如果你需要解决多个变量的原子性问题,建议还是使用互斥锁方
案。原子类虽好,但使用要慎之又慎。

5.课后思考

下面的示例代码是合理库存的原子化实现,仅实现了设置库存上限 setUpper() 方法,你觉得
setUpper() 方法的实现是否正确呢?

public class SafeWM {

  class WMRange {

    final int upper;
    final int lower;

    WMRange(int upper, int lower) {
      //	省略构造函数实现
    }
  }

  final AtomicReference<WMRange>
      rf = new AtomicReference<>(new WMRange(0, 0));

  //	设置库存上限
  void setUpper(int v) {
    WMRange nr;
    WMRange or = rf.get();
    do {
      //	检查参数合法性
      if (v < or.lower) {
        throw new IllegalArgumentException();
      }
      nr = new WMRange(v, or.lower);
    } while (!rf.compareAndSet(or, nr));
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值