Java實習生面試複習(十一):什麼是CAS?

我是一名很普通的大三學生。我會堅持寫博客,輸出知識的同時鞏固自己的基礎,記錄自己的成長和鍛鍊自己,奧利給!!

如果你覺得內容對你有幫助的話,不如給個贊,鼓勵一下更新😂。

加油啊!新的一天,新的開始😘

CAS

說到CAS,我們就得提起通常所說的併發包也就是 java.util.concurrent(JUC)及其子包,它集中了 Java 併發的各種基礎工具類,具體主要包括幾個方面:

  • 提供了比 synchronized 更加高級的各種同步結構 CountDownLatch、Semaphore 等,可以實現更加豐富的多線程操作,比如 Semaphore 作爲資源控制器,可以限制同時進行工作的線程數量。
  • 各種線程安全的容器,比如面試常問到的 ConcurrentHashMap或者通過類似快照機制,實現線程安全的動態數組 CopyOnWriteArrayList 等。
  • 各種併發隊列實現,如各種 BlockedQueue 實現,比較典型的 ArrayBlockingQueue、 SynchorousQueue 或針對特定場景的 優先隊列 PriorityBlockingQueue 等。
  • 強大的 Executor 框架,可以創建各種不同類型的線程池,調度任務運行等,絕大部分情況下,不再需要自己從頭實現線程池和任務調度器。

如果想要讀懂 Java 中的併發包,其核心就是要先讀懂 CAS 機制,因爲 CAS 在併發包中可以說是隨處可見,這篇文章我們就來簡單學習一下CAS

神祕的CAS?

CAS全稱Compare and swap,字面意思就是比較並交換,一個 CAS 涉及到以下操作:

我們假設內存中的原數據爲V,舊的預期值爲A,需要修改的新值爲B。
1、比較 A 與 V 是否相等。
2、如果比較相等,將 B 寫入 V。
3、否則,可能出現不同的選擇,要麼進行重試(自旋),要麼就返回一個成功或者失敗的結果。

當多個線程同時對某個資源進行CAS操作,只能有一個線程操作成功,但是並不會阻塞其他線程,其他線程只會收到操作失敗的信號。所以 CAS 其實是一個樂觀鎖。

補充:什麼是樂觀鎖?
樂觀鎖就是在你操作數據的時候認爲不加鎖也沒事,然後我們可以先不加鎖,當出現了衝突的時候,我們在想辦法解決。

CAS 的實現

文章開頭我有提到提高JUC這個包,其實在 Java實習生面試複習(八):volatile的學習 文章中我也有提及這個特殊的類,就是以Atomic開頭的支持原子性操作的類,比如AtomicInteger,它是對 int 類型的一個封裝,提供原子性的訪問和更新操作,其原子性操作的實現是基於 CAS(compare-and-swap)技術。

那麼它又是怎麼實現的CAS呢?跟隨AtomInteger的代碼我們一路往下 => 從 AtomicInteger 的內部屬性可以看出,它依賴於 Unsafe 提供的一些底層能力,進行底層操作;以 volatile 的 value 字段,記錄數值,以保證可見性。

    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); }
    }

    private volatile int value;

具體的原子操作細節,可以參考AtomInteger中的 getAndIncrement。其底層就是Unsafe 會利用 value 字段的內存地址偏移,直接完成操作。

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

也就是說,這個 CAS 的方法是使用了本地的方法。所以這幾個方法的具體實現就需要我們自己去 jdk 的源碼中搜索了。

再往下,,我也不會了,哈哈😭,菜哭,以後有機會再深入看看。

CAS 的應用

瞭解完 CAS 的原理我們繼續看看 CAS 的相關應用

併發修改的情況下,使用CAS版本號機制保證數據的一致性

模擬的場景如下:

  • 先從用戶表中讀取到用戶的餘額
  • 在內存中對餘額進行+1操作
  • 把+1後的餘額寫到DB
建表SQL

