并发编程-03 聊透乐观锁CAS

Doug Lea的CAS

    • 一、CAS
      • 1.1 CAS的应用
      • 1.2 CAS源码
      • 1.3 CAS缺陷
      • 1.4 ABA问题
      • 1.5 CAS源码
    • 二、Atomic
      • 2.1 Atomic原子操作类
      • 2.2 AtomicStampedReference 解决ABA
      • 2.3 LongAdder原理

一、CAS

1.1 CAS的应用

CAS(Compare And Swap),比较并交换。
通常用法是:比较内存中某个变量的值是否与预期一致,如果预期一致,则认为可以交换(修改)内存中的值(或认为可以获取到锁)。
应用场景:

  • 自增(累加)
  • 加减操作
  • 高并发提高性能(减少用锁)

cas伪代码:

value = 内存中实际的变量值;
expect = 该变量的预期值;
useValue = 替换后内存中的值; 
for(;;){
~
	if(value == expect){
		value = useValue; 
		return value ;
	}else{
		return value ;
	}
~
}

cas操作中包括2个部分,1、比较预期值与内存值是否一致,2、当预期一致时,将内存中该变量的值修改。上述两步操作必须是原子的,否则在交换的过程中,内存值已发生变化则该操作毫无意义。
在硬件层面上,处理器架构支持上述cas操作,后面在cas源码会详述。

1.2 CAS源码

简单的例子来看cas的应用。静态内部类Entity有个int类型的字段x。cas预期内存值为0 —> 改为3,预期3—> 改为5,预期.—> 改为8。

    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

下面结合例子,详述所涉及的知识点:

  • Unsafe类
    Unsafe的构造方法被private 修饰,所以无法通过new对象来创建,如果想要获取Unsafe类,需要通过反射并设置access权限true。
public final class Unsafe {
    private static final Unsafe theUnsafe;
    private Unsafe() {
    }
    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }
}

反射获取Unsafe 类的代码

    public static Unsafe getUnsafe() {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            return (Unsafe) field.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
  • offset偏移量
    在JVM章节,分析对象内存结构时,32位计算机的对象地址都是32位的,64位计算机也会将对象信息进行压缩至34-36位。
    对象的内存结构组成:8位Mark word对象头(8字节)+4位klass ponit(类型指针) + 字段类型长度
public class CasImplTest {
    public static void main(String[] args) {
        Entity entity = new Entity();

        Unsafe unsafe = UnsafeFactory.getUnsafe();

        long offset = UnsafeFactory.getFieldOffset(unsafe, Entity.class, "x");
        //查看内存结构
        ClassLayout layout = ClassLayout.parseInstance(entity);
        System.out.println(layout.toPrintable());

        System.out.println(offset);
        boolean successful;

        // 4个参数分别是:对象实例、字段的内存偏移量、字段期望值、字段新值
        successful = unsafe.compareAndSwapInt(entity, offset, 0, 3);
        System.out.println(successful + "\t" + entity.x);

        successful = unsafe.compareAndSwapInt(entity, offset, 3, 5);
        System.out.println(successful + "\t" + entity.x);

        successful = unsafe.compareAndSwapInt(entity, offset, 3, 8);
        System.out.println(successful + "\t" + entity.x);
    }

    static class Entity{
        int x;
    }
}

com.huawei.concurrent.cas.CasImplTest$Entity object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4    int Entity.x                                  0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

12
true	3
true	5
false	5

Process finished with exit code 0
  • var4 预期内存的值
  • var5 修改成为的值

1.3 CAS缺陷

通过一个普遍应用的场景来看cas的应用过程。(起10个线程,每个线程自加1w次,需要保证求和结果sum为10w)

此处当然也可以使用synchronized或ReentrantLock来实现锁,但该场景中竞争激烈,如果用synchronized,势必将锁升级到重量级锁,甚至触发park、unpark等内核态操作,效率势必大减。

  • cas只能保证操作的原子性,如果需要保证内存中变量的可见性和有序性,需要通过字段通过volatile来修饰(cas操作的值必须由volatile修饰)
public class CasTest {
    private volatile static int sum = 0;
    static CASLock casLock = new CASLock();

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(() -> {
                for(;;){
                    if(casLock.getState()==0 && casLock.cas()) {
                        try {
                            for (int j = 0; j < 10000; j++) {
                                sum++;
                            }
                            System.out.println(casLock.getState());
                        } finally {
                            casLock.setState(0);
                        }
                        break;
                    }
                }

            });
            thread.start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(sum);
    }
}

