由AtomicInteger开始讲CAS

AtomicInteger底层依赖于Unsafe类,基于CAS(compare and swap/set)原理,CAS底层由cpu原语支持,可以保证更新一个Integer变量的原子性。Unsafe类可以直接操作内存,指定在内存的某位置读取或者写入某值。

AtomicInteger主要的三个成员变量如下。
U是Unsafe类的一个实例,Unsafe类采用单例模式,其中包含了一些用于操作底层的非安全的方法,虽然这些方法都是public,但是不应该随便使用,因为unsafe。
VALUE代表了AtomicInteger.class在jvm的内存分配中,名为"value"的成员变量在的偏移量,可以利用这个偏移量直接读取或者更新"value"的值。它是调用Unsafe中的objectFieldOffset方法得到的。
value用volatile修饰,当一个线程修改过之后,其他线程可以立即看到。

private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
private volatile int value;

下面列出一个关键代码,体现其CAS思想。
可以看到AtomicInteger中的getAndIncrement方法,又调用了Unsafe类的getAndAddInt方法。

/**
 * Atomically increments the current value,
 * with memory effects as specified by {@link VarHandle#getAndAdd}.
 *  * <p>Equivalent to {@code getAndAdd(1)}.
 *  * @return the previous value
 */
public final int getAndIncrement() {
    return U.getAndAddInt(this, VALUE, 1);
}

CAS的思想就体现在Unsafe的getAndAddInt中。根据VALUE(value的偏移量offset)直接获取value在内存中的值v,接下来,调用weakCompareAndSetInt方法尝试更新,有四个入参:

  • o代表被更新的对象;
  • offset代表成员变量value的偏移量;
  • v代表期望值(上一步通过getIntVolatile(o, offset)查出来的内存中的值),更新的时候,比较该期望值v和内存中该偏移量实时存储的值是否一致,若一致则更新并返回true,若不一致则不更新并返回false。“若一致则更新”这个步骤是由cpu原语支撑的,是原子的。
  • v+delta代表新值。
/**
 * Atomically adds the given value to the current value of a field
 * or array element within the given object {@code o}
 * at the given {@code offset}.
 *
 * @param o object/array to update the field/element in
 * @param offset field/element offset
 * @param delta the value to add
 * @return the previous value
 * @since 1.8
 */
@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!weakCompareAndSetInt(o, offset, v, v + delta));
    return v;
}

当多个线程尝试使用CAS同时更新一个变量时,只有其中一个线程能更新变量的值,其他线程都将失败。然而失败的线程并不会被挂起,而是被告知在这次竞争中失败,并可以再次尝试。由于一个线程在竞争CAS时失败不会阻塞,因此它可以决定是否重新尝试,或者执行一些恢复操作,也可以不执行任何操作。

CAS的典型使用模式是:首先从V中读取值A,并根据A计算新值B,然后通过CAS以原子方式将V中的值由A变成B(只要在这期间没有任何线程将V的值修改为其他值)。由于CAS能检测到来自其他线程的干扰,因此即使不使用锁也能够实现原子的读-改-写操作序列。

AtomicInteger保证了变量更新的原子性,而volatile保证了变量的可见性和有序性。那么我们可以说原子类的更新是线程安全的吗?No

CAS的缺点:

  • ABA问题。假设value初始值是A,当前线程拿到期望值v=A,在尝试更新value之前,另一个线程把value更新为B,另一个线程又把value更新为A。那么当前线程在更新value时,发现期望值和内存中的值相同,就进行更新了,而者期间value的值已经被更新过两次。
  • 自旋消耗。看上面的getAndAddInt方法,在超高并发的场景下,当前线程可能会一直在while循环中,持续占有时间片,造成资源浪费。
  • 活锁问题。多个相互协作的线程都对彼此相应从而修改各自的状态,并使得任何一个线程都无法继续执行时,就发生了活锁。就像生活中,两个有礼貌的人面对面相向而行,都想给对方让路反而一时之间谁都不能前进半步。

解决方案:

  • ABA问题:
    某些场景下是不需要解决的,例如只是简单的对一个整数进行加减,不涉及任何业务场景,那么这个ABA问题就可以忽略,因为不会对结果产生影响。如果必须要解决,可以记录对于该对象的更新版本号,每次更新时不仅比较期望值和实际值是否一致,还要比较版本号的期望值和实际值是否一致。每次更新的时候都让版本号+1。java中的AtomicStampedReference.class和AtomicMarkableReference.class记录了版本号。
  • 自旋消耗问题:
    破坏掉while死循环,当超过一定时间或者超过一定次数的时候放弃更新,每次重试之前等待一个随机的时间。
  • 活锁问题:
    解决活锁问题,需要在每次重试机制中引入随机性。每次重新尝试之前,等待一个随机的时间。

适用场景

在中低程度的竞争下,atomic类能够提供更高的可伸缩性,效率更高;而在高强度的竞争下,锁能够更有效地避免竞争(使线程阻塞),效率更高。
我们可以通过提高处理竞争的效率来提高可伸缩性,但是只有完全消除竞争,才能实现真正的可伸缩性。如果能够避免使用共享状态,那么开销会更加小,例如使用ThreadLocal来保存变量值。

Atomic类,有一个共同特点,只能针对一个变量的更新保持其原子性。若有多个共享变量需要保证并发安全,需要考虑其他方案,当然也可以考虑把这些个变量集成在一个引用类型里,使用原子类管理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章