通過實現網站訪問計數器帶你理解 輕量級鎖CAS原理,還學不會算我輸!!!


一、實現網站訪問計數器

1、線程不安全的做法

1.1、代碼

package com.chentongwei.concurrency;

import static java.lang.Thread.sleep;

/**
 * @Description:
 * @Project concurrency
 */
public class TestCount {

    private static int count;

    public void incrCount() {
        count ++;
    }

    public static void main(String[] args) throws InterruptedException {
        TestCount testCount = new TestCount();
        // 開啟五個線程
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 每個線程都讓count自增100
                for (int j = 0; j < 100; j++) {
                    testCount.incrCount();
                }
            }).start();
        }
        sleep(2000);
        // 正確的情況下會輸出500
        System.out.println(count);
    }
}

 

1.2、結果

並不一定是500,極大可能小於500。不固定。

1.3、分析

很明顯上面那段程序是線程不安全的,為什么線程不安全?因為++操作其實是類似如下的兩步驟,如下:

count ++;
||
// 獲取count
int temp = count;
// 自增count
count = temp + 1;

 

很明顯是先獲取在自增,那么問題來了,我線程A和線程B都讀取到了int temp = count;這一步,然后都進行了自增操作,其實這時候就錯了因為這時候count丟了1,並發了。所以導致了線程不安全,結果小於等於500。

2、Synchronized保證線程安全

2.1、代碼

package com.chentongwei.concurrency;

import static java.lang.Thread.sleep;

/**
 * @Description:
 * @Project concurrency
 */
public class TestCount {

    private static int count;

    public  void incrCount() {
        count ++;
    }

    public static void main(String[] args) throws InterruptedException {
        TestCount testCount = new TestCount();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for (int j = 0; j < 100; j++) {
                    synchronized (TestCount.class) {
                        testCount.incrCount();
                    }
                }
            }).start();
        }
        sleep(2000);
        System.out.println(count);
    }
}

 

2.2、結果

500

2.3、分析

沒什么可分析的,我用了Java的內置鎖Synchronized來保證了線程安全性。加了同步鎖之后,count自增的操作變成了原子性操作,所以最終輸出一定是500。眾所周知性能不好,所以繼續往下看替代方案。

3、原子類保證線程安全

3.1、代碼

package com.chentongwei.concurrency;

import java.util.concurrent.atomic.AtomicInteger;

import static java.lang.Thread.sleep;

/**
 * @Description:
 * @Project concurrency
 */
public class TestCount {
 
    // 原子類
    private static AtomicInteger count = new AtomicInteger();

    public  void incrCount() {
        count.getAndIncrement();
    }

    public static void main(String[] args) throws InterruptedException {
        TestCount testCount = new TestCount();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for (int j = 0; j < 100; j++) {
                    testCount.incrCount();
                }
            }).start();
        }
        sleep(2000);
        System.out.println(count);
    }
}

 

3.2、結果

500

3.3、分析

所謂原子操作類,指的是java.util.concurrent.atomic包下,一系列以Atomic開頭的包裝類。如AtomicBoolean,AtomicUInteger,AtomicLong。它們分別用於Boolean,Integer,Long類型的原子性操作。每個原子類內部都采取了CAS算法來保證的線程安全性。

二、什么是CAS算法

1、概念

CAS的英文單詞Compare and Swap的縮寫,翻譯過來就是比較並替換。

2、原理

CAS機制中使用了3個基本操作數:內存地址V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,才將內存值修改為B,否則什么都不做,最后返回現在的V值。

簡單理解為這句話:我認為V的值應該是A,如果是A的話我就把他改成B,如果不是A的話(那就證明被別人修改過了),那我就不修改了,避免多人 同時修改導致數據出錯。換句話說:要想修改成功,必須保證A和V中的值是一樣的,修改前有個對比的過程。

比如:更新一個變量,只有當變量的預期值(A)和內存地址(V)的實際值相同時,才會將內存地址(V)對應的值修改為B。

我們看如下的原理圖:

1、在內存地址V當中,存儲着值為10的變量。

通過實現網站訪問計數器帶你理解 輕量級鎖CAS原理,還學不會算我輸!!!

2、此時線程1想把變量的值增加1,對於線程1來說,舊的預期值A=10,要修改的新值B=11。

通過實現網站訪問計數器帶你理解 輕量級鎖CAS原理,還學不會算我輸!!!

3、在線程1要提交更新之前,另一個線程2搶先一步,把內存地址V中的變量率先更新成了11。

通過實現網站訪問計數器帶你理解 輕量級鎖CAS原理,還學不會算我輸!!!

4、線程1開始提交更新,首先進行A和地址V的實際值對比,發現A!=V,提交失敗。

通過實現網站訪問計數器帶你理解 輕量級鎖CAS原理,還學不會算我輸!!!

5、線程1重新獲取內存地址V的當前值,並重新計算想要修改的值。此時對線程1來說:A=11,B=12.這個重新嘗試的過程稱為自旋

通過實現網站訪問計數器帶你理解 輕量級鎖CAS原理,還學不會算我輸!!!

6、這一次比較幸運,沒有其他線程改變地址V的值。線程1進行比較,發現A和地址V的實際值是相等的。

