JUC之并发包内部组成(AQS 和 CAS)

AtomicInteger底层实现原理?

AtomicInteger是int类型的一个封装,提供原子性的访问和更新操作,其原子性操作的实现是基于CAS操作。所谓CAS表征的是一些列操作的集合,获取当前数值,进行一些运算,利用CAS指令试图进行更新。如果当前数值未变,代表没有其他线程进行并发修改,则成功更新。否则可能出现不同的选择,要么返回false,要么进行重试。
从AtomicInteger的内部属性可以看出它依赖于Unsafe提供一些底层能力,进行底层操作;以及Volatile的value字段,记录数值保证可见性。

private satic fnal jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
private satic fnal long VALUE = U.objectFieldOfset(AtomicInteger.class, "value");
private volatile int value;

具体的原子操作,可参考任意一个原子更新方法,比如下面的getAndIncrement.
Unsafe会利用value字段的内存地址偏移,直接完成操作。

public fnal int getAndIncrement() {
return U.getAndAddInt(this, VALUE, 1);
}

因为getAndIncrement需要返回数值,所以需要添加失败重试逻辑。

public final int getAndAddInt(Object o, long ofset, int delta) {
	int v;
	do {
		v = getIntVolatile(o, ofset);
		} while (!weakCompareAndSetInt(o, ofset, v, v + delta));
	return v;
}

而类似于CompareAndSet这种返回boolean类型的函数,因为其返回值表现就是成功与否,所以不需要重试。

public fnal boolean compareAndSet(int expectedValue, int newValue);

CAS 是Java并发中所谓lock-free机制的基础。

问题:

  • 在什么场景下可以采用CAS技术,调用Unsafe毕竟不是大多数场景的最好选择,是否有更加推荐的方式。
  • 对 ReentrantLock、CyclicBarrier底层机制的理解。

CAS机制

关于CAS机制的使用你可以设想一个这样的场景:在数据库产品中为保证数据的一致性,一个常见的选择是,保证只有一个线程能够排他性的修改一个索引分区,如何在数据库抽象层面实现呢。
可以考虑为索引分区对象添加一个逻辑上的锁,例如,以当前独占的线程ID作为锁的数值
,然后通过原子操作设置Lock数值,来实现加锁和释放锁,伪代码:

public class AtomicBTreePartition {
private volatile long lock;
public void acquireLock(){}
public void releaseeLock(){}
}

在Java代码中如何实现锁操作呢?有两种方式:

  1. 使用java.util.concurrent.AutomicLongFieldUpdater,它是基于反射机制创建的,我们需要保证类型和字段名称正确。
private static final AtomicLongFieldUpdater<AtomicBTreePartition> lockFieldUpdater =
	AtomicLongFieldUpdater.newUpdater(AtomicBTreePartition.class, "lock");
