1. JUC概況
以下是Java JUC包的主體結構:
- Atomic : AtomicInteger
- Locks : Lock, Condition, ReadWriteLock
- Collections : Queue, ConcurrentMap
- Executer : Future, Callable, Executor
- Tools : CountDownLatch, CyclicBarrier, Semaphore
2. 原子操作
多個線程執行一個操作時,其中任何一個線程要麼完全執行完此操作,要麼沒有執行此操作的任何步驟,那麼這個操作就是原子的。出現原因: synchronized的代價比較高。
以下以AtomicInteger爲例:
- int addAndGet(int delta):以原子方式將給定值與當前值相加。 實際上就是等於線程安全版本的i =i+delta操作。
- boolean compareAndSet(int expect, int update):如果當前值 == 預期值,則以原子方式將該值設置爲給定的更新值。 如果成功就返回true,否則返回false,並且不修改原值。
- int decrementAndGet():以原子方式將當前值減 1。 相當於線程安全版本的–i操作。
- int getAndAdd(int delta):以原子方式將給定值與當前值相加。 相當於線程安全版本的t=i;i+=delta;return t;操作。
- int getAndDecrement():以原子方式將當前值減 1。 相當於線程安全版本的i–操作。
- int getAndIncrement():以原子方式將當前值加 1。 相當於線程安全版本的i++操作。
- int getAndSet(int newValue):以原子方式設置爲給定值,並返回舊值。 相當於線程安全版本的t=i;i=newValue;return t;操作。
- int incrementAndGet():以原子方式將當前值加 1。 相當於線程安全版本的++i操作。
3. 指令重排
你的程序並不能總是保證符合CPU處理的特性。
要程序的最終結果等同於它在嚴格的順序化環境下的結果,那麼指令的執行順序就可能與代碼的順序不一致。
多核CPU,大壓力下,兩個線程交替執行,x,y輸出結果不確定。可能結果:
1
2
3
4
|
x =0, y =1
x =1, y =1
x =1, y =0
x =0, y =0
|
4. Happens-before法則:(Java 內存模型)
如果動作B要看到動作A的執行結果(無論A/B是否在同一個線程裏面執行),那麼A/B就需要滿足happens-before關係。
Happens-before的幾個規則:
- Program order rule:同一個線程中的每個Action都happens-before於出現在其後的任何一個Action。
- Monitor lock rule:對一個監視器的解鎖happens-before於每一個後續對同一個監視器的加鎖。
- Volatile variable rule:對volatile字段的寫入操作happens-before於每一個後續的同一個字段的讀操作。
- Thread start rule:Thread.start()的調用會happens-before於啓動線程裏面的動作。
- Thread termination rule:Thread中的所有動作都happens-before於其他線程檢查到此線程結束或者Thread.join()中返回或者Thread.isAlive()==false。
- Interruption rule:一個線程A調用另一個另一個線程B的interrupt()都happens-before於線程A發現B被A中斷(B拋出異常或者A檢測到B的isInterrupted()或者interrupted())。
- Finalizer rule:一個對象構造函數的結束happens-before與該對象的finalizer的開始
- Transitivity:如果A動作happens-before於B動作,而B動作happens-before與C動作,那麼A動作happens-before於C動作。
因爲CPU是可以不按我們寫代碼的順序執行內存的存取過程的,也就是指令會亂序或並行運行, 只有上面的happens-before所規定的情況下,才保證順序性。
JMM的特性:
多個CPU之間的緩存也不保證實時同步;
JMM不保證創建過程的原子性,讀寫併發時,可能看到不完整的對象。(so D-check)
volatile語義:
volatile實現了類似synchronized的語義,卻又沒有鎖機制。它確保對 volatile字段的更新以可預見的方式告知其他的線程。
- Java 存儲模型不會對volatile指令的操作進行重排序:這個保證對volatile變量的操作時按照指令的出現順序執行的。
- volatile變量不會被緩存在寄存器中(只有擁有線程可見),每次總是從主存中讀取volatile變量的結果。
ps:volatile並不能保證線程安全的,也就是說volatile字段的操作不是原子性的,volatile變量只能保證可見性。
5. CAS操作
Compare and Swap
CAS有3個操作數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改爲B,否則什麼都不做。
實現簡單的非阻塞算法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
privatevolatileintvalue;// 藉助volatile原語,保證線程間的數據是可見的
publicfinalintget() {
returnvalue;
}
publicfinalintincrementAndGet() {
for(;;) {
intcurrent = get();
intnext = current +1;
if(compareAndSet(current, next))
returnnext;
}//Spin自旋等待直到返爲止置
}
|
整個J.U.C都是建立在CAS之上的,對於synchronized阻塞算法,J.U.C在性能上有了很大的提升。會出現所謂的“ABA”問題
6. Lock 鎖
Synchronized屬於獨佔鎖,高併發時性能不高,JDK5以後開始用JNI實現更高效的鎖操作。
Lock—->
ReentrantLock—->
ReentrantReadWriteLock.ReadLock / ReentrantReadWriteLock.writeLock
ReadWriteLock—-> ReentrantReadWriteLock
LockSupport
Condition
方法名稱 | 作用 |
void lock() | 獲取鎖。如果鎖不可用,出於線程調度目的,將禁用當前線程,並且在獲得鎖之前,該線程將一直處於休眠狀態。 |
void lockInterruptibly() throws InterruptedException; | 如果當前線程未被中斷,則獲取鎖。如果鎖可用,則獲取鎖,並立即返回。 |
Condition newCondition(); | 返回綁定到此 Lock 實例的新 Condition 實例 |
boolean tryLock(); | 僅在調用時鎖爲空閒狀態才獲取該鎖 |
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; | 如果鎖在給定的等待時間內空閒,並且當前線程未被中斷,則獲取鎖 |
void unlock(); | 釋放鎖 |
PS : 一般來說,獲取鎖和釋放鎖是成對兒的操作,這樣可以避免死鎖和資源的浪費。
注:在 finally 裏面做釋放鎖的操作
7. AQS
鎖機制實現的核心所在。AbstractQueuedSynchronizer是Lock/Executor實現的前提。
AQS實現:
基本的思想是表現爲一個同步器,AQS支持下面兩個操作:
acquire:
1
2
3
4
5
|
while(synchronization state does not allow acquire){
enqueue current threadifnot already queued;
possibly block current thread;
}
dequeue current threadifit was queued;
|
release:
1
2
3
|
update synchronization state;
if(state may permit a blocked thread to acquire)
unlock one or more queued threads;
|
要支持這兩個操作,需要實現的三個條件:
- Atomically managing synchronization state(原子性操作同步器的狀態位)
- Blocking and unblocking threads(阻塞和喚醒線程)
- Maintaining queues(維護一個有序的隊列)
Atomically managing synchronization state
使用一個32位整數來描述狀態位:private volatile int state; 對其進行CAS操作,確保值的正確性。
Blocking and unblocking threads
JDK 5.0以後利用JNI在LockSupport類中實現了線程的阻塞和喚醒。
LockSupport.park() //在當前線程中調用,導致線程阻塞
LockSupport.park(Object)
LockSupport.unpark(Thread)
Maintaining queues
在AQS中採用CHL列表來解決有序的隊列的問題。(CHL= Craig, Landin, and Hagersten)
Node裏面是什麼結構?
WaitStatus –>節點的等待狀態,一個節點可能位於以下幾種狀態:
- CANCELLED = 1: 節點操作因爲超時或者對應的線程被interrupt。節點不應該不留在此狀態,一旦達到此狀態將從CHL隊列中踢出。
- SIGNAL = -1: 節點的繼任節點是(或者將要成爲)BLOCKED狀態(例如通過LockSupport.park()操作),因此一個節點一旦被釋放(解鎖)或者取消就需要喚醒(LockSupport.unpack())它的繼任節點。
- CONDITION = -2:表明節點對應的線程因爲不滿足一個條件(Condition)而被阻塞。
- 0: 正常狀態,新生的非CONDITION節點都是此狀態。
非負值標識節點不需要被通知(喚醒)。
隊列管理操作:
入隊enqueue:
採用CAS操作,每次比較尾結點是否一致,然後插入的到尾結點中。
1
2
3
|
do{
pred = tail;
}while( !compareAndSet(pred,tail,node) );
|
出隊dequeue:
1
2
|
while(pred.status != RELEASED) ;
head = node;
|
加鎖操作:
1
2
3
4
5
|
public final void acquire(intarg) {
if(!tryAcquire(arg))
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
|
釋放操作:
1
2
3
4
5
6
7
8
9
|
public final boolean release(intarg) {
if(tryRelease(arg)) {
Node h = head;
if(h !=null && h.waitStatus !=0)
unparkSuccessor(h);
return true;
}
return false;
}
|
The synchronizer framework provides a ConditionObject class for use by synchronizers that maintain exclusivesynchronization and conform to the Lock interface. —— Doug Lea《 The java.util.concurrent Synchronizer Framework 》
以下是AQS隊列和Condition隊列的出入結點的示意圖,可以通過這幾張圖看出線程結點在兩個隊列中的出入關係和條件。