最近加班太多,好久沒有更新博客了,週末看了下阿里大神併發的書籍,把知識點作個記錄。node
一:線程安全的定義算法
當多個線程併發訪問某個類時,若是不用考慮運營環境下的調度和交替運行,且不須要額外的輔助,這裏認爲這個類就是線程安全的。數據庫
原子操做的描述:一個操做單元要麼所有成功,要麼所有失敗。後面再看看數據庫中關於事務的ACID,這裏A表示的就是原子特性,一個事務單元要麼成功,要麼失敗(數據庫中的單機事務依據的是undo),與I要作好區分。緩存
二:關於指令重排序安全
JVM能夠根據機器的特性(我以爲主要是cpu的多級緩存、多核處理),適當的從新排序機器指令,使機器指令更加符合cpu執行特色,發揮機器的最大性能。服務器
三:Happens-Before多線程
沒啥好說的了,動做A\B的順序關係。happens-before有一堆的亂七八糟規則。併發
四:volatile語義app
volatile至關於synchronized的弱實現,說的通白點,就是volatile實現了後者的語義,可是沒有後者的鎖機制。高併發
volatile不會被緩存在寄存器當中或者其餘cpu不可見的地方,每次都是從主存中讀取最新的結果值。
可是,可是,volatile並不能保證線程安全。
五:比較重要的概念,CAS
引入這個概念之前,看下咱們用鎖解決併發會致使的問題:
1.在高併發場景,加鎖、釋放鎖會致使頻繁的上下文切換,引起性能問題。(因爲我作遊戲服務器,以前測試機器人200同頻戰鬥,io線程數量設置爲cpu*2並無cpu的執行效率高)。
2.一個線程持有鎖,其餘的線程被掛起。這裏有可能某個優先級的高的線程等待優先級低的線程,從而致使優先級致使,會不會引起性能風險呢?
什麼是獨佔鎖,悲觀鎖,樂觀鎖?
獨佔鎖也是悲觀鎖,synchronized就是悲觀鎖。反之,更有效的就是樂觀鎖,假設有衝突就重試,直到完成。
CAS:compare and swap
cas的三個操做數:內存值V,舊的預期值A,要修改的新值B,僅當A==V時,才把V改爲B。
現代cpu提供了特殊的指令,能夠自動更新共享的數據,而且可以檢測到其餘線程的干擾。
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
在這裏採用了CAS操做,每次從內存中讀取數據而後將此數據和+1後的結果進行CAS操做,若是成功就返回結果,不然重試直到成功爲止。
而compareAndSet利用JNI來完成CPU指令的操做。
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
總體的過程就是這樣子的,利用CPU的CAS指令,同時藉助JNI來完成Java的非阻塞算法。其它原子操做都是利用相似的特性完成的。
CAS引發的ABA問題?
六:關於AQS概念的引入
號稱J.U.C最複雜的一個類,我從網上找到一些,後面略過。。。
基本的思想是表現爲一個同步器,支持下面兩個操做:
獲取鎖:首先判斷當前狀態是否容許獲取鎖,若是是就獲取鎖,不然就阻塞操做或者獲取失敗,也就是說若是是獨佔鎖就可能阻塞,若是是共享鎖就可能失敗。另外若是是阻塞線程,那麼線程就須要進入阻塞隊列。當狀態位容許獲取鎖時就修改狀態,而且若是進了隊列就從隊列中移除。
while(synchronization state does not allow acquire){
enqueue current thread if not already queued;
possibly block current thread;
}
dequeue current thread if it was queued;
釋放鎖:這個過程就是修改狀態位,若是有線程由於狀態位阻塞的話就喚醒隊列中的一個或者更多線程。
update synchronization state;
if(state may permit a blocked thread to acquire)
unlock one or more queued threads;
要支持上面兩個操做就必須有下面的條件:
狀態位的原子操做
這裏使用一個32位的整數來描述狀態位,前面章節的原子操做的理論知識整好派上用場,在這裏依然使用CAS操做來解決這個問題。事實上這裏還有一個64位版本的同步器
(AbstractQueuedLongSynchronizer),這裏暫且不談。
阻塞和喚醒線程
標準的JAVA API裏面是沒法掛起(阻塞)一個線程,而後在未來某個時刻再喚醒它的。JDK 1.0的API裏面有Thread.suspend和Thread.resume,而且一直延續了下來。可是這
些都是過期的API,並且也是不推薦的作法。
在JDK 5.0之後利用JNI在LockSupport類中實現了此特性。
LockSupport.park()
LockSupport.park(Object)
LockSupport.parkNanos(Object, long)
LockSupport.parkNanos(long)
LockSupport.parkUntil(Object, long)
LockSupport.parkUntil(long)
LockSupport.unpark(Thread)
上面的API中park()是在當前線程中調用,致使線程阻塞,帶參數的Object是掛起的對象,這樣監視的時候就可以知道此線程是由於什麼資源而阻塞的。因爲park()當即返回,因此
一般狀況下須要在循環中去檢測競爭資源來決定是否進行下一次阻塞。park()返回的緣由有三:
其實第三條就決定了須要循環檢測了,相似於一般寫的while(checkCondition()){Thread.sleep(time);}相似的功能。
AQS採用的CHL模型採用下面的算法完成FIFO的入隊列和出隊列過程。
對於入隊列(enqueue):採用CAS操做,每次比較尾結點是否一致,而後插入的到尾結點中。
do {
pred = tail;
}while ( !compareAndSet(pred,tail,node) );
對於出隊列(dequeue):因爲每個節點也緩存了一個狀態,決定是否出隊列,所以當不知足條件時就須要自旋等待,一旦知足條件就將頭結點設置爲下一個節點。
while (pred.status != RELEASED) ;
head = node;
AQS裏面有三個核心字段:
private volatile int state;
private transient volatile Node head;
private transient volatile Node tail;
其中state描述的有多少個線程取得了鎖,對於互斥鎖來講state<=1。head/tail加上CAS操做就構成了一個CHL的FIFO隊列。下面是Node節點的屬性。
volatile int waitStatus; 節點的等待狀態,一個節點可能位於如下幾種狀態:
- CANCELLED = 1: 節點操做由於超時或者對應的線程被interrupt。節點不該該留在此狀態,一旦達到此狀態將從CHL隊列中踢出。
- SIGNAL = -1: 節點的繼任節點是(或者將要成爲)BLOCKED狀態(例如經過LockSupport.park()操做),所以一個節點一旦被釋放(解鎖)或者取消就須要喚醒(LockSupport.unpack())它的繼任節點。
- CONDITION = -2:代表節點對應的線程由於不知足一個條件(Condition)而被阻塞。
- 0: 正常狀態,新生的非CONDITION節點都是此狀態。
- 非負值標識節點不須要被通知(喚醒)。
volatile Node prev;此節點的前一個節點。節點的waitStatus依賴於前一個節點的狀態。
volatile Node next;此節點的後一個節點。後一個節點是否被喚醒(uppark())依賴於當前節點是否被釋放。
volatile Thread thread;節點綁定的線程。
Node nextWaiter;下一個等待條件(Condition)的節點,因爲Condition是獨佔模式,所以這裏有一個簡單的隊列來描述Condition上的線程節點。