private void acquireLock(){
	long t = Thread.currentThread().getId();
	while (!lockFieldUpdater.compareAndSet(this, 0L, t)) {
	// 等待一会儿,数据库操作可能比较慢}
}

Atomic提供了最常用的原子性数据类型,甚至是引用,数组等相关原子类型和更新操作工具,是很多线程安全程序的首选。

  1. 第二种方法,Variable Handle API ,提供了各种粒度的原子或者有序性的操作等。可以将前面的代码改成如下:
private satic fnal VarHandle HANDLE = 
	MethodHandles.lookup().fndStaticVarHandle(AtomicBTreePartition.class, "lock");
private void acquireLock(){
	long t = Thread.currentThread().getId();
	while (!HANDLE.compareAndSet(this, 0L, t)){
	// 等待一会儿,数据库操作可能比较慢}
}

过程非常直观,首先获取相应的变量句柄,然后直接调用其提供的CAS方法。

总结:
一般来说,我们进行的类似于CAS操作,推荐使用Variable Handle API去实现,因为其提供了精细粒度的公共底层API。这里强调公共是因为,其API不会像内部API那样,发生不可预测的修改,这一点提供了对于未来产品的维护升级保障。
可能发生的问题:

  1. 常用的失败重用机制有一个假设,即竞争情况是短暂的。大多数情况确实如此,但是在有些情况下我们还是需要烤炉考虑大量自旋所带来的的CPU消耗。
  2. CAS还需要防止ABA问题。CAS是在更新时比较前值,如果对方只是恰好相等,比如发生了A——>B——>A的更新,只判断数值是A,就可能导致不合理的修改。这种情况Java提供了AtomicStampedReferece工具类,通过引入版本号的方式来维持正确性。

其实大多数情况下Java开发者并不需要直接利用CAS代码去实现线程安全容器等,更多的是通过并发包间接的享受到lock-free机制在扩展性上的好处。

AQS机制

为什么需要AQS?
因为,从原理上一种同步结构往往是可以利用其他结构实现的。例如用Semaphore实现互斥锁。但是对某种同步结构的倾向,会导致复杂、晦涩的实现逻辑,所以他们选择将基础的同步相关操作抽象在AbstractQueueSynchronizer 中,利用AQS为我们构建同步结构提供了范本。
AQS内部数据和方法:

  • 一个volatile的整数成员表征状态,同时提供了setState()和getState()方法。
private volatile int state;
  • 一个先入先出的等待线程队列,以实现多线程间竞争和等待。是AQS机制的核心之一。
  • 各种基于AQS的基础操作方法,以及各种期望具体同步结构去实现的acqurie/release方法。

利用AQS实现一个同步结构,至少要实现两个基本类型的方法,分别是acquire,获取资源的独占权;还有就是release操作,释放某个资源的独占。

以ReentrantLock为例,它内部通过AQS实现了Sync类型,以AQS的state来反应锁的持有情况。

private final Sync sync;
abstract static class Sycn extends AbstractQueueSynchronizer{ ... }

下面是ReentrantLock对应的acquire和release操作,如果是CountDownLatch则可以看做是await()/countDown(),具体实现也有区别。

public void lock() {
	sync.acquire(1);
}
public void unLock() {
	sync.release(1);
}

排除掉一些细节,整体的分析acquire方法逻辑,其直接实现是在AQS内部,调用了tryAcuire和AcuireQuened,这是两个要搞清楚的基本部分。

public final void acquire (int arg) {
	if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
		selfInterrupt();
}

首先看tryAcquire。 在ReentrantLock中,tryAcquire的逻辑实现在NonfairSync和 FairSync中,分别提供了进一步的非公平或公平方法,而AQS内部tryAcquire仅仅是个接近未实现的方法(直接抛出异常),这是个留给操作者自己定义的操作。
我们可以看到公平性在ReentrantLock构建时是如何指定的,具体:

public ReentrantLock() {
	sync = new NonfairSync();//默认是不公平的
}
public ReentrantLock(boolean fair) {
	sync = fair ? new FairSync() : new NonfairSync();
}

以非公平的tryAcquire为例,其内部实现了如何配合状态与CAS获取锁,注意,对比公平版本的tryAcquire,它在锁无人占有时,并不检查是否有其他等待者,这里体现了非公平的语义。

final boolean nonfairTryAcquire(int acquires) {
	final Thread currrent = Thread.currentThread();
	int c = getState();//获取当前AQS内部状态量
	if(c == 0) { //0表示无人占有,直接用CAS获取状态位。
		if (compareAndsetState(0, acquires)) { // 不检查排队情况,直接争抢
			setExclusiveOwnerThread(current);  //并设置当前线程独占锁
			return true;
		}
	}else if (current == getExclusiveOwnerThread()) { //即使状态不是0,也可能当前线程是持有者,因为这是再入锁
		int nextc = c + acquiers;
		if (nextc < 0) //overflow
			throw new Error("Maximum lock count exceeded");
		setState(nextc);
		return true;
	}
	return false;
}

接下来分析acquireQueued。 如果前面tryAcquire失败,代表着锁争抢失败,进入排队竞争阶段。这里就是我们说的利用FIFO队列,实现线程间对锁的竞争的部分,是AQS的核心。
当前线程会被包装成一个排他模式的节点(EXCLUSIVE),通过addWaiter方法添加到队列中。acquireQueued的逻辑,简单来说就是,如果当前节点的前面是头结点,则试图获取锁,一切顺利则成为新的头结点;否则,有必要就等待,具体处理逻辑看代码。

final boolean acquireQueued(final Node node, int arg) {
	boolean interrupted  = false;
	try {
		for(;;){ //循环
			final Node p = node.predecessor();//获取前一个节点
			if (p == head && tryAcquire(arg)) { //如果前一个节点是头结点,表示当前节点合适去tryAcquire
				setHead(node);// acquire成功,则设置新的头节点
				p.next = null;// 将前面节点对当前节点的引用清空
				return interrupted;
			}
			if (shouldParkAfterFailedACquire(p, node)) //检查是否失败后需要Park
				interrupted |= parkAndCheckInterrupted();
		}
	}catch (Throwable t) {
		cancelAcquire(node);//出现异常,取消
		if (intrrupted)
			selfInterrupt();
		throw t;
	}
} 

到这里线程试图获取锁的过程基本展示出来了,tryAcquire是按照特定场景需要开发者去实现的部分,而线程间竞争则是AQS通过Waiter队列和acquireQueued提供的,在release方法中,同样会对队列进行对应操作。

发布了40 篇原创文章 · 获赞 0 · 访问量 630

猜你喜欢

转载自blog.csdn.net/weixin_42610002/article/details/104724399
今日推荐