public class CASLock {
    //加锁标记
    private volatile int state;
    private static final Unsafe UNSAFE;
    private static final long OFFSET;
    static {
        try {
            UNSAFE = UnsafeFactory.getUnsafe();
            OFFSET = UnsafeFactory.getFieldOffset(
                UNSAFE, CASLock.class, "state");
        } catch (Exception e) {
            throw new Error(e);
        }
    }
    public boolean cas() {
        return UNSAFE.compareAndSwapInt(this, OFFSET, 0, 1);
    }
    public int getState() {
        return state;
    }
    public void setState(int state) {
        this.state = state;
    }
}

尽管拜托了繁杂的锁机制,但cas仍然存在以下问题:

  1. for(;;)自旋过程中,只有一个线程能够cas成功,其他线程所占CPU一直无休止空转(本文2.3节提供Doug lea的解决方案)
  2. 只能保证1个共享变量的原子操作(本文2.1节详述解决方案)
  3. ABA问题(本文1.4节详述问题及解决方案)

1.4 ABA问题

ABA问题描述:cas操作前从内存中查到value的值为A,预期内存中该变量的值为A,比较内存值value与A相等,则修改内存值为U。
但假如,在读取A值后,到cas之前,内存中的值发生过2次变化。即:其他线程将内存值value修改为B,而后又改回A,这对当前线程而言是不可见的。如果再继续操作将导致后续数据可能出现脏数据。
并发编程-03 聊透乐观锁CAS_第1张图片
ABA问题图解:
并发编程-03 聊透乐观锁CAS_第2张图片
ABA问题的解决办法:
CAS操作是乐观锁,每次修改都预期内存值没有发生过改变。这类似于数据库操作中,一种基于数据版本来实现数据同步的机制。简言之,加版本号。
详见本文2.2AtomicStampedReference

1.5 CAS源码

核心逻辑:

  • LOCK_IF_MP(%4) “cmpxchgl %1,(%3)” [cmpxchgl (cmpxchg exchange_value)是执行cas的核心操作]
  • LOCK_IF_MP(%4) [在多CPU处理器架构下,使用#lock前缀指令,保证内存变量操作的原子性、有序性]

Hotspot 虚拟机对compareAndSwapInt 方法的实现如下:

#unsafe.cpp
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  // 根据偏移量,计算value的地址
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  // Atomic::cmpxchg(x, addr, e) cas逻辑 x:要交换的值   e:要比较的值
  //cas成功,返回期望值e,等于e,此方法返回true 
  //cas失败,返回内存中的value值,不等于e,此方法返回false
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;

核心逻辑在Atomic::cmpxchg方法中,这个根据不同操作系统和不同CPU会有不同的实现。这里我们以linux_64x的为例,查看Atomic::cmpxchg的实现

#atomic_linux_x86.inline.hpp
inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  //判断当前执行环境是否为多处理器环境
  int mp = os::is_MP();
  //LOCK_IF_MP(%4) 在多处理器环境下,为 cmpxchgl 指令添加 lock 前缀,以达到内存屏障的效果
  //cmpxchgl 指令是包含在 x86 架构及 IA-64 架构中的一个原子条件指令,
  //它会首先比较 dest 指针指向的内存值是否和 compare_value 的值相等,
  //如果相等,则双向交换 dest 与 exchange_value,否则就单方面地将 dest 指向的内存值交给exchange_value。
  //这条指令完成了整个 CAS 操作,因此它也被称为 CAS 指令。
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;

二、Atomic

2.1 Atomic原子操作类

2.2 AtomicStampedReference 解决ABA

2.3 LongAdder原理

如果多线程竞争激烈,那么cpu狂飙,累加计算的效率却不高。为了解决自旋瓶颈问题,doug lea提供了一个情理之中却又语出惊人的思路,让其他线程不要都只对一个数据累加,线程只要空转,就把线程进行hash加到对应的数组中,分别针对不同的值累加,最终将所有结果求和。
考虑如何提升CPU效率,考虑良久,看到狗哥的思路,直呼惊人的好想法。
并发编程-03 聊透乐观锁CAS_第3张图片

你可能感兴趣的:(多线程,JVM,硬件架构,java,多线程)