【线程 锁】jdk 1.8 CAS的底层实现(AtomicInteger&compareAndSwapInt&compareAndSwapObject)

1. 什么是CAS?

CAS: compare and swap(compare and exchange),比较并交换,读取内存中的数值,计算修改后,将修改前的值与内存值比较,如果相同,则说明没有其他线程修改过,就将修改后的值写入内存;如果不同,则说明被改过,重复上述过程,直到写入成功,听着有点蒙,没关系,看一张图:
在这里插入图片描述

CAS究竟干了什么事呢?举个例子,假如有两个线程想对一个值【V】进行+1操作,第一个进程进来之后,先把这个值V【0】取出来,进行+1操作,然后再把+1后的结果【1】写回去之前,我先判断这个值是否依然为0,如果我发现这个值不是0了,变成【2】了,那么我就知道这肯定是有其他线程将这个值改掉了,这时候我不去写这个值,而是把这个值【2】再次读出来,再进行+1操作,写之前我判断这个值V是否为【2】,如果不为2,我就一直循环下去,直到成功为止。这个过程是不需要加锁的,所以说CAS叫无锁,又叫自旋锁。

2. ABA问题

看到这你可能有个疑问,如果我这个线程将值改成【1】后,在我往回写的过程之中,有一个线程将这个值【0】取走了,改成【1】,写回去,又有一个线程将值【1】取走了,改成了【0】,这个值从0 -->1 --> 0,如果是简单的值从0到1又回到0,这种没问题,不影响最终的结果,但是如果值是引用类型,不允许中间有人改过,那这就有问题,这就是CAS的ABA问题。

怎么解决?加版本号,举个例子,比如说你和你的女朋友分手了,分手之后她又经历了别的男人,又回到了你的身边,但是你发现她和以前不太一样了,那么你怎么察觉这一点?很简单,她离开的时候在她脑门上贴个标签1.0,当她再回来的时候,发现脑门上是99.0,那你就知道她到底经历了什么。

如果问你JDK具体的实现,BooleanReference, 用布尔值标注它有没有改变或版本号version标注。

从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

3. compareAndSwapInt()

3.1 AtomicInteger例子

看到这你可能又会问,你CAS不是比较并交换吗,如果一个线程比较完之后,将要写回的过程,另一个线程将这个值改了,那你不是傻了吗。其实最后这一步也就是写回的操作是原子操作,是不允许打断的,那么这个原子操作是怎么实现的呢?

如果我想实现一个功能,对一个值进行自增操作,创建100个线程,每个线程都对这个值进行10000次自增,那么期待值就是1000000,先用synchronized实现:

private static AtomicInteger m = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
    Thread[] threads = new Thread[100];
    CountDownLatch latch = new CountDownLatch(threads.length);  //初始个数
    for (int i = 0; i < 100; i++) {
        threads[i] = new Thread(() -> {
            for (int j = 0; j < 10000; j++) {
                m.incrementAndGet();
            }
        });
      	latch.countDown();  //当前子线程执行完,就减1,直至为0,表示所有的子线程都执行完了
    }
    Arrays.stream(threads).forEach(Thread::start);
    latch.await(); //主线程阻塞,等所有的子线程执行完
    System.out.println(m);
}

AtomicInteger底层都是CAS实现的,CountDownLatch叫门栓,latch.await();的意思就是把门栓打开,把门堵住,想进来必须拿到锁,必须等签名100把锁latch.countDown()到0了才能执行下面的操作,等同于写100遍thread.join();说跑题了,这里的核心是m.incrementAndGet();,它没有加锁是怎么保证数据一致性的?

3.2 AtomicInteger的incrementAndGet()底层实现

简单,进源码看下:

incrementAndGet() 返回的是修改后的值,注意和getAndAdd()进行区分,后者返回的是修改前的值。

    /**
     * Atomically increments by one the current value.
     *
     * @return the updated value 返回的是修改后的值,注意和getAndAdd进行区分
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

注意:不同jdk版本,底层代码实现稍有不同!

上面的代码有点难以理解的地方,可以看下图绿色部分的解释:
在这里插入图片描述

这里解释一下valueOffset变量,首先valueOffset的初始化在static静态代码块里面,代表value成员变量相对起始内存地址的字节相对偏移量:

private static final long valueOffset;
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

在这里插入图片描述

AtomicInteger类在编译时,就确定了成员变量的顺序,包括value成员,算出偏移量,后续,如果知道了AtomicInteger实例的对象的地址后,那么根据偏移量,就能得到value属性的地址,那么操作内存时,传入对象和偏移量,就等价传入对象和该对象的成员变量,那么就能修改该对象的指定成员变量。

在生成一个AtomicInteger对象后,可以看做生成了一段内存,对象中各个字段按一定顺序放在这段内存中,字段可能不是连续放置的,unsafe.objectFieldOffset(Field f)这个方法准确地告诉我"value"字段相对于AtomicInteger对象的起始内存地址的字节相对偏移量:

private volatile int value;
 
/**
 * Creates a new AtomicInteger with the given initial value.
 *
 * @param initialValue the initial value
 */
public AtomicInteger(int initialValue) {
    value = initialValue;
}

你会发现它调用了unsafe类的getAndAddInt()方法,点进去:

public final int getAndAddInt(Object obj, long var2, int var4) {
  	int var5;
  	do {
    		var5 = this.getIntVolatile(obj, var2);
  	} while(!this.compareAndSwapInt(obj, var2, var5, var5 + var4));

  	return var5;
}

在这里插入图片描述

compareAndSwapInt,这个方法怎么看着那么熟悉,What?这不就是CAS吗?点进去:

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

很不幸,只能到这里了,看到了native,这是调用了C++代码的实现,如果你想继续往下看,那你只能看UnSafe的底层C++代码了

3.2.1 C++代码实现

上网搜unsafe.cpp compareAndSetInt代码实现:

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSetInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) {
    oop p = JNIHandles::resolve(obj);
    //获取对象的变量的地址
    jint* addr = (jint *)index_oop_from_field_offset_long(p, offset);
    //调用Atomic操作
    return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
} UNSAFE_END

发现最终调用了cmpxchg方法:

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  int mp = os::is_MP();   //判断是否为单核
  __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;
}

这里已经是底层汇编实现了,看到LOCK_IF_MP,如果mp为true,就Lock,那么我们看下is_MP方法:

  static inline bool is_MP() {
    // During bootstrap if _processor_count is not yet initialized
    // we claim to be MP as that is safest. If any platform has a
    // stub generator that might be triggered in this phase and for
    // which being declared MP when in fact not, is a problem - then
    // the bootstrap routine for the stub generator needs to check
    // the processor count directly and leave the bootstrap routine
    // in place until called after initialization has ocurred.
    return (_processor_count != 1) || AssumeMP;
  }

看到_processor_count != 1进程数不等于1,什么意思呢,就是不是单核的,再看下LOCK_IF_MP方法:

// Adding a lock prefix to an instruction on MP machine
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

从注释翻译出来, 添加一个“lock”的前缀,在哪添加呢?在某一指令上,MP的意思是multi processor,多核机器,也就是在多核上添加一个lock的前缀。所以最终汇编调用的指令是lock cmpxchg这条汇编的指令的作用就是锁总线。锁总线什么意思?如果多CPU也就是多核,未防止我写入的时候被其他CPU打断,我把CPU通往内存的一组线给锁定,不允许其他CPU去修改。

所以CAS的底层实现就是lock cmpxchg,多核才会加锁,单核不需要。

4. compareAndSwapObject()

我们AQS源码(AbstractQueuedSynchronizer)中添加同步队列节点的逻辑:

不断地自旋加入一个新的节点compareAndSetTail或compareAndSetHead:

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

在这里插入图片描述
在这里插入图片描述
如果某个对象的tailOffset属性指向的对象地址仍是expect对象的话,那么把tailOffset属性指向的对象地址变更为update对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值