悲觀鎖和樂觀鎖
synchronized同步方法最主要的問題是線程阻塞和喚醒帶來的性能消耗,阻塞同步是悲觀的併發策略,只要有可能出現競爭,都認爲一定要先加鎖;然而還有一種樂觀的併發策略,直接操作數據,如果沒有發現其他線程同時操作數據則認爲這個操作是成功的,如果其他線程也操作了數據,那麼操作是失敗的,一般採用不斷重試的手段(自旋),直到成功爲止。樂觀策略適用於併發程度不高且臨界區較小的場景,優點是不需要阻塞線程,屬於非阻塞同步手段,性能更高。
CAS
樂觀鎖併發策略主要有兩個重點階段,一個是對數據進行操作,另一個是檢測是否發生衝突(即是否存在同時操作數據的其他線程),這裏操作數據和衝突檢測需要具備原子性,即操作數據和衝突檢查必須同時成功或者同時失敗,這個原子性通過CAS指令來實現,目前絕大多數的CPU都支持CAS指令。
CAS指令需要三個參數:分別是內存地址,期望的舊值,新值。
自旋鎖
自旋鎖是樂觀鎖的一種實現,當線程曲獲取一個鎖時,如果發現該鎖被其他線程佔用,那麼進入一個無意義的循環不斷嘗試加鎖,直到成功獲取鎖。自旋鎖適用於臨界區比較小的場景,如果鎖被持有的時間過長,那麼自旋本身會長時間的白白浪費CPU資源。
public class SpinLock {
private final AtomicReference<Thread> owner = new AtomicReference<Thread>();
public void lock(){
while (!owner.compareAndSet(null, Thread.currentThread())){
System.out.println(Thread.currentThread().getName() + " loop");
}
System.out.println(Thread.currentThread().getName() + " lock");
}
public void unlock(){
owner.compareAndSet(Thread.currentThread(), null);
System.out.println(Thread.currentThread().getName() + " unlcok");
}
}
上述代碼中owner變量保存了持鎖線程,這裏有兩個缺點,第一個是沒有保證公平性,另一個是由於多個線程同時操作同一個共享變量owner,而每個CPU都會緩存該變量,任意一個線程加鎖和解鎖後,其他所有CPU中的owner緩存都立刻失效,而線程自旋過程中又都會使用(讀)該變量,所以各個CPU都需要重新讀內存(CPU的緩存一致性原理),因此自旋鎖會頻繁的進行緩存一致性同步操作,每次加鎖和解鎖都會帶來一次,這導致繁重的系統總線流量和內存操作,會降低性能。
公平自旋鎖
爲了解決公平性問題,可以讓鎖維護一個編號來表示下次該獲取鎖的線程,每個線程在申請鎖時首先被分配一個編號,然後始終自旋直到輪到自己然後嘗試加鎖。雖然解決了公平性問題,但是依然存在緩存同步導致性能下降的問題。
public class FairSpinLock {
private final AtomicInteger nextNo = new AtomicInteger();
private final AtomicInteger threadNo = new AtomicInteger();
public int lock(){
int myNo = threadNo.getAndIncrement();
while (nextNo.get() != myNo){
}
System.out.println(Thread.currentThread().getName() + " lock");
return myNo;
}
public void unlock(int threadNo){
int next = threadNo + 1;
nextNo.compareAndSet(threadNo, next);
System.out.println(Thread.currentThread().getName() + " unlcok");
}
}
MCS自旋鎖
自旋鎖之所以頻繁的發生緩存失效的問題,是因爲所有線程加鎖和解鎖都會操作同一個變量,因此如果是不同的變量,或者說多個線程操作不同變量,那麼可避免高頻率的緩存同步操作,這就是MCS的實現思路。
MCS基於鏈表實現,每個申請鎖的線程都對應鏈表上的一個節點,這些線程一直輪詢自身節點來確定自己是否獲得了鎖。獲得鎖的線程在釋放鎖的時候,負責通知後繼節點已獲取鎖,即更新後繼節點的運行狀態,這會導致其他CPU中該變量的緩存失效,但並不是所有線程都會使用這個後繼節點,所以不會發生所有CPU同時進行緩存一致性同步操作,而且僅在線程通知後繼線程時發生一次緩存失效,這樣緩存同步操作就減少很多,降低了系統總線和內存的開銷。
不支持重入,可加一個ThreadLocal變量記錄重入次數來實現。
public class McsLock {
/**
* 隊尾
*/
private volatile Node tail;
/**
* 隊尾原子操作
*/
private static final AtomicReferenceFieldUpdater<McsLock, Node> TAIL_UPDATER =
AtomicReferenceFieldUpdater.newUpdater(McsLock.class, Node.class, "tail");
/**
* 線程到節點的映射
*/
private ThreadLocal<Node> currentThreadNode = new ThreadLocal<>();
/**
* 加鎖
*/
public void lock(){
Node cNode = currentThreadNode.get();
if (cNode == null){
cNode = new Node();
currentThreadNode.set(cNode);
}
//原子地入隊並返回原隊尾
Node predecessor = TAIL_UPDATER.getAndSet(this, cNode); //step 1
if (predecessor != null){
//需要排隊
predecessor.setNext(cNode); //step 2
while (cNode.isWaiting){
//自旋等待前置線程更新自己的狀態 //step
}
}else{
//無需排隊,自己更新自己狀態
cNode.setWaiting(false);
}
System.out.println(Thread.currentThread().getName() + " lock");
return;
}
/**
* 解鎖
*/
public void unlock(){
// 獲取當前線程對應的節點
Node cNode = currentThreadNode.get();
if (cNode == null || cNode.isWaiting){
throw new RuntimeException(cNode + " not lock");
}
if (cNode.getNext() == null && !TAIL_UPDATER.compareAndSet(this, cNode, null)){
// 沒有後繼節點的情況,將tail置空
// 如果CAS操作失敗了表示突然有節點排在自己後面了,可能還不知道是誰,下面是等待後繼節點入隊
// 這裏之所以要忙等是因爲上述的lock操作中step 1執行完後,step 2可能還沒執行完
while (cNode.getNext() == null){
//step 5
}
}
if (cNode.getNext() != null){
// 通知後繼節點獲取鎖
cNode.getNext().setWaiting(false);
//help GC
cNode.setNext(null);
}
System.out.println(Thread.currentThread().getName() + " unlock");
}
/**
* 隊列節點類
*/
private static class Node {
//默認是等待狀態
private volatile boolean isWaiting = true;
//後繼節點
private volatile Node next;
public boolean isWaiting() {
return isWaiting;
}
public void setWaiting(boolean waiting) {
isWaiting = waiting;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
}
CLH自旋鎖
CLH鎖和MCS鎖的原理大致相同,都是各個線程各自關注各自的變量,來避免多線程同時操作同一個變量,從而減少緩存同步;不同點在於MCS自旋輪詢的是當前節點的屬性,而CLH輪詢的是前驅節點的屬性,來判斷前一個線程是否釋放了鎖。
public class ClhLock {
public static class Node {
//是否結束
private volatile boolean isOver = false;
}
private volatile Node tail;
private static AtomicReferenceFieldUpdater<ClhLock, Node> TAIL_UPDATER =
AtomicReferenceFieldUpdater.newUpdater(ClhLock.class, Node.class, "tail");
public void lock(Node cThread){
Node predesser = TAIL_UPDATER.getAndSet(this, cThread);
if (null != predesser){
while (predesser.isOver){}
}
System.out.println(Thread.currentThread().getName() + " lock");
}
public void unlock(Node cThread){
if (!TAIL_UPDATER.compareAndSet(this, cThread, null)){
//存在等待線程
cThread.isOver = true;
}
System.out.println(Thread.currentThread().getName() + " unlock");
}
從代碼可知,CLH比MCS要簡潔很多;CLH是在前驅節點的屬性上自旋,其等待隊列是隱式的,節點並不實際持有前驅或者後繼,通過每個節點都輪詢前驅形成邏輯鏈表,而MCS的隊列是物理存在的