CREATE TABLE `user` (
  `id` int(11) NOT NULL COMMENT 'id',
  `balance` int(255) DEFAULT NULL COMMENT '餘額',
  `version` int(255) DEFAULT NULL COMMENT '版本號',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_croatian_ci;

INSERT INTO `demo`.`user`(`id`, `balance`, `version`) VALUES (1, 0, 0);

Main 啓動 100個線程對user表中,id爲1的balance字段進行: 讀取 , +1 ,寫入的操作

import java.sql.DriverManager;
import java.sql.SQLException;
public class Main {
	
	private static final String MYSQL_URL = "jdbc:mysql://localhost:3306/demo?serverTimezone=GMT%2b8";
	private static final String USER = "root";
	private static final String PASS = "root";
	public static void main(String[] args) {
		Service service = new Service();
		for (int x = 0; x < 100; x++) {
			new Thread(() -> {
				try {
					service.increment(DriverManager.getConnection(MYSQL_URL, USER, PASS));
				} catch (SQLException e) {
					e.printStackTrace();
				}
			}).start();
		}
	}

	static {
		try {
			Class.forName("com.mysql.cj.jdbc.Driver");
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}
}

Service

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class Service {

	private static final int USER_ID = 1;

	// cas 樂觀鎖
	public void incrementCas(Connection connection) throws SQLException {
		
		connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
		
		while (true) {
			
			connection.setAutoCommit(false);
			
			// 從DB檢索到餘額和版本號
			PreparedStatement preparedStatement = connection.prepareStatement("SELECT `balance` ,`version` FROM `user` WHERE `id` = ?;");
			preparedStatement.setInt(1, USER_ID);
			ResultSet resultSet = preparedStatement.executeQuery();
			int balance = 0;
			int version = 0;
			if (resultSet.next()) {
				balance = resultSet.getInt("balance");
				version = resultSet.getInt("version");
			}

			// +1後寫入到DB
			preparedStatement = connection.prepareStatement("UPDATE `user` SET `balance` = ? ,`version` = `version` + 1 WHERE `id` = ? AND `version` = ?;");
			preparedStatement.setInt(1, balance + 1);
			preparedStatement.setInt(2, USER_ID);
			preparedStatement.setInt(3, version);		// 版本號
			int result = preparedStatement.executeUpdate();
			
			connection.commit();
			
			if(result != 0) {
				break;
			}
			// 更新失敗,再次進入循環
		}
	}
}

簡單自旋鎖實現

自旋鎖,在lock()的時候,一直while()循環,直到 cas 操作成功爲止。

擴展:這裏還可以考慮那種能夠根據線程最近獲得鎖的狀態來調整循環次數的自旋鎖,我們稱之爲自適應自旋鎖,因爲普通的自旋鎖會由於自旋的次數,出現過度消耗 CPU的問題。

public class SpinLock {

  private AtomicReference<Thread> sign =new AtomicReference<>();

  public void lock(){
    Thread current = Thread.currentThread();
    while(!sign .compareAndSet(null, current)){
    }
  }

  public void unlock (){
    Thread current = Thread.currentThread();
    sign .compareAndSet(current, null);
  }
}

CAS 缺點

誰偷偷更改了我的值(ABA問題)

著名的ABA問題。前面提到 CAS 是在更新時比較前值,如果對方只是恰好相同,例如期間發生了 A -> B -> A 的更新,僅僅判斷數值是 A,可能導致不合理的修改操作。針對這種情況,Java 提供了 AtomicStampedReference 工具類,通過爲引用建立類似版本號(stamp)的方式,來保證 CAS 的正確性。

對於基本類型的值來說,這種把數字改變了在改回原來的值是沒有太大影響的,但如果是對於引用類型的話,就會產生很大的影響了。

那麼我們怎麼解決?

爲了解決這個 ABA 的問題,我們可以引入版本控制,例如,每次有線程修改了引用的值,就會進行版本的更新,雖然兩個線程持有相同的引用,但他們的版本不同,這樣,我們就可以預防 ABA 問題了。
Java 中提供了 AtomicStampedReference 這個類,就可以進行版本控制了,我們可以看個簡單的案例。

/**
 * @author 南街
 * @program JavaAdvanced
 * @classname AtomicStampedReferenceDemo
 * @description 版本號原子引用,可用於解決ABA問題
 **/
public class AtomicStampedReferenceDemo {

    private static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
    private static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<Integer>(100,1);;

    public static void main(String[] args) {
        System.out.println("==========以下是ABA問題的產生==============");
        new Thread(()->{
            atomicReference.compareAndSet(100,101);
            atomicReference.compareAndSet(101,100);
        },"t1").start();
        Thread t2 = new Thread(() -> {
            // 暫停一秒鐘t2線程,保證上面的t1線程完成一次ABA操作
            try {
                TimeUnit.SECONDS.sleep(1);
                System.out.println(atomicReference.compareAndSet(100, 200) + "\t" + atomicReference.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2");
        t2.start();
        while (t2.isAlive()) {

        }
        System.out.println("==========以下是ABA問題的解決==============");

        new Thread(() -> {
            System.out.println("第一次版本號stamp = " + stampedReference.getStamp());
            // 暫停3秒鐘t3線程
            try {
                TimeUnit.SECONDS.sleep(3);
                stampedReference.compareAndSet(100, 101, stampedReference.getStamp(),
                        stampedReference.getStamp() + 1);
                System.out.println("第二次版本號stamp = " + stampedReference.getStamp());
                stampedReference.compareAndSet(101, 100, stampedReference.getStamp(),
                        stampedReference.getStamp() + 1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("第三次版本號stamp = " + stampedReference.getStamp());
        },"t3").start();
        new Thread(() -> {
            int stamp = stampedReference.getStamp();
            System.out.println("t4第一次版本號stamp = " + stamp);
            // 暫停5秒鐘t4線程
            try {
                TimeUnit.SECONDS.sleep(5);
                System.out.println("t4修改:" + stampedReference.compareAndSet(100, 2019, stamp,
                        stamp + 1));
                System.out.println("stampedReference.getReference() = " + stampedReference.getReference());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"t4").start();
    }
}

嘿嘿,運行結果的話,自己不妨複製下來跑跑看,程序還是要多實踐滴🐱‍👓!

總結

  1. CAS 是 Java 併發中所謂 lock-free(無鎖) 機制的基礎。
  2. CAS 的使用能夠避免線程的阻塞。
  3. 多數情況下我們使用的是 while true 直到成功爲止,也叫自旋鎖,在自旋鎖上我們可以優化出自適應自旋鎖。
  4. 使用 AtomicStampedReference 解決ABA問題。

這篇文章到這就結束啦,我們下期📝再見(我也不知道啥時候🤣),喜歡的話就給個贊 + 收藏 + 關注吧!

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