Java的各種鎖

不管是工作還是跳槽面試,鎖這個問題始終避不開,而且極易成為絆腳石,前幾天看到一些比較好的文檔,于是就搬過來做個筆記

  1. 鎖的分類
    • 自旋鎖:自旋,jvm默認是10次吧,有jvm自己控制。for去爭取鎖
    • 阻塞鎖:被阻塞的線程,不會爭奪鎖。
    • 可重入鎖:多次進入改鎖的域
    • 讀寫鎖:
    • 互斥鎖:鎖本身就是互斥的
    • 悲觀鎖:不相信,這里會是安全的,必須全部上鎖
    • 樂觀鎖:相信,這里是安全的。
    • 公平鎖:有優(yōu)先級的鎖
    • 非公平鎖:無優(yōu)先級的鎖
    • 偏向鎖:無競爭不鎖,有競爭掛起,轉(zhuǎn)為輕量鎖
    • 對象鎖:鎖住對象
    • 線程鎖:
    • 鎖粗化:多鎖變成一個,自己處理
    • 輕量級鎖:CAS 實現(xiàn)
    • 鎖消除:偏向鎖就是鎖消除的一種
    • 鎖膨脹:jvm實現(xiàn),鎖粗化
    • 信號量:使用阻塞鎖 實現(xiàn)的一種策略
    • 排它鎖:X鎖,若事務(wù)T對數(shù)據(jù)對象A加上X鎖,則只允許T讀取和修改A,其他任何事務(wù)都不能再對A加任何類型的鎖,直到T釋放A上的鎖。這就保證了其他事務(wù)在T釋放A上的鎖之前不能再讀取和修改A。
  1. 自旋鎖
    • 自旋鎖是采用讓當(dāng)前線程不停地的在循環(huán)體內(nèi)執(zhí)行實現(xiàn)的,當(dāng)循環(huán)的條件被其他線程改變時 才能進入臨界區(qū)
      // 注:該例子為非公平鎖,獲得鎖的先后順序,不會按照進入lock的先后順序進行。
      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函數(shù)將owner設(shè)置為當(dāng)前線程,并且預(yù)測原來的值為空。unlock函數(shù)將owner設(shè)置為null,并且預(yù)測值為當(dāng)前線程。當(dāng)有第二個線程調(diào)用lock操作時由于owner值不為空,導(dǎo)致循環(huán)一直被執(zhí)行,直至第一個線程調(diào)用unlock函數(shù)將owner設(shè)置為null,第二個線程才能進入臨界區(qū)。由于自旋鎖只是將當(dāng)前線程不停地執(zhí)行循環(huán)體,不進行線程狀態(tài)的改變,所以響應(yīng)速度更快。但當(dāng)線程數(shù)不停增加時,性能下降明顯,因為每個線程都需要執(zhí)行,占用CPU時間。如果線程競爭不激烈,并且保持鎖的時間段。適合使用自旋鎖。
    • 自旋鎖還有三種常見的鎖形式:TicketLock ,CLHlock 和MCSlock
      • Ticket鎖主要解決的是訪問順序的問題,主要的問題是在多核CPU上,代碼如下,每次都要查詢一個serviceNum 服務(wù)號,影響性能(必須要到主內(nèi)存讀取,并阻止其他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);
            }
        }
        
      • CLHLock:Craig, Landin, and Hagersten Locks,是一個自旋鎖,能確保無饑餓性,提供先來先服務(wù)的公平性;CLH鎖也是一種基于鏈表的可擴展、高性能、公平的自旋鎖,申請線程只在本地變量上自旋,它不斷輪詢前驅(qū)的狀態(tài),如果發(fā)現(xiàn)前驅(qū)釋放了鎖就結(jié)束自旋;
        • 當(dāng)一個線程需要獲取鎖時:
          1. 創(chuàng)建一個的CLHNode,將其中的locked設(shè)置為true表示需要獲取鎖;
          2. 線程對tail域調(diào)用getAndSet方法,使自己加入到隊列的尾部,同時獲取一個指向其前趨結(jié)點的引用preNode;
          3. 該線程就在前趨結(jié)點的locked字段上旋轉(zhuǎn),直到前趨結(jié)點釋放鎖;
          4. 當(dāng)一個線程需要釋放鎖時,將當(dāng)前結(jié)點的locked域設(shè)置為false,同時回收前趨結(jié)點;
        • 示例代碼如下:
        import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;        
        public class CLHLock {
            public static class CLHNode {
                private volatile boolean isLocked = true;
            }
            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則是對本地變量的節(jié)點進行循環(huán)。MSC與CLH最大的不同并不是鏈表是顯示還是隱式,而是線程自旋的規(guī)則不同:CLH是在前趨結(jié)點的locked域上自旋等待,而MCS是在自己的結(jié)點的locked域上自旋等待。正因為如此,它解決了CLH在NUMA系統(tǒng)架構(gòu)中獲取locked域狀態(tài)內(nèi)存過遠的問題;不存在CLHlock 的問題。
        import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
        public class MCSLock {
            public static class MCSNode {
                volatile MCSNode next;
                volatile boolean locked = true;
            }
            private static final ThreadLocal<MCSNode> node = new ThreadLocal<MCSNode>();
            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.locked) {}
                }
            }
            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.locked = false;
                    currentNode.next = null;
                }
            }
        }
        
    • 自旋鎖總結(jié)
      • 從代碼上 看,CLH 要比 MCS 更簡單;
      • CLH 的隊列是隱式的隊列,沒有真實的后繼結(jié)點屬性;
      • MCS 的隊列是顯式的隊列,有真實的后繼結(jié)點屬性;
      • JUC ReentrantLock 默認內(nèi)部使用的鎖 即是 CLH鎖(有很多改進的地方,將自旋鎖換成了阻塞鎖等等);
  2. 阻塞鎖
    • 與自旋鎖不同,改變了線程的運行狀態(tài)。在JAVA環(huán)境中,線程Thread有如下幾個狀態(tài):1,新建狀態(tài);2,就緒狀態(tài);3,運行狀態(tài);4,阻塞狀態(tài);5,死亡狀態(tài)
    • 阻塞鎖,可以說是讓線程進入阻塞狀態(tài)進行等待,當(dāng)獲得相應(yīng)的信號(喚醒,時間) 時,才可以進入線程的準備就緒狀態(tài),準備就緒狀態(tài)的所有線程,通過競爭,進入運行狀態(tài)。
    • JAVA中,能夠進入\退出、阻塞狀態(tài)或包含阻塞鎖的方法有 ,synchronized 關(guān)鍵字(其中的重量鎖),ReentrantLock,Object.wait()\notify(),LockSupport.park()/unpart()(JUC經(jīng)常使用)
    • 下面是一個JAVA 阻塞鎖實例
      import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
      import java.util.concurrent.locks.LockSupport;
      public class CLHLock1 {
          public static class CLHNode {
              private volatile Thread locked;
          }
          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.locked = 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.locked.getName());
                  LockSupport.unpark(node.locked);
              }
              node = null;
          }
      }
      
      • 在這里我們使用了LockSupport.unpark()的阻塞鎖。 該例子是將CLH鎖修改而成。阻塞鎖的優(yōu)勢在于,阻塞的線程不會占用cpu時間, 不會導(dǎo)致 CPu占用率過高,但進入時間以及恢復(fù)時間都要比自旋鎖略慢。在競爭激烈的情況下 阻塞鎖的性能要明顯高于 自旋鎖。理想的情況則是; 在線程競爭不激烈的情況下,使用自旋鎖,競爭激烈的情況下使用,阻塞鎖。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容