通過實現網站訪問計數器帶你理解 輕量級鎖CAS原理,還學不會算我輸!!!

7、線程1進行交換,把地址V的值替換為B,也就是12.

通過實現網站訪問計數器帶你理解 輕量級鎖CAS原理,還學不會算我輸!!!

3、對比Synchronized

從思想上來講,Synchronized屬於悲觀鎖,悲觀的認為程序中的並發情況嚴重,所以嚴防死守,高並發情況下效率低下。而CAS屬於樂觀鎖,樂觀的認為程序中的並發情況不那么嚴重,所以讓線程不斷去重試更新。但實際上Synchronized已經改造了,帶有鎖升級的功能。效率不亞於cas。

4、CAS缺點

(1)CPU開銷可能過大

在並發比較大的時候,若多線程反復嘗試更新某個變量,卻又一直更新不成功,循環往復,會給CPU帶來很大的壓力。(因為是個死循環,下面分析底層實現就懂了。)

(2)不能保證代碼塊的原子性

CAS機制所保證的只是一個變量的原子操作,而不能保證整個代碼塊的原子性。比如需要保證三個變量共同進行原子性的更新,就不得不使用Synchronized或Lock等機制了。

(3)ABA問題。

下面會單獨抽出一塊地來詳細講解。這是CAS最大的漏洞。

三、CAS底層實現(Java)

1、概述

要說Java中CAS的案例,那么最屬java.util.concurrent.atomic包下的原子類有發言權了。最經典、最簡單。

2、講解

比如我們這里隨便找個AtomicInteger來講解CAS算法底層實現。

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

private volatile int value; 

public final int get() {
    return value;
}

 

  1. 獲取當前值

  2. 當前值+1,計算出目標值

  3. 進行CAS操作,如果成功則跳出循環,如果失敗則重復上述步驟

如何保證獲取的當前值是內存中的最新值?很簡單,用volatile關鍵字來保證(保證線程間的可見性)。

通過實現網站訪問計數器帶你理解 輕量級鎖CAS原理,還學不會算我輸!!!

compareAndSet方法的實現很簡單,只有一行代碼。這里涉及到兩個重要的對象,一個是unsafe,一個是valueOffset。

什么是unsafe呢?

3、Unsafe

Unsafe是CAS的核心類,Java語言不像C,C++那樣可以直接訪問底層操作系統,Java無法直接訪問底層操作系統,但是JVM為我們提供了一個后門,這個后門就是unsafe。unsafe為我們提供了硬件級別的原子操作

而valueOffset是通過unsafe.objectFiledOffset方法得到,所代表的是AtomicInteger對象value成員變量在內存中的偏移量。我們可以簡單的把valueOffset理解為value變量的內存地址。

我們上面說過,CAS機制中使用了3個基本操作數:內存地址V,舊的預期值A,要修改的新值B。

而unsafe的compareAndSwapInt方法的參數包括了這三個基本元素:valueOffset參數代表了V,expect參數代表了A,update參數代表了B。

正是unsafe的compareAndSwapInt方法保證了Compare和Swap操作之間的原子性操作。

四、ABA問題

1、演示

線程1准備用CAS將變量的值由A替換為B,在此之前,線程2將變量的值由A替換為C,又由C替換為A。然后線程1執行CAS時發現變量的值仍是A,所以CAS成功,這么看沒毛病,但是如果操作的是個鏈表呢?那就炸了,因為雖然值一樣,但是鏈表的位置不一樣了。

例如:

(1)現有一個用單向鏈表實現的堆棧,棧頂為A,這時線程T1已經知道A.next為B,然后希望用CAS將棧頂替換為B:

head.compareAndSet(A,B);

通過實現網站訪問計數器帶你理解 輕量級鎖CAS原理,還學不會算我輸!!!

(2)在T1執行上面這條指令(CAS)之前,線程T2介入,將A、B出棧,在push三個D、C、A,如下:

通過實現網站訪問計數器帶你理解 輕量級鎖CAS原理,還學不會算我輸!!!

(3)此時輪到線程T1執行CAS操作,檢測發現棧頂仍為A,所以CAS成功,棧頂變為B,但實際上B.next為null,因為B已經再上一步被移除了,成為了游離態。所以此時的情況變為

通過實現網站訪問計數器帶你理解 輕量級鎖CAS原理,還學不會算我輸!!!

導致了其中堆棧中只有B一個元素,C和D組成的鏈表不再存在於堆棧中,平白無故就把C、D丟掉了。

以上就是由於ABA問題帶來的隱患,各種樂觀鎖的實現中通常都會用版本戳version來對記錄或對象標記,避免並發操作帶來的問題,在Java中,AtomicStampedReference<E>也實現了這個作用,它通過包裝[E,Integer]的元組來對對象標記版本戳stamp,從而避免ABA問題。

2、生活案例

你和你前任分手后她又回來了,但是你在這期間又和其他女人...,你表面還是你,但是本質的你已經變了。把這個例子帶到代碼里來就是:

你有個class,里面有個LinkedList屬性,這個鏈表里有你和你前任,你先把它踹了,然后小蒼進來跟你...,這時候你前任就回來了,但是這期間鏈表已經發生了無感知的變化。`


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM