一、CAS概念
package syncbasics; import java.util.concurrent.CountDownLatch; /** * 多線程訪問同一份數據,會產生競爭,race condition => 競爭條件 * 就有可能產生數據的不一致,併發訪問之下產生的不期望出現的結果 * 如何保障數據的一致呢?---->線程同步(線程執行的順序安排好), * 具體:保障操作的原子性(Atomicity) * 1.悲觀的認爲這個操作會被別的線程打斷(悲觀鎖)synchronized * 2.樂觀的認爲這個操作不會被別的線程打斷(樂觀鎖)cas操作 * CAS = Compare And Set/Swap/Exchange * * ++操作: * 把n從內存裏面讀到寄存器裏,加完了之後,再寫回去。 * 還沒來得及寫回去的時候,另外的線程讀到了原值, * 因此,++這個正在執行的操作被另外線程打斷了。 * * 只要我們能保證這個執行的操作不被打斷,即保證我這個線程讀過來之後改完這個值1,再寫回去之後,其他線程才能執行, * 那最後的結果一定是對的。這種不能夠被打斷的操作稱之爲原子操作。 * * 什麼樣的語句是原子性的?什麼樣的不是? * java內存中的8大原子操作,瞭解即可。要查彙編手冊。 * * synchronized: * 讓原來的併發變成了序列化。 * synchronized本身是保證可見性的,n++結束了之後這個線程一定是要和主內存做同步,主內存裏一定都是最新的。 * synchronized保障了可見性、原子性。 * 那麼保證有序性嗎? * ----不可以,synchronized裏面的代碼塊裏面的操作指令完全有可能換順序。DCL要加volatile就是證明了。 * * CAS概念: * 還以下面這個小程序n++操作來說明CAS的概念。 * n開始等於0,線程1把n讀過來加1,原來是需要加鎖的,現在整個過程不上鎖了,把0讀過來改成1之後,再往回寫 * 的過程之中做個判斷,判斷原值依然是否爲0,如果依然爲0,說明在線程1讀0加1的過程中,沒有人來過,那就直接把1 * 寫回去,搞定。 * 萬一中間有人改了呢?萬一其他線程已經將0改成8了,線程1把1往回寫的時候發現原值已經變爲8了,不是你所期望的0, * 這時候怎麼辦?那就再來一遍,把8讀出來加1變成9,把9往回寫的過程之中看看判斷原值是否依然爲8,如依然爲8,說明在我 * 將8改爲9的過程之中沒有其他人來過,那就直接將9寫回去了,搞定。 * 當然,如果將8讀來加1的過程之中又有人打斷了,有人將8改成100了,怎麼辦?那就把100讀過來加1,將101往回寫的過程中判斷 * 原值是否依然是100....一直到某一次成功了爲止。 * 你會發現它就在這裏不停的循環,讀取當前值,計算結果,比較當前值和新值,如果當前值和新值相等,更新爲新值。如果不相等, * 就再來一遍,總有一次能成功。 * * CAS的ABA問題: * 上面的這段話描述裏面有個很重要的問題,此0非彼0的問題,線程1把0讀過來改成了1,往回寫的過程中發現原值依然爲0,但是 * 這個0是不是你所看到的那個0呢?未必,有可能在這個過程之中,這個0被別的線程改成了8,又被別的線程改回了0!中間有個 * 0->8->0的過程,此0非彼0,A->B->A,這就是ABA問題。 * 但是咱們這個程序是不存在這個問題,只是理論上有,簡單數據類型就算ABA問題,但是對我而言沒關係,這種可以不在乎,略過。 * 但是在有些情況下,是要解決的。如果這個值是一個引用的話,讀過來引用值,對它的屬性進行了一些修改,它是一個對象,當你再 * 往回寫的時候,有可能這個引用指向的對象裏面的內容發生了改變,引用依然還是這個引用,但是裏面的內容發生了改變,這時就要 * 在乎這個ABA問題了。 * * 解決ABA問題: * 加version,你的女朋友,你早上離開了她,晚上回來發現依然是她,但此她還是彼她嘛?你比較懷疑,怎麼辦?在她腦門上寫1.0,你就 * 走了,回來的時候依然是她,不過如果中間經過任何其他線程操作,這個ver1.0都會加1。這時候你發現她腦門上是99.0,那肯定中間經過 * 了改變,當然就看你在乎不在乎。所以加version,加版本就可以解決。 * * CAS的底層原子性保障: * 除了ABA問題,CAS還有一個巨大問題。分析一下: * 線程1將0讀過來改成1,把1往回寫的過程中是一個CAS操作。CAS叫compare and swap,compare and set,僞代碼就是: * if(v==0){v=1},這個操作實際上底層就是兩步:比較和設定。那萬一當你執行完if(v==0)後,這個時間點上被另外線程打斷, * 另外線程把0改成8了,那線程1又將8->1,那還是出問題了,數據還是不一致! * 所以,如果想讓CAS產生作用的話,必須保證CAS操作本身必須是原子性的。 * * 悲觀鎖和樂觀鎖的效率: * 不要認爲悲觀鎖的效率一定比樂觀鎖的效率高,CAS一定比上來就上鎖的效率高,不是的,不一定。 * 悲觀鎖一般採用什麼樣的實現呢,一把鎖lock,和這把鎖關聯的隊列,這個隊列用來等待着這把鎖,比如說小明wc蹲着,這把 * 鎖是他的,隊列裏還有小紅、小花、小光、小剛...他們在等着。在隊列裏排着隊,操作系統OS說小紅輪到你了,你出來,可以搶 * 這把鎖了,凡是在隊列裏等待的這些線程,是不消耗CPU資源的。可是與此形成鮮明對比的是,如果這時候小明在這我們用的是樂觀鎖, * 線程會while循環不斷看小明有沒有出來,很多人,小紅、小花...這些人不會坐在那裏安安靜靜的等待,他們會領着褲子原地打轉。 * 這些線程消耗cpu,都是活着的線程,cpu一個是一直要運行他們的while循環,一個是要進行他們的線程切換。而等待隊列裏的cpu是不佔用 * cpu的,他們狀態是parking、waiting或blocked,什麼時候OS說輪到你了你再佔用cpu資源。所以樂觀鎖是要消耗cpu的,消耗的比悲觀鎖要 * 多一些。 * 使用場景: * 悲觀鎖:臨界區執行時間比較長,等的人很多 * 樂觀鎖:時間短,等的人少 * 還是沒有量化啊,實際中該使用悲觀鎖還是樂觀鎖呢?壓測,可以寫兩種比較一下時間,實際中決定。 * 但是實戰中啊,就用synchronized,因爲synchronized現在做了一系列的優化,它內部既有自旋鎖,又有偏向鎖,又有重量級鎖進行 * 自適應的升級過程,自動完成鎖升級,它的效率已經調試的很不錯了。 * */ public class T00_IPlusPlus { private static long n = 0L; public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[100]; CountDownLatch latch = new CountDownLatch(threads.length); for(int i=0; i<threads.length; i++){ threads[i] = new Thread(() -> { for(int j=0; j<10000; j++){ synchronized (T00_IPlusPlus.class) { n++; } } latch.countDown(); }); } for(Thread t : threads){ t.start(); } latch.await(); System.out.println(n); } }
二、AtomicXXX類:
package atomicxxx; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; /** * 這裏就是一種CAS機制 * incrementAndGet每次往回寫的時候都要比較一下,看下是否原值是我期望的那個值。 * incrementAndGet操作自帶原子性: * unsafe.getAndAddInt->this.compareAndSwapInt->native boolean compareAndSwapInt * 得益於CPU在彙編級別上支持指令:cmpxchg,但是cmpxchg不是原子性的,最終實現: * lock cmpxchg * 所以你會發現,CAS在宏觀上我們叫做自旋鎖,樂觀鎖,但它在底層上的實現,微觀上的實現實際上是一個悲觀鎖。 */ public class T01_AtomicInteger { AtomicInteger count = new AtomicInteger(0); void m(){ for(int i=0; i<10000; i++){ count.incrementAndGet(); } } public static void main(String[] args) { T01_AtomicInteger t = new T01_AtomicInteger(); List<Thread> threads = new ArrayList<>(); for(int i=0; i<100; i++){ threads.add(new Thread(t::m, "thread-" + i)); } threads.forEach(o -> o.start()); threads.forEach(o -> { try { o.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); System.out.println(t.count); } }
---