鎖作為并發共享數據,保證一致性的工具,在java平臺有多種實現(如 synchronized 和 ReentrantLock等等 ) 。java平臺下的鎖如下所示:
1、自旋鎖2、自旋鎖的其他種類3、阻塞鎖4、可重入鎖5、讀寫鎖6、互斥鎖7、悲觀鎖8、樂觀鎖9、公平鎖10、非公平鎖11、偏向鎖12、對象鎖13、線程鎖14、鎖粗化15、輕量級鎖16、鎖消除17、鎖膨脹18、信號量
自旋鎖是采用讓當前線程不停地的在循環體內執行實現的,當循環的條件被其他線程改變時 才能進入臨界區。如下
public class SpinLock { PRivate AtomicReference<Thread> sign =new AtomicReference<>(); public void lock(){ Thread current = Thread.currentThread(); while(!sign .compareAndSet(null, current)){ } } public void unlock (){ Thread current = Thread.currentThread(); sign .compareAndSet(current, null); }}使用了CAS原子操作,lock函數將owner設置為當前線程,并且預測原來的值為空。unlock函數將owner設置為null,并且預測值為當前線程。
當有第二個線程調用lock操作時由于owner值不為空,導致循環一直被執行,直至第一個線程調用unlock函數將owner設置為null,第二個線程才能進入臨界區。
由于自旋鎖只是將當前線程不停地執行循環體,不進行線程狀態的改變,所以響應速度更快。但當線程數不停增加時,性能下降明顯,因為每個線程都需要執行,占用CPU時間。如果線程競爭不激烈,并且保持鎖的時間段。適合使用自旋鎖。
注:該例子為非公平鎖,獲得鎖的先后順序,不會按照進入lock的先后順序進行。
2.自旋鎖的其他種類
在自旋鎖中 另有三種常見的鎖形式:TicketLock ,CLHlock 和MCSlock
Ticket鎖主要解決的是訪問順序的問題,主要的問題是在多核cpu上
import java.util.concurrent.atomic.AtomicInteger;public class TicketLock { private AtomicInteger serviceNum = new AtomicInteger(); private AtomicInteger ticketNum = new AtomicInteger(); private static final ThreadLocal<Integer> LOCAL= new ThreadLocal<Integer>(); public void lock() { int myticket = ticketNum.getAndIncrement(); LOCAL.set(myticket); while (myticket != serviceNum.get()) { } } public void unlock() { int myticket = LOCAL.get(); serviceNum.compareAndSet(myticket, myticket + 1); }}每次都要查詢一個serviceNum 服務號,影響性能(必須要到主內存讀取,并阻止其他cpu修改)。CLHLock 和MCSLock 則是兩種類型相似的公平鎖,采用鏈表的形式進行排序,
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;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; }}CLHlock是不停的查詢前驅變量, 導致不適合在NUMA 架構下使用(在這種結構下,每個線程分布在不同的物理內存區域)
MCSLock則是對本地變量的節點進行循環。不存在CLHlock 的問題。
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;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) { } } } else { currentNode.next.isLocked = false; currentNode.next = null; } }}從代碼上 看,CLH 要比 MCS 更簡單,
CLH 的隊列是隱式的隊列,沒有真實的后繼結點屬性。
MCS 的隊列是顯式的隊列,有真實的后繼結點屬性。
JUC ReentrantLock 默認內部使用的鎖 即是 CLH鎖(有很多改進的地方,將自旋鎖換成了阻塞鎖等等)。
三、阻塞鎖:
阻塞鎖,與自旋鎖不同,改變了線程的運行狀態。在JAVA環境中,線程Thread有如下幾個狀態:
1,新建狀態
2,就緒狀態
3,運行狀態
4,阻塞狀態
5,死亡狀態
阻塞鎖,可以說是讓線程進入阻塞狀態進行等待,當獲得相應的信號(喚醒,時間) 時,才可以進入線程的準備就緒狀態,準備就緒狀態的所有線程,通過競爭,進入運行狀態。JAVA中,能夠進入/退出、阻塞狀態或包含阻塞鎖的方法有 ,synchronized 關鍵字(其中的重量鎖),ReentrantLock,Object.wait()/notify(),LockSupport.park()/unpart()(j.u.c經常使用)
下面是一個JAVA 阻塞鎖實例
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;import java.util.concurrent.locks.LockSupport;public class CLHLock1 { 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<CLHLock1, CLHNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLock1.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占用率過高,但進入時間以及恢復時間都要比自旋鎖略慢。
在競爭激烈的情況下 阻塞鎖的性能要明顯高于 自旋鎖。
理想的情況則是; 在線程競爭不激烈的情況下,使用自旋鎖,競爭激烈的情況下使用,阻塞鎖。
本文里面講的是廣義上的可重入鎖,而不是單指JAVA下的ReentrantLock。
可重入鎖,也叫做遞歸鎖,指的是同一線程 外層函數獲得鎖之后 ,內層遞歸函數仍然有獲取該鎖的代碼,但不受影響。在JAVA環境下 ReentrantLock 和synchronized 都是 可重入鎖
下面是使用實例
public class Test implements Runnable{ public synchronized void get(){ System.out.println(Thread.currentThread().getId()); set(); } public synchronized void set(){ System.out.println(Thread.currentThread().getId()); } @Override public void run() { get(); } public static void main(String[] args) { Test ss=new Test(); new Thread(ss).start(); new Thread(ss).start(); new Thread(ss).start(); }} |
public class Test implements Runnable { ReentrantLock lock = new ReentrantLock(); public void get() { lock.lock(); System.out.println(Thread.currentThread().getId()); set(); lock.unlock(); } public void set() { lock.lock(); System.out.println(Thread.currentThread().getId()); lock.unlock(); } @Override public void run() { get(); } public static void main(String[] args) { Test ss = new Test(); new Thread(ss).start(); new Thread(ss).start(); new Thread(ss).start(); }}
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); }}對于自旋鎖來說,1、若有同一線程兩調用lock() ,會導致第二次調用lock位置進行自旋,產生了死鎖說明這個鎖并不是可重入的。(在lock函數內,應驗證線程是否為已經獲得鎖的線程)2、若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); } }}該自旋鎖即為可重入鎖。
新聞熱點
疑難解答