一、實作網站訪問計數器
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的變數,

2、此時執行緒1想把變數的值增加1,對于執行緒1來說,舊的預期值A=10,要修改的新值B=11,

3、在執行緒1要提交更新之前,另一個執行緒2搶先一步,把記憶體地址V中的變數率先更新成了11,

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

5、執行緒1重新獲取記憶體地址V的當前值,并重新計算想要修改的值,此時對執行緒1來說:A=11,B=12.這個重新嘗試的程序稱為自旋,

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

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

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,計算出目標值
-
進行CAS操作,如果成功則跳出回圈,如果失敗則重復上述步驟
如何保證獲取的當前值是記憶體中的最新值?很簡單,用volatile關鍵字來保證(保證執行緒間的可見性),

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

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

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

導致了其中堆疊中只有B一個元素,C和D組成的鏈表不再存在于堆疊中,平白無故就把C、D丟掉了,
以上就是由于ABA問題帶來的隱患,各種樂觀鎖的實作中通常都會用版本戳version來對記錄或物件標記,避免并發操作帶來的問題,在Java中,AtomicStampedReference<E>也實作了這個作用,它通過包裝[E,Integer]的元組來對物件標記版本戳stamp,從而避免ABA問題,
2、生活案例
你和你前任分手后她又回來了,但是你在這期間又和其他女人...,你表面還是你,但是本質的你已經變了,把這個例子帶到代碼里來就是:
你有個class,里面有個LinkedList屬性,這個鏈表里有你和你前任,你先把它踹了,然后小蒼進來跟你...,這時候你前任就回來了,但是這期間鏈表已經發生了無感知的變化,`
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/142942.html
標籤:Java
