情人節快樂!
[JDK] 多線程高併發探祕之「鎖」
鎖做爲併發共享數據,保證一致性的工具,在JAVA平臺有多種實現(如 synchronized 和 ReentrantLock等等 ) 。這些已經寫好提供的鎖爲咱們開發提供了便利,可是鎖的具體性質以及類型卻不多被說起。html
1. 自旋鎖
自旋鎖是採用讓當前線程不停地的在循環體內執行實現的,當循環的條件被其餘線程改變時 才能進入臨界區。java
Ex1
node
@RequestMapping(value = "lock/{index}", method = RequestMethod.GET)public void lock(@PathVariable Integer index) { switch (index) { case 0:
concurrenceLock.doTest(); break; case 1:
concurrenceLock.doTest1(); break; case 2:
concurrenceLock.doTest2(); break; default: break;
}
}
package com.example.concurrence.lock;import lombok.extern.slf4j.Slf4j;import java.util.Objects;import java.util.concurrent.atomic.AtomicReference;/** * <p> * 自旋鎖 * 自旋鎖是採用讓當前線程不停地的在循環體內執行實現的,當循環的條件被其餘線程改變時 才能進入臨界區。 * 自旋鎖是指嘗試獲取鎖的線程不會當即阻塞,而是採用循環的方式去嘗試獲取鎖,這樣的好處是減小線程上下文切換的消耗,缺點是循環會消耗CPU。 * </p> * * @author xiachaoyang * @version V1.0 * @date 2019年01月23日 15:33 * @modificationHistory=========================邏輯或功能性重大變動記錄 * @modify By: {修改人} 2019年01月23日 * @modify reason: {方法名}:{緣由} * ... */@Slf4jpublic class SpinLock { private AtomicReference<Thread> sign = new AtomicReference<>(); public void lock(){
Thread current = Thread.currentThread(); //若是當前值{@code ==}爲指望值,原子地將值設置爲給定的更新值
log.debug("{} spinLock start lock..........", current.getName()); while (!sign.compareAndSet(null, current)) {
log.debug("{} spinLock locking........", current.getName());
}
log.debug("{} spinLock quit lock..........", current.getName());
} public void unlock() {
Thread current = Thread.currentThread(); boolean flag = sign.compareAndSet(current, null);
log.debug("{} spinLock unlock() >>>> {}",current.getName(),flag);
}
}//ConcurrenceLockServiceImplpackage com.example.service.concurrence.impl;import com.example.concurrence.lock.SpinLock;import com.example.service.concurrence.ConcurrenceService;import lombok.extern.slf4j.Slf4j;import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;import org.springframework.stereotype.Service;import javax.annotation.Resource;/** * <p> * * </p> * * @author xiachaoyang * @version V1.0 * @date 2019年01月23日 15:42 * @modificationHistory=========================邏輯或功能性重大變動記錄 * @modify By: {修改人} 2019年01月23日 * @modify reason: {方法名}:{緣由} * ... */@Slf4j@Servicepublic class ConcurrenceLockServiceImpl implements ConcurrenceService.LockPart { private static SpinLock spinLock = new SpinLock(); private Thread last; @Resource
private ThreadPoolTaskExecutor threadPoolTaskExecutor; @Override
public void doTest() throws InterruptedException {
spinLock.lock();
Thread.sleep(10000);
spinLock.unlock();
}
}
每次執行完自旋的判斷後,sign的引用會被指向當前線程,下次進入判斷後,預測值和當前線程不一致,則會返回false,即進入自旋鎖循環體內sign.compareAndSet(null, current)
該方法,前者爲預測原來AtomicReference的引用值,後者爲更新值,預測正確的話,則更新爲更新值,返回true;預測錯誤則返回false。spring
使用了CAS原子操做,lock函數將owner設置爲當前線程,而且預測原來的值爲空。unlock函數將owner設置爲null,而且預測值爲當前線程。apache
當有第二個線程調用lock操做時因爲owner值不爲空,致使循環一直被執行,直至第一個線程調用unlock函數將owner設置爲null,第二個線程才能進入臨界區。編程
因爲自旋鎖只是將當前線程不停地執行循環體,不進行線程狀態的改變,因此響應速度更快。但當線程數不停增長時,性能降低明顯,由於每一個線程都須要執行,佔用CPU時間。若是線程競爭不激烈,而且保持鎖的時間段。適合使用自旋鎖。緩存
注:該例子爲非公平鎖,得到鎖的前後順序,不會按照進入lock的前後順序進行。微信
日誌
多線程
//單次調用 2019-01-28 17:40:38.553 DEBUG 8496 --- [nio-8080-exec-2] com.example.concurrence.lock.SpinLock : http-nio-8080-exec-2 spinLock start lock.......... 2019-01-28 17:40:38.553 DEBUG 8496 --- [nio-8080-exec-2] com.example.concurrence.lock.SpinLock : http-nio-8080-exec-2 spinLock quit lock.......... 2019-01-28 17:40:48.553 DEBUG 8496 --- [nio-8080-exec-2] com.example.concurrence.lock.SpinLock : http-nio-8080-exec-2 spinLock unlock() >>>> true //併發調用 2019-01-28 17:44:07.431 DEBUG 8496 --- [nio-8080-exec-8] com.example.concurrence.lock.SpinLock : http-nio-8080-exec-8 spinLock locking........ 2019-01-28 17:44:07.432 DEBUG 8496 --- [nio-8080-exec-8] com.example.concurrence.lock.SpinLock : http-nio-8080-exec-8 spinLock quit lock.......... 2019-01-28 17:44:07.432 DEBUG 8496 --- [nio-8080-exec-6] com.example.concurrence.lock.SpinLock : http-nio-8080-exec-6 spinLock unlock() >>>> true 2019-01-28 17:44:17.432 DEBUG 8496 --- [nio-8080-exec-8] com.example.concurrence.lock.SpinLock : http-nio-8080-exec-8 spinLock unlock() >>>> true
2. 自旋鎖的其餘種類
除了前文提到的自旋鎖,在自旋鎖中另有三種常見的鎖形式:TicketLock
,CLHlock
和MCSlock
架構
2.1 TicketSpinLock
package com.example.concurrence.lock;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.atomic.AtomicInteger;/** * <p> * Ticket鎖主要解決的是訪問順序的問題,主要的問題是在多核cpu上 * 可是每次都要查詢一個serviceNum 服務號,影響性能(必需要到主內存讀取,並阻止其餘cpu修改) * </p> * * @author xiachaoyang * @version V1.0 * @date 2019年01月24日 10:58 * @modificationHistory=========================邏輯或功能性重大變動記錄 * @modify By: {修改人} 2019年01月24日 * @modify reason: {方法名}:{緣由} * ... */@Slf4jpublic class TicketSpinLock { private AtomicInteger serviceNum = new AtomicInteger(); private AtomicInteger ticketNum = new AtomicInteger(); private static final ThreadLocal<Integer> LOCAL = new ThreadLocal<Integer>(); public void lock() {
log.debug("{} try lock..................",Thread.currentThread().getName()); int myticket = ticketNum.getAndIncrement();
LOCAL.set(myticket);
log.debug("{} set in LOCAL..................",myticket); while (myticket != serviceNum.get()) {
log.debug("{} locking..................",Thread.currentThread().getName());
}
} public void unlock() { int myticket = LOCAL.get(); boolean flag = serviceNum.compareAndSet(myticket, myticket + 1);
log.debug("{} unlocked..................>>myticket is {}, and flag is {}",Thread.currentThread().getName(),myticket,flag);
} //淺析TicketLock(https://blog.csdn.net/yxc5463/article/details/78193991)
//Java多線程編程排隊鎖(Ticket Lock詳解)(http://www.leftso.com/blog/466.html)}
測試代碼
@Overridepublic void doTest3() throws InterruptedException {
ticketSpinLock.lock();
Thread.sleep(10000);
ticketSpinLock.unlock();
}//模擬多線程,利用PostMan觸發請求,調用接口;中間停頓10s是爲了模擬線程阻塞
日誌
2019-01-28 16:43:30.339 DEBUG 13280 --- [nio-8080-exec-9] c.e.concurrence.lock.TicketSpinLock : http-nio-8080-exec-9 locking.................. 2019-01-28 16:43:30.339 DEBUG 13280 --- [nio-8080-exec-9] c.e.concurrence.lock.TicketSpinLock : http-nio-8080-exec-9 locking.................. 2019-01-28 16:43:30.339 DEBUG 13280 --- [nio-8080-exec-9] c.e.concurrence.lock.TicketSpinLock : http-nio-8080-exec-9 locking.................. 2019-01-28 16:43:30.561 DEBUG 13280 --- [nio-8080-exec-7] c.e.concurrence.lock.TicketSpinLock : http-nio-8080-exec-7 unlocked..................>>myticket is 3, and flag is true 2019-01-28 16:43:31.108 DEBUG 13280 --- [nio-8080-exec-7] o.s.b.w.s.f.OrderedRequestContextFilter : Cleared thread-bound request context: org.apache.catalina.connector.RequestFacade@b5f05b5 2019-01-28 16:43:41.108 DEBUG 13280 --- [nio-8080-exec-9] c.e.concurrence.lock.TicketSpinLock : http-nio-8080-exec-9 unlocked..................>>myticket is 4, and flag is true
缺點
Ticket Lock
雖然解決了公平性的問題,可是多處理器系統上,每一個進程/線程佔用的處理器都在讀寫同一個變量serviceNum
,每次讀寫操做都必須在多個處理器緩存之間進行緩存同步,這會致使繁重的系統總線和內存的流量,大大下降系統總體的性能。
2.2 CLHLock & MCSLock
CLHLock 和MCSLock 則是兩種類型類似的公平鎖,採用鏈表的形式進行排序
CLHLock
package com.example.concurrence.lock;import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;/** * <p> * CLHlock是不停的查詢前驅變量, 致使不適合在NUMA 架構下使用(在這種結構下,每一個線程分佈在不一樣的物理內存區域) * </p> * * @author xiachaoyang * @version V1.2.0 * @date 2019年01月28日 17:04 * @modificationHistory=========================邏輯或功能性重大變動記錄 * @modify By: {修改人} 2019年01月28日 * @modify reason: {方法名}:{緣由} * ... */public class CLHLock { public static class CLHNode { private volatile boolean isLocked = true;
} @SuppressWarnings("unused") private volatile CLHNode tail; private static final ThreadLocal<CLHNode> LOCAL = new ThreadLocal<CLHNode>(); private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLock.class,
CLHNode.class, "tail"); public void lock() {
CLHNode node = new CLHNode();
LOCAL.set(node);
CLHNode preNode = UPDATER.getAndSet(this, node); if (preNode != null) { while (preNode.isLocked) {
}
preNode = null;
LOCAL.set(node);
}
} public void unlock() {
CLHNode node = LOCAL.get(); if (!UPDATER.compareAndSet(this, node, null)) {
node.isLocked = false;
}
node = null;
}
}
MCSLock
package com.example.concurrence.lock;import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;/** * <p> * MCSLock則是對本地變量的節點進行循環,不存在CLHlock 的問題。 * </p> * * @author xiachaoyang * @version V1.2.0 * @date 2019年01月28日 17:11 * @modificationHistory=========================邏輯或功能性重大變動記錄 * @modify By: {修改人} 2019年01月28日 * @modify reason: {方法名}:{緣由} * ... */public class MCSLock { public static class MCSNode { volatile MCSNode next; volatile boolean isLocked = true;
} private static final ThreadLocal<MCSNode> NODE = new ThreadLocal<MCSNode>(); @SuppressWarnings("unused") private volatile MCSNode queue; private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(MCSLock.class,
MCSNode.class, "queue"); public void lock() {
MCSNode currentNode = new MCSNode();
NODE.set(currentNode);
MCSNode preNode = UPDATER.getAndSet(this, currentNode); if (preNode != null) {
preNode.next = currentNode; while (currentNode.isLocked) {
}
}
} public void unlock() {
MCSNode currentNode = NODE.get(); if (currentNode.next == null) { if (UPDATER.compareAndSet(this, currentNode, null)) {
} else { while (currentNode.next == null) {
} // 釋放鎖
currentNode.next.isLocked = false;
currentNode.next = null;
}
} else {
currentNode.next.isLocked = false;
currentNode.next = null;
}
}
}
CAS
/**AtomicReferenceFieldUpdater*/public final boolean compareAndSet(T obj, V expect, V update) {
accessCheck(obj);
valueCheck(update); /**this.offset = U.objectFieldOffset(field);初始化構造時寫入 public static <U,W> AtomicReferenceFieldUpdater<U,W> newUpdater(Class<U> tclass,Class<W> vclass,String fieldName) { return new AtomicReferenceFieldUpdaterImpl<U,W> (tclass, vclass, fieldName, Reflection.getCallerClass()); } AtomicReferenceFieldUpdaterImpl(final Class<T> tclass, final Class<V> vclass, final String fieldName, final Class<?> caller) */
return U.compareAndSwapObject(obj, offset, expect, update);
}/** * 比較obj的offset處內存位置中的值和指望的值,若是相同則更新。此更新是不可中斷的。 * * @param obj 須要更新的對象 * @param offset obj中整型field的偏移量 * @param expect 但願field中存在的值 * @param update 若是指望值expect與field的當前值相同,設置filed的值爲這個新值 * @return 若是field的值被更改返回true */public native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);
CAS操做有3個操做數,內存值M,預期值E,新值U,若是M==E,則將內存值修改成B,不然啥都不作。
Java中具體的CAS操做類sun.misc.Unsafe。Unsafe類提供了硬件級別的原子操做,Java沒法直接訪問到操做系統底層(如系統硬件等),爲此Java使用native方法來擴展Java程序的功能。
日誌
2019-02-01 11:47:22.539 DEBUG 8948 --- [nio-8080-exec-1] com.example.concurrence.lock.MCSLock : http-nio-8080-exec-1 MCSLock unlock() 2019-02-01 11:47:22.539 DEBUG 8948 --- [nio-8080-exec-5] com.example.concurrence.lock.MCSLock : http-nio-8080-exec-5 MCSLock quit lock.......... 2019-02-01 11:47:22.564 DEBUG 8948 --- [nio-8080-exec-1] o.s.b.w.s.f.OrderedRequestContextFilter : Cleared thread-bound request context: org.apache.catalina.connector.RequestFacade@27711663 2019-02-01 11:47:32.540 DEBUG 8948 --- [nio-8080-exec-5] com.example.concurrence.lock.MCSLock : http-nio-8080-exec-5 MCSLock unlock()
分析
從代碼上 看,
CLH
要比MCS
更簡單;CLH
的隊列是隱式的隊列,沒有真實的後繼結點屬性。MCS
的隊列是顯式的隊列,有真實的後繼結點屬性。
JUC ReentrantLock
默認內部使用的鎖 便是 CLH
鎖(有不少改進的地方,將自旋鎖換成了阻塞鎖等等)。
3.阻塞鎖
阻塞鎖,與自旋鎖不一樣,改變了線程的運行狀態。在JAVA環境中,線程Thread有以下幾個狀態:
新建狀態
就緒狀態
運行狀態
阻塞狀態
死亡狀態
阻塞鎖,能夠說是讓線程進入阻塞狀態進行等待,當得到相應的信號(喚醒,時間) 時,才能夠進入線程的準備就緒狀態,準備就緒狀態的全部線程,經過競爭,進入運行狀態。
JAVA中,可以進入\退出、阻塞狀態或包含阻塞鎖的方法有 ,synchronized
關鍵字(其中的重量鎖),ReentrantLock
,Object.wait()\notify()
,LockSupport.park()/unpart()
(j.u.c常用)
package com.example.concurrence.lock;import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;import java.util.concurrent.locks.LockSupport;/** * <p> * 阻塞鎖,能夠說是讓線程進入阻塞狀態進行等待,當得到相應的信號(喚醒,時間) 時,才能夠進入線程的準備就緒狀態,準備就緒狀態的全部線程,經過競爭,進入運行狀態。 * </p> * * @author xiachaoyang * @version V1.2.0 * @date 2019年02月13日 10:00 * @modificationHistory=========================邏輯或功能性重大變動記錄 * @modify By: {修改人} 2019年02月13日 * @modify reason: {方法名}:{緣由} * ... */public class CLHLockWithBlock { public static class CLHNode { private volatile Thread isLocked;
} @SuppressWarnings("unused") private volatile CLHNode tail; private static final ThreadLocal<CLHNode> LOCAL = new ThreadLocal<CLHNode>(); private static final AtomicReferenceFieldUpdater<CLHLockWithBlock, CLHNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLockWithBlock.class,
CLHNode.class, "tail"); public void lock() {
CLHNode node = new CLHNode();
LOCAL.set(node);
CLHNode preNode = UPDATER.getAndSet(this, node); if (preNode != null) {
preNode.isLocked = Thread.currentThread();
LockSupport.park(this);
preNode = null;
LOCAL.set(node);
}
} public void unlock() {
CLHNode node = LOCAL.get(); if (!UPDATER.compareAndSet(this, node, null)) {
System.out.println("unlock\t" + node.isLocked.getName());
LockSupport.unpark(node.isLocked);
}
node = null;
}
}
在這裏咱們使用了LockSupport.unpark()的阻塞鎖。 該例子是將CLH鎖修改而成。
阻塞鎖的優點在於,阻塞的線程不會佔用CPU時間, 不會致使 CPU佔用率太高,但進入時間以及恢復時間都要比自旋鎖略慢。
在競爭激烈的狀況下 阻塞鎖的性能要明顯高於自旋鎖。
理想的狀況則是; 在線程競爭不激烈的狀況下,使用自旋鎖
,競爭激烈的狀況下使用阻塞鎖
。
LockSupport
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
將一個線程進行掛起是經過park方法實現的,調用 park後,線程將一直阻塞直到超時或者中斷等條件出現。unpark能夠終止一個掛起的線程,使其恢復正常。整個併發框架中對線程的掛起操做被封裝在 LockSupport類中,LockSupport類中有各類版本pack方法,但最終都調用了Unsafe.park()方法。
4.可重入鎖
本文裏面講的是廣義上的可重入鎖,而不是單指JAVA下的ReentrantLock
。
可重入鎖,也叫作遞歸鎖,指的是同一線程外層函數得到鎖以後 ,內層遞歸函數仍然有獲取該鎖的代碼,但不受影響。在JAVA環境下 ReentrantLock 和synchronized 都是可重入鎖。
synchronized
package com.example.concurrence.lock;import lombok.extern.slf4j.Slf4j;/** * <p> * * </p> * * @author xiachaoyang * @version V1.2.0 * @date 2019年02月13日 10:14 * @modificationHistory=========================邏輯或功能性重大變動記錄 * @modify By: {修改人} 2019年02月13日 * @modify reason: {方法名}:{緣由} * ... */@Slf4jpublic class SynchronizedLock implements Runnable{ /** * When an object implementing interface <code>Runnable</code> is used * to create a thread, starting the thread causes the object's * <code>run</code> method to be called in that separately executing * thread. * <p> * The general contract of the method <code>run</code> is that it may * take any action whatsoever. * * @see Thread#run() */
@Override
public void run() {
get();
} private synchronized void get() {
log.debug("synchronized current thread's id is {}",Thread.currentThread().getId());
set();
} private synchronized void set() {
log.debug("synchronized current thread's id is {}",Thread.currentThread().getId());
} public static void main(String[] args) {
SynchronizedLock lock = new SynchronizedLock(); new Thread(lock).start(); new Thread(lock).start(); new Thread(lock).start();
}
}
日誌
10:22:31.186 [Thread-0] DEBUG com.example.concurrence.lock.SynchronizedLock - synchronized current thread's id is 13 10:22:31.191 [Thread-0] DEBUG com.example.concurrence.lock.SynchronizedLock - synchronized current thread's id is 13 10:22:31.191 [Thread-2] DEBUG com.example.concurrence.lock.SynchronizedLock - synchronized current thread's id is 15 10:22:31.191 [Thread-2] DEBUG com.example.concurrence.lock.SynchronizedLock - synchronized current thread's id is 15 10:22:31.191 [Thread-1] DEBUG com.example.concurrence.lock.SynchronizedLock - synchronized current thread's id is 14 10:22:31.191 [Thread-1] DEBUG com.example.concurrence.lock.SynchronizedLock - synchronized current thread's id is 14
ReentrantLock
package com.example.concurrence.lock;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.locks.ReentrantLock;/** * <p> * * </p> * * @author xiachaoyang * @version V1.2.0 * @date 2019年02月13日 10:19 * @modificationHistory=========================邏輯或功能性重大變動記錄 * @modify By: {修改人} 2019年02月13日 * @modify reason: {方法名}:{緣由} * ... */@Slf4jpublic class ReentrantLockThread implements Runnable{
ReentrantLock lock = new ReentrantLock(); /** * When an object implementing interface <code>Runnable</code> is used * to create a thread, starting the thread causes the object's * <code>run</code> method to be called in that separately executing * thread. * <p> * The general contract of the method <code>run</code> is that it may * take any action whatsoever. * * @see Thread#run() */
@Override
public void run() {
get();
} private void get() {
lock.lock();
log.debug("ReentrantLockThread current thread's id is {}",Thread.currentThread().getId());
set();
lock.unlock();
} private void set() {
lock.lock();
log.debug("ReentrantLockThread current thread's id is {}",Thread.currentThread().getId());
lock.unlock();
} public static void main(String[] args) {
ReentrantLockThread lock = new ReentrantLockThread(); new Thread(lock).start(); new Thread(lock).start(); new Thread(lock).start();
}
}
日誌
10:24:22.765 [Thread-0] DEBUG com.example.concurrence.lock.ReentrantLockThread - ReentrantLockThread current thread's id is 13 10:24:22.770 [Thread-0] DEBUG com.example.concurrence.lock.ReentrantLockThread - ReentrantLockThread current thread's id is 13 10:24:22.770 [Thread-1] DEBUG com.example.concurrence.lock.ReentrantLockThread - ReentrantLockThread current thread's id is 14 10:24:22.770 [Thread-1] DEBUG com.example.concurrence.lock.ReentrantLockThread - ReentrantLockThread current thread's id is 14 10:24:22.770 [Thread-2] DEBUG com.example.concurrence.lock.ReentrantLockThread - ReentrantLockThread current thread's id is 15 10:24:22.770 [Thread-2] DEBUG com.example.concurrence.lock.ReentrantLockThread - ReentrantLockThread current thread's id is 15
兩個例子最後的結果都是正確的,即 同一個線程id被連續輸出兩次。
可重入鎖最大的做用是避免死鎖,咱們以自旋鎖做爲例子:
public class SpinLock { private AtomicReference<Thread> owner =new AtomicReference<>(); public void lock(){
Thread current = Thread.currentThread(); while(!owner.compareAndSet(null, current)){
}
} public void unlock (){
Thread current = Thread.currentThread();
owner.compareAndSet(current, null);
}
}
對於自旋鎖來講,
一、如有同一線程兩調用lock() ,會致使第二次調用lock位置進行自旋,產生了死鎖
說明這個鎖並非可重入的。(在lock函數內,應驗證線程是否爲已經得到鎖的線程)
二、若1問題已經解決,當unlock()第一次調用時,就已經將鎖釋放了。實際上不該釋放鎖。
(採用計數次進行統計)
修改以後
public class SpinLock1 { private AtomicReference<Thread> owner =new AtomicReference<>(); private int count =0; public void lock(){
Thread current = Thread.currentThread(); if(current==owner.get()) {
count++; return ;
} while(!owner.compareAndSet(null, current)){
}
} public void unlock (){
Thread current = Thread.currentThread(); if(current==owner.get()){ if(count!=0){
count--;
}else{
owner.compareAndSet(current, null);
}
}
}
}
該自旋鎖即爲可重入鎖。
5.ReentrantLock(重入鎖)以及公平性
ReentrantLock的實現不只能夠替代隱式的synchronized關鍵字,並且可以提供超過關鍵字自己的多種功能。
這裏提到一個鎖獲取的公平性問題,若是在絕對時間上,先對鎖進行獲取的請求必定被先知足,那麼這個鎖是公平的,反之,是不公平的,也就是說等待時間最長的線程最有機會獲取鎖,也能夠說鎖的獲取是有序的。ReentrantLock這個鎖提供了一個構造函數,可以控制這個鎖是不是公平的。
而鎖的名字也是說明了這個鎖具有了重複進入的可能,也就是說可以讓當前線程屢次的進行對鎖的獲取操做,這樣的最大次數限制是Integer.MAX_VALUE,約21億次左右。
事實上公平的鎖機制每每沒有非公平的效率高,由於公平的獲取鎖沒有考慮到操做系統對線程的調度因素,這樣形成JVM對於等待中的線程調度次序和操做系統對線程的調度之間的不匹配。對於鎖的快速且重複的獲取過程當中,連續獲取的機率是很是高的,而公平鎖會壓制這種狀況,雖然公平性得以保障,可是響應比卻降低了,可是並非任何場景都是以TPS做爲惟一指標的,由於公平鎖可以減小「飢餓」發生的機率,等待越久的請求越是可以獲得優先知足。
在ReentrantLock中,對於公平和非公平的定義是經過對同步器AbstractQueuedSynchronizer的擴展加以實現的,也就是在tryAcquire的實現上作了語義的控制。
非公平的獲取語義
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current); return true;
}
} else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc); return true;
} return false;
}
上述邏輯主要包括:
若是當前狀態爲初始狀態,那麼嘗試設置狀態;
若是狀態設置成功後就返回;
若是狀態被設置,且獲取鎖的線程又是當前線程的時候,進行狀態的自增;
若是未設置成功狀態且當前線程不是獲取鎖的線程,那麼返回失敗。
公平的獲取語義
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current); return true;
}
} else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded");
setState(nextc); return true;
} return false;
}
上述邏輯相比較非公平的獲取,僅加入了當前線程(Node)以前是否有前置節點在等待的判斷。hasQueuedPredecessors()方法命名有些歧義,其實應該是currentThreadHasQueuedPredecessors()更爲妥帖一些,也就是說當前面沒有人排在該節點(Node)前面時候隊且可以設置成功狀態,纔可以獲取鎖。
釋放語義
protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c); return free;
}
上述邏輯主要主要計算了釋放狀態後的值,若是爲0則徹底釋放,返回true,反之僅是設置狀態,返回false。
下面將主要的筆墨放在公平性和非公平性上,首先看一下兩者測試的對比:
package com.example.concurrence.lock;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;/** * <p> * * </p> * * @author xiachaoyang * @version V1.2.0 * @date 2019年02月13日 10:39 * @modificationHistory=========================邏輯或功能性重大變動記錄 * @modify By: {修改人} 2019年02月13日 * @modify reason: {方法名}:{緣由} * ... */@Slf4jpublic class ReentrantLockWithFailCondition { private static Lock fairLock = new ReentrantLock(true); private static Lock unfairLock = new ReentrantLock(); public void fair() {
log.debug("fair version"); for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Job(fairLock));
thread.setName("" + i);
thread.start();
} try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} public void unfair() {
log.debug("unfair version"); for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Job(unfairLock));
thread.setName("" + i);
thread.start();
} try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} private static class Job implements Runnable { private Lock lock; public Job(Lock lock) { this.lock = lock;
} @Override
public void run() { for (int i = 0; i < 5; i++) {
lock.lock(); try {
System.out.println("Lock by:"
+ Thread.currentThread().getName());
} finally {
lock.unlock();
}
}
}
} public static void main(String[] args) {
ReentrantLockWithFailCondition test = new ReentrantLockWithFailCondition();
test.fair();
log.debug("unfair --------------------------------------------------->>>");
test.unfair();
}
}
仔細觀察返回的結果(其中每一個數字表明一個線程),非公平的結果一個線程連續獲取鎖的狀況很是多,而公平的結果連續獲取的狀況基本沒有。那麼在一個線程獲取了鎖的那一刻,究竟鎖的公平性會致使鎖有什麼樣的處理邏輯呢?
經過以前的同步器(AbstractQueuedSynchronizer)的介紹,在鎖上是存在一個等待隊列,sync隊列,咱們經過複寫ReentrantLock的獲取當前鎖的sync隊列,輸出在ReentrantLock被獲取時刻,當前的sync隊列的狀態。
修改測試
package com.example.concurrence.lock;import lombok.extern.slf4j.Slf4j;import java.util.Collection;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;/** * <p> * * </p> * * @author xiachaoyang * @version V1.2.0 * @date 2019年02月13日 10:39 * @modificationHistory=========================邏輯或功能性重大變動記錄 * @modify By: {修改人} 2019年02月13日 * @modify reason: {方法名}:{緣由} * ... */@Slf4jpublic class ReentrantLockWithFailCondition{ private static Lock fairLock = new ReentrantLock(true); private static Lock unfairLock = new ReentrantLock(); public void fair() {
log.debug("fair version"); for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Job(fairLock));
thread.setName("" + i);
thread.start();
} try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} public void unfair() {
log.debug("unfair version"); for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Job(unfairLock));
thread.setName("" + i);
thread.start();
} try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} private static class Job implements Runnable { private Lock lock; public Job(Lock lock) { this.lock = lock;
} @Override
public void run() { for (int i = 0; i < 5; i++) {
lock.lock(); try { //log.debug("Lock by:{}",Thread.currentThread().getName());
log.debug("Lock by:{} and {} waits.",Thread.currentThread().getName(),((ReentrantLock2) lock).getQueuedThreads());
} finally {
lock.unlock();
}
}
}
} private static Lock fairLock2 = new ReentrantLock2(true); private static Lock unfairLock2 = new ReentrantLock2(); private static class ReentrantLock2 extends ReentrantLock { // Constructor Override
private static final long serialVersionUID = 1773716895097002072L; /** * Creates an instance of {@code ReentrantLock} with the * given fairness policy. * * @param fair {@code true} if this lock should use a fair ordering policy */
public ReentrantLock2(boolean fair) { super(fair);
} /** * Creates an instance of {@code ReentrantLock}. * This is equivalent to using {@code ReentrantLock(false)}. */
public ReentrantLock2() {
} @Override
public Collection<Thread> getQueuedThreads() { return super.getQueuedThreads();
}
} public void fair2() {
log.debug("fair version"); for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Job(fairLock2));
thread.setName("" + i);
thread.start();
} try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} public void unfair2() {
log.debug("unfair version"); for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Job(unfairLock2));
thread.setName("" + i);
thread.start();
} try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} public static void main(String[] args) {
ReentrantLockWithFailCondition test = new ReentrantLockWithFailCondition(); // test.fair();
//log.debug("unfair --------------------------------------------------->>>");
//test.unfair();
test.fair2();
log.debug("unfair2 --------------------------------------------------->>>");
test.unfair2();
}
}
能夠明顯看出,在非公平獲取的過程當中,「插隊」現象很是嚴重,後續獲取鎖的線程根本不顧及sync隊列中等待的線程,而是能獲取就獲取。反觀公平獲取的過程,鎖的獲取就相似線性化的,每次都由sync隊列中等待最長的線程(鏈表的第一個,sync隊列是由尾部結點添加,當前輸出的sync隊列是逆序輸出)獲取鎖。一個 hasQueuedPredecessors方法可以得到公平性的特性,
這點其實是由AbstractQueuedSynchronizer來完成的,看一下acquire方法:
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
能夠看到,若是獲取狀態和在sync隊列中排隊是短路的判斷,也就是說若是tryAcquire成功,那麼是不會進入sync隊列的,能夠經過下圖來深入的認識公平性和AbstractQueuedSynchronizer的獲取過程。
非公平的,或者說默認的獲取方式以下圖所示:
對於狀態的獲取,能夠快速的經過tryAcquire的成功,也就是黃色的Fast路線,也能夠因爲tryAcquire的失敗,構造節點,進入sync隊列中排序後再次獲取。所以能夠理解爲Fast就是一個快速通道,當例子中的線程釋放鎖以後,快速的經過Fast通道再次獲取鎖,就算當前sync隊列中有排隊等待的線程也會被忽略。這種模式,能夠保證進入和退出鎖的吞吐量,可是sync隊列中過早排隊的線程會一直處於阻塞狀態,形成「飢餓」場景。
而公平性鎖,就是在tryAcquire的調用中顧及當前sync隊列中的等待節點(廢棄了Fast通道),也就是任意請求都須要按照sync隊列中既有的順序進行,先到先得。這樣很好的確保了公平性,可是能夠從結果中看到,吞吐量就沒有非公平的鎖高了。
REFERENCES
[java鎖的種類以及辨析(一):自旋鎖] (http://ifeve.com/java_lock_see1/)
[Java鎖的種類以及辨析(二):自旋鎖的其餘種類] (http://ifeve.com/java_lock_see2/)
[Java鎖的種類以及辨析(三):阻塞鎖] (http://ifeve.com/java_lock_see3/)
[Java鎖的種類以及辨析(四):可重入鎖] (http://ifeve.com/java_lock_see4/)
[ReentrantLock(重入鎖)以及公平性] (http://ifeve.com/reentrantlock-and-fairness/)
[Java多線程系列—「JUC原子類」04之 AtomicReference原子類] (http://www.cnblogs.com/skywang12345/p/3514623.html)
[淺析TicketLock] (https://blog.csdn.net/yxc5463/article/details/78193991)
[Java多線程編程排隊鎖(Ticket Lock詳解)] (http://www.leftso.com/blog/466.html)
[Java中Unsafe類詳解] (https://www.cnblogs.com/mickole/articles/3757278.html)
更多

掃碼關注或搜索架構探險之道
獲取最新文章,不積跬步無以致千里,堅持每週一更,堅持技術分享^_^
!
本文分享自微信公衆號 - 架構探險之道(zacsnz1314)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。