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机制的使用你可以设想一个这样的场景:在数据库产品中为保证数据的一致性,一个常见的选择是,保证只有一个线程能够排他性的修改一个索引分区,如何在数据库抽象层面实现呢。
可以考虑为索引分区对象添加一个逻辑上的锁,例如,以当前独占的线程ID作为锁的数值
,然后通过原子操作设置Lock数值,来实现加锁和释放锁,伪代码:
public class AtomicBTreePartition {
private volatile long lock;
public void acquireLock(){}
public void releaseeLock(){}
}
在Java代码中如何实现锁操作呢?有两种方式:
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提供了最常用的原子性数据类型,甚至是引用,数组等相关原子类型和更新操作工具,是很多线程安全程序的首选。
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那样,发生不可预测的修改,这一点提供了对于未来产品的维护升级保障。
可能发生的问题:
其实大多数情况下Java开发者并不需要直接利用CAS代码去实现线程安全容器等,更多的是通过并发包间接的享受到lock-free机制在扩展性上的好处。
为什么需要AQS?
因为,从原理上一种同步结构往往是可以利用其他结构实现的。例如用Semaphore实现互斥锁。但是对某种同步结构的倾向,会导致复杂、晦涩的实现逻辑,所以他们选择将基础的同步相关操作抽象在AbstractQueueSynchronizer 中,利用AQS为我们构建同步结构提供了范本。
AQS内部数据和方法:
private volatile int state;
利用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方法中,同样会对队列进行对应操作。