CAS機制及AtomicInteger源碼分析

1、CAS簡介

CAS即Compare And Swap對比交換,區別於悲觀鎖,藉助CAS能夠實現區別於synchronized獨佔鎖的一種樂觀鎖,被普遍應用在各大編程語言之中。Java JUC底層大量使用了CAS,能夠說java.util.concurrent徹底是創建在CAS之上的。可是CAS也有相應的缺點,諸如ABAcpu使用率高等問題沒法避免。java

CAS總共有3個操做數,當前內存值V,舊的預期值A,要修改的新值N。當且僅當A和V相同時,將V修改成N,不然什麼都不作。程序員

2、CAS源碼分析

咱們都知道,java提供了一些列併發安全的原子操做類,如AtomicIntegerAtomicLong.下面咱們拿AtomicInteger爲例分析其源碼實現。編程

// 一、獲取UnSafe實例對象,用於對內存進行相關操做
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 二、內存偏移量
private static final long valueOffset;

static {
    try {
        // 三、初始化地址偏移量
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

// 四、具體值,使用volatile保證可見性
private volatile int value;
複製代碼

從上面代碼中咱們能夠看出,AtomicInteger中依賴於一個叫Unsafe的實例對象,咱們都知道,java語言屏蔽了像C++那樣直接操做內存的操做,程序員不需手動管理內存,但話說回來,java仍是開放了一個叫Unsafe的類直接對內存進行操做,由其名字能夠看出,使用Unsafe中的操做是不安全的,要當心謹慎。安全

valueOffset是對象的內存偏移地址,經過Unsafe對象進行初始化,有一點須要注意的是,對於給定的某個字段都會有相同的偏移量,同一類中的兩個不一樣字段永遠不會有相同的偏移量。也就是說,只要對象不死,這個偏移量就永遠不會變,能夠想象,CAS所依賴的第一個參數(內存地址值)正是經過這個地址偏移量進行獲取的。bash

value屬於共享資源,藉助volatile保證內存可見性,關於volatile的簡單分析,能夠參考併發

Java 反彙編、反編譯、volitale解讀編程語言

// 一、獲取並增長delta
public final int getAndAdd(int delta) {
    return unsafe.getAndAddInt(this, valueOffset, delta);
}
// 二、加一
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
複製代碼

上面兩個方法依賴下面Unsafe類中的getAndAddInt操做,藉助openjdk提供的Unsafe源碼,咱們看下其實現:ide

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    // 一、不斷的循環比較,直到CAS操做成功返回
    do {
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}
複製代碼

從上面能夠看出,本質上CAS使用了自旋鎖進行自旋,直到CAS操做成功,若是很長一段時間都沒有操做成功,那麼將一直自旋下去。高併發

3、CAS的缺點

從第1、二節能夠看出,CAS在java中的實現本質上是使用Unsafe類提供的方法獲取對象的內存地址偏移量,進而經過自旋實現的。CAS的優勢很明顯,那就是區別於悲觀策略,樂觀策略在高併發下性能表現更好,固然CAS也是有缺點的,主要有相似ABA自旋時間過長只能保證一個共享變量原子操做三大問題,下面咱們一一分析。源碼分析

一、ABA

什麼是ABA呢?簡單的說,就是有兩個線程,線程A和線程B,對於同一個變量X=0,A準備將X置爲10,按照CAS的步驟,首先會從內存讀取值舊的預期值0,而後比較,最後置爲10,但就在A讀取完X=0後,還沒來得及比較和賦值,此時線程B完成了X=0 -> X=10 -> X=0這3個操做,隨後A繼續執行比較,發現此時內存的值依舊是0,最後CAS執行成功。雖然過程和結果沒有問題,可是A比較時的0已經不是最初那個0了,有種被偷樑換柱的感受。

下面代碼舉例演示ABA問題,線程1模擬將變量從100->110->100,線程2執行100->120,最後看下輸出:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * 
 * @Author jiawei huang
 * @Since 2020年1月17日
 * @Version 1.0
 */
public class ABATest {
    // 初始值爲100
    private static AtomicInteger atomicInteger = new AtomicInteger(100);
    public static void main(String[] args) throws InterruptedException {
    
    	// AtomicInteger實現 100->110->100
    	Thread t1 = new Thread(new Runnable() {
    		@Override
    		public void run() {
    			atomicInteger.compareAndSet(100, 110);
    			atomicInteger.compareAndSet(110, 100);
    		}
    	});
    
    	// 實現 100->120
    	Thread t2 = new Thread(new Runnable() {
    		@Override
    		public void run() {
    			try {
    				// 這裏模擬線程1執行完畢,偷樑換柱成功
    				TimeUnit.SECONDS.sleep(2);
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    			// 下面依舊返回true
    			System.out.println("AtomicInteger:" + atomicInteger.compareAndSet(100, 120));
    		}
    	});
    
    	t1.start();
    	t2.start();
	}
}
複製代碼

輸出結果爲:

AtomicInteger:true
複製代碼

可見線程2中的CAS也執行成功了,那麼如何解決這個問題呢?解決方案是經過版本號,Java提供了AtomicStampedReference來解決。AtomicStampedReference經過包裝[E,Integer]的元組來對對象標記版本戳stamp,從而避免ABA問題。

/*
 * Copyright (C) 2011-2019 DL 
 * 
 * All right reserved.
 * 
 * This software is the confidential and proprietary information of DL of China.
 * ("Confidential Information"). You shall not disclose such Confidential 
 * Information and shall use it only in accordance with the argeements 
 * reached into with DL himself.
 *
 */
package com.algorithm.leetcode.linkedlist;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * 
 * @Author jiawei huang
 * @Since 2020年1月17日
 * @Version 1.0
 */
public class ABATest {
    // 初始值100,版本號1
    private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(100, 1);
    
    public static void main(String[] args) throws InterruptedException {
    // AtomicStampedReference實現
    Thread tsf1 = new Thread(new Runnable() {
    	@Override
    	public void run() {
    		try {
    			// 讓 tsf2先獲取stamp,致使預期時間戳不一致
    			TimeUnit.SECONDS.sleep(2);
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    		// 預期引用:100,更新後的引用:110,預期標識getStamp() 更新後的標識getStamp() + 1
    		atomicStampedReference.compareAndSet(100, 110, atomicStampedReference.getStamp(),
    				atomicStampedReference.getStamp() + 1);
    		atomicStampedReference.compareAndSet(110, 100, atomicStampedReference.getStamp(),
    				atomicStampedReference.getStamp() + 1);
    	}
    });
    
    Thread tsf2 = new Thread(new Runnable() {
    	@Override
    	public void run() {
        	int stamp = atomicStampedReference.getStamp();
        
        	try {
        		TimeUnit.SECONDS.sleep(2); // 線程tsf1執行完
        	} catch (InterruptedException e) {
        		e.printStackTrace();
        	}
        	System.out.println(
        			"AtomicStampedReference:" + atomicStampedReference.compareAndSet(100, 120, stamp, stamp + 1));
    	}
    });
    
    tsf1.start();
    tsf2.start();
	}
}
複製代碼

輸出結果:

AtomicStampedReference:false
複製代碼

能夠看出線程1執行失敗了。

二、自旋時間過長

經過第二節分析能夠得知,CAS本質上是經過自旋來判斷是否更新的,那麼問題來了,若是屢次舊預期值不等於內存值的狀況,那麼這個自旋將會自旋下去,而自旋太久將會致使CPU利用率變高。

三、只能保證一個共享變量原子操做

從第二節能夠看出,只是單純對單個共享對象進行CAS操做,保證了其更新獲取的原子性,沒法對多個共享變量同時進行原子操做。這是CAS的侷限所在,但JDK提供同時了AtomicReference類來保證引用對象之間的原子性,能夠把多個變量放在一個對象裏來進行CAS操做。

相關文章
相關標籤/搜索