CAS
簡介
CAS的全稱是“比較并交換”,是一種無鎖的原子操作,其體現了樂觀所的思想,在無鎖的情況下保證線程操作共享數據的原子性。
CAS一共有3個值:
1、V:要更新的值;
2、E:預期值;
3、N:新值。
在比較和交換的過程中,需要比較V與E是否相等,若相等,則將V設置為N;若不相等,則說明有其他線程對共享變量進行了更新,當前線程應放棄。若CAS失敗,則繼續嘗試獲取新值,直至更新成功。
CAS底層實現
CAS底層是通過Unsafe類來直接調用操作系統底層的CAS指令,Unsafe類中的方法都是native方法,由系統提供接口實現,Unsafe類對CAS實現是通過C++進行的。
CAS存在的問題
ABA問題
ABA問題是指一個位置原來是A,后面被改為B,再后來又被改為A,進行CAS操作的線程無法知道在該位置的值發生過改變。
可通過加入時間戳或版本號的方式,解決ABA問題,在進行CAS操作時,需要值和版本號(或時間戳)均匹配時才能進行修改操作。在Java中,AtomicStampedReference類就實現了這種機制,它會同時檢查引用值和stamp是否都相等。
循環性能開銷問題
CAS在進行自旋操作時,若一直不成功,則會給CPU代理巨大的開銷,可通過限制自旋次數來解決這個問題。
只能保證一個變量的原子操作
CAS只能保證對一個變量進行原子操作,當存在多個變量時,CAS無法直接保證對他們的原子操作。可通過以下兩種方式解決:
1、考慮改用鎖實現原子操作;
2、合并多個變量,將多個變量封裝成一個對象,通過AtomicReference來保證原子性。
volatile
volatile的主要作用
保證變量在線程間的可見性
當一個線程修改某個變量的值時,volatile關鍵字會將修改的值刷新到主存中,該新值就對其他線程可見,對于volatile修飾的關鍵字,禁止使用JIT(即時編譯器)進行優化。
禁止指令重排序
使用volatile修飾的變量,會在讀、寫共享變量時加上屏障,阻止其他讀寫操作越過屏障對共享變量進行讀寫操作。
對于volatile的寫操作,會在其前后加上屏障,其中在寫操作前的屏障,禁止前面的普通讀操作與該寫操作重排;寫操作后的屏障,禁止后面的volatile讀寫操作與該寫操作重排序,如下圖所示:
對于volatile的讀操作,會在其后面加上屏障,禁止后面的普通讀寫操作與重排序,該屏障強制讓本地內存中的變量值失效,從而重新從主內存中讀取最新的值,如下圖所示:
對于volatile的寫操作前的操作,不會被編譯器重排到該寫操作后;對于volatile的讀操作后的操作,不會被編譯器重排到該寫操作前。
AQS
AQS是阻塞式鎖和相關同步工具的框架,中文名為抽象隊列同步器,它是構建鎖或其他同步組件的基礎框架。其思想是,若被請求的資源空閑,則線程申請該資源成功,反之該線程則進入一個等待隊列,其他線程釋放該資源時,系統隨機選擇一個在等待隊列中的線程,賦予其資源。
AQS是悲觀鎖,通過Java實現,其開啟和釋放都需要手動進行,其工作機制是:
同步狀態state由volatile修飾,保證其他線程對其可見,同時采用一個FIFO雙端隊列存儲線程,如下圖所示:
當線程要獲取鎖時,會嘗試改變state的狀態,若state狀態為0,則可將其改為1,該線程搶占鎖成功。若線程強制鎖失敗,則線程進入FIFO隊列中等待。
AQS通過CAS自旋鎖保證線程的原子性,保證每次只能有一個線程修改state成功。
AQS既可實現公平鎖,又可實現非公平鎖。當新來的線程與隊列中的線程共同強資源時,該鎖為非公平鎖(如AQS實現類ReentrantLock),新來的線程進入等待隊列中等待,只允許隊列中head線程占用資源時,該鎖為公平鎖。
ReentrantLock
概述
ReentrantLock是可重入鎖,其可中斷、可設置超時時間、可設置公平鎖、支持多個條件變量、支持重入。ReentrantLock主要利用CAS+AQS實現的,通過new ReentrantLock()創建的鎖默認為非公平鎖,要將其設置為公平鎖,則應該通過有參構造函數的方式創建,并將變量設置為true(該變量設置為false時實現的非公平鎖),代碼:
//實現公平鎖
ReentrantLock lock = new ReentrantLock(true);
構造函數如下圖:
NonfairSync、FairSync的父類為Sync,Sync的父類為AQS。
加鎖
調用lock()方法可實現加鎖,unlock()方法實現解鎖。在公平鎖的條件下,鎖會授予給等待時間最長的線程,在非公平鎖的條件下,其加鎖的方式如下:
當線程調用lock()嘗試獲取鎖時,首先通過CAS方式修改state變量,若成功將其修改為1,則讓exclusiveOwnerThread線程指向這個線程,該線程獲取鎖成功。若修該失敗,則線程獲取鎖失敗,則線程進入等待隊列中。當exclusiveOwnerThread為null時,則持有鎖的線程釋放了鎖,則會喚醒雙向隊列中在head位置的線程,公平鎖和非公平鎖的情況見上述AQS介紹。
為了實現鎖的可重入,ReentrantLock內部有一個計數器跟蹤線程持有鎖的次數,當線程首次獲取鎖時,計數器的值變為1,如果同一線程再次獲取鎖,計數器增加;每釋放一次鎖,計數器減 1。當線程調用unlock()方法時,ReentrantLock會將持有鎖的計數減1,如果計數到達0,則釋放鎖,并喚醒等待隊列中的線程來競爭鎖。
Synchronized和Lock對比
1、Synchronized是關鍵字,源碼在JVM中,通過C++實現;而Lock是接口,源碼由JDK提供,通過Java實現;
2、使用Synchronized時,退出同步代碼塊會自動釋放鎖,而使用lock時,需要通過unlock()釋放;
3、兩種都是悲觀鎖,支持互斥、同步、鎖重入等功能;
4、相比于Synchronized,Lock支持獲取鎖狀態、設置公平鎖、可打斷、可超時等,同時支持ReentrantLock、ReentrantReadWriteLock不同適合條件的實現;
5、在沒有競爭時,Synchronized做了如偏向鎖、輕量級鎖等優化,但在競爭激烈時,Lock的性能更好。