並發編程相關面試題四

  • 2020 年 3 月 28 日
  • 筆記

一、Java開發中用過哪些鎖

1、樂觀鎖

  樂觀鎖顧名思義,就是很樂觀,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號等機制。樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量,在Java中java.util.concurrent.atomic包下面的原子變數類就是使用了樂觀鎖的一種實現方式CAS(Compare and Swap 比較並交換)實現的

  樂觀鎖適合讀操作非常多的場景,不加鎖會帶來大量的性能提升;  

  樂觀鎖在Java中的使用,是無鎖編程,常常採用的是CAS演算法,典型的例子就是原子類,通過CAS自旋實現原子操作的更新。

2、悲觀鎖

  悲觀鎖總是假設最壞的情況,每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。比如Java裡面的同步原語synchronized關鍵字的實現就是悲觀鎖。

  悲觀鎖適合寫操作非常多的場景;

  悲觀鎖在Java中的使用,就是利用各種鎖;

3、獨享鎖

  獨享鎖是指該鎖一次只能被一個執行緒所持有。

  獨享鎖通過AQS來實現的,通過實現不同的方法,來實現獨享鎖。

  對於Synchronized而言,當然是獨享鎖。

4、共享鎖

  共享鎖是指該鎖可被多個執行緒所持有。

  讀鎖的共享鎖可保證並發讀是非常高效的,讀寫,寫讀,寫寫的過程是互斥的。

  共享鎖也是通過AQS來實現的,通過實現不同的方法,來實現共享鎖。

5、互斥鎖

  互斥鎖在Java中的具體實現就是ReentrantLock。

6、讀寫鎖

  讀寫鎖在Java中的具體實現就是ReadWriteLock。

7、可重入鎖    

  重入鎖也叫作遞歸鎖,指的是同一個執行緒外層函數獲取到一把鎖後,內層函數同樣具有這把鎖的控制許可權;
  synchronized和ReentrantLock就是重入鎖對應的實現;
  synchronized重量級的鎖 ;
  ReentrantLock輕量級的鎖;

8、公平鎖

  公平鎖是指多個執行緒按照申請鎖的順序來獲取鎖。

  對於Java ReetrantLock而言,通過構造函數指定該鎖是否是公平鎖,默認是非公平鎖。非公平鎖的優點在於吞吐量比公平鎖大。

9、非公平鎖

  非公平鎖是指多個執行緒獲取鎖的順序並不是按照申請鎖的順序,有可能後申請的執行緒比先申請的執行緒優先獲取鎖。有可能,會造成優先順序反轉或者飢餓現象。  

  對於Synchronized而言,也是一種非公平鎖。由於其並不像ReentrantLock是通過AQS的來實現執行緒調度,所以並沒有任何辦法使其變成公平鎖。

10、分段鎖

  分段鎖其實是一種鎖的設計,並不是具體的一種鎖,對於ConcurrentHashMap而言,其並發的實現就是通過分段鎖的形式來實現高效的並發操作。

  我們以ConcurrentHashMap來說一下分段鎖的含義以及設計思想,ConcurrentHashMap中的分段鎖稱為Segment,它即類似於HashMap(JDK7和JDK8中HashMap的實現)的結構,即內部擁有一個Entry數組,數組中的每個元素又是一個鏈表;同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。

  當需要put元素的時候,並不是對整個hashmap進行加鎖,而是先通過hashcode來知道他要放在哪一個分段中,然後對這個分段進行加鎖,所以當多執行緒put的時候,只要不是放在一個分段中,就實現了真正的並行的插入。

  但是,在統計size的時候,可就是獲取hashmap全局資訊的時候,就需要獲取所有的分段鎖才能統計。

  分段鎖的設計目的是細化鎖的粒度,當操作不需要更新整個數組的時候,就僅僅針對數組中的一項進行加鎖操作。

11、偏向鎖  

  偏向鎖是指一段同步程式碼一直被一個執行緒所訪問,那麼該執行緒會自動獲取鎖。降低獲取鎖的代價。

12、輕量級鎖

  輕量級鎖是指當鎖是偏向鎖的時候,被另一個執行緒所訪問,偏向鎖就會升級為輕量級鎖,其他執行緒會通過自旋的形式嘗試獲取鎖,不會阻塞,提高性能。

13、重量級鎖

  重量級鎖是指當鎖為輕量級鎖的時候,另一個執行緒雖然是自旋,但自旋不會一直持續下去,當自旋一定次數的時候,還沒有獲取到鎖,就會進入阻塞,該鎖膨脹為重量級鎖。重量級鎖會讓他申請的執行緒進入阻塞,性能降低。

14、自旋鎖

  在Java中,自旋鎖是指嘗試獲取鎖的執行緒不會立即阻塞,而是採用循環的方式去嘗試獲取鎖,這樣的好處是減少執行緒上下文切換的消耗,缺點是循環會消耗CPU。

二、synchronized關鍵字理解

  使用了synchronized關鍵字可以輕鬆地解決多執行緒共享數據同步問題。

  synchronized關鍵字可以作為函數的修飾符,也可作為函數內的語句,也就是平時說的同步方法和同步語句塊。如果再細的分類,synchronized可作用於instance變數、object reference(對象引用)、static函數和class literals(類名稱字面常量)身上。 

  synchronized取得的鎖都是對象;每個對象只有一個鎖(lock)與之相關聯;實現同步是要很大的系統開銷作為代價的,甚至可能造成死鎖,所以盡量避免無謂的同步控制。

  synchronized的4種用法:

    1. 方法聲明時使用,執行緒獲得的是成員鎖;

    2. 對某一程式碼塊使用,synchronized後跟括弧,括弧里是變數,執行緒獲得的是成員鎖;

    3. synchronized後面括弧里是一對象,此時,執行緒獲得的是對象鎖;

    4. synchronized後面括弧里是類,此時,執行緒獲得的是對象鎖;

三、CAS無鎖機制

  CAS:Compare and Swap,即比較交換;

  jdk1.5增加了並發包java.util.concurrent.*,其下面的類使用CAS演算法實現了區別於synchronized同步鎖的一種樂觀鎖。jdk1.5之前java語言是靠synchronized關鍵字保證同步的,這是一種獨佔鎖,也是悲觀鎖;

  本身無鎖,採用樂觀鎖的思想,在數據操作時對比數據是否一致,如果一致代表之前沒有執行緒操作該數據,那麼就會更新數據,如果不一致代表有縣城更新則重試;

  CAS當中包含三個參數CAS(V,E,N),V標識要更新的變數,E標識預期值,N標識新值  

  運行過程:

    1.執行緒訪問時,先會將主記憶體中的數據同步到執行緒的工作記憶體當中;

    2.假設執行緒A和執行緒B都有對數據進行更改,那麼假如執行緒A先獲取到執行許可權;

    3.執行緒A先會對比工作記憶體當中的數據和主記憶體當中的數據是否一致,如果一致(V==E)則進行更新,不一致則刷新數據,重新循環判斷;

    4.這時更新完畢後,執行緒B也要進行數據更新,主記憶體數據和工作記憶體數據做對比,如果一致則進行更新,不一致則將主記憶體數據重新更新到工作記憶體,然後循環再次對比兩個記憶體中的數據,直到一致為止; 

  CAS無鎖機制存在一個問題

    ABA問題,如果將原來A的值改為了B,然後又改回了A,雖然最終結果沒有發生改變,但是在過程中是對該數據進行了修改操作

    解決該問題:在Java中並發包下有一個原子類:AtomicStampedReference,在該類當中通過版本控制判斷值到底是否被修改

    解釋:如果對值進行了更改則版本號+1,那麼在CAS當中不僅僅對比變數的值,還要對比版本號,如果值和版本號都相等則代表沒有被修改,如果有一方不相等代表進行過更改

    那麼就從主記憶體中重新刷新數據到工作記憶體然後循環對比,直到成功為止~

四、AQS

  AQS:全稱AbstractQueueSynchronizer,抽象隊列同步器,這個類在java.util.concurrent.locks包下

  它是一個底層同步工具類,比如CountDownLatch,Sammphore,ReentrantLock,ReentrantReadWriteLock等等都是基於AQS

    底層三個內容:

      1.state(用於計數器)

       2.執行緒標記(哪一個執行緒加的鎖)

       3.阻塞隊列(用於存放阻塞執行緒)

  AQS提供了一種實現阻塞鎖和一系列依賴FIFO等待隊列的同步器的框架,如下圖所示。AQS為一系列同步器依賴於一個單獨的原子變數(state)的同步器提供了一個非常有用的基礎。子類們必須定義改變state變數的protected方法,這些方法定義了state是如何被獲取或釋放的。

    

  J.U.C是基於AQS實現的,AQS是一個同步器,設計模式是模板模式。

  核心數據結構:雙向鏈表 + state(鎖狀態)

  底層操作:CAS

    

五、ReentrantLock底層實現

  ReentrantLock是基於AQS的,AQS是Java並發包中眾多同步組件的構建基礎,它通過一個int類型的狀態變數state和一個FIFO隊列來完成共享資源的獲取,執行緒的排隊等待等。AQS是個底層框架,採用模板方法模式,它定義了通用的較為複雜的邏輯骨架,比如執行緒的排隊,阻塞,喚醒等,將這些複雜但實質通用的部分抽取出來,這些都是需要構建同步組件的使用者無需關心的,使用者僅需重寫一些簡單的指定的方法即可(其實就是對於共享變數state的一些簡單的獲取釋放的操作)。

  無參構造器(默認為非公平鎖) 

public ReentrantLock() {         sync = new NonfairSync();//默認是非公平的  }

  synchronized是ReentrantLock內部實現的一個同步組件,它是Reentrantlock的一個靜態內部類,繼承於AQS;

  帶布爾值的構造器(是否公平)

public ReentrantLock(boolean fair) {          sync = fair ? new FairSync() : new NonfairSync();//fair為true,公平鎖;反之,非公平鎖  }

  此處可以指定是否採用公平鎖,FailSync和NonFailSync亦為Reentrantlock的靜態內部類,都繼承於synchronized;

六、ReentrantLock和synchronized之間的區別

  • synchronized 競爭鎖時會一直等待;ReentrantLock 可以嘗試獲取鎖,並得到獲取結果
  • synchronized 獲取鎖無法設置超時;ReentrantLock 可以設置獲取鎖的超時時間
  • synchronized 無法實現公平鎖;ReentrantLock 可以滿足公平鎖,即先等待先獲取到鎖
  • synchronized 控制等待和喚醒需要結合加鎖對象的 wait() 和 notify()、notifyAll();ReentrantLock 控制等待和喚醒需要結合 Condition 的 await() 和 signal()、signalAll() 方法
  • synchronized 是 JVM 層面實現的;ReentrantLock 是 JDK 程式碼層面實現
  • synchronized 在加鎖程式碼塊執行完或者出現異常,自動釋放鎖;ReentrantLock 不會自動釋放鎖,需要在 finally{} 程式碼塊顯示釋放

七、ReentrantReadWriteLock(讀寫鎖)

  相比Java中的鎖(Locks in Java)里Lock實現,讀寫鎖更複雜一些。

  假設你的程式中涉及到對一些共享資源的讀和寫操作,且寫操作沒有讀操作那麼頻繁。在沒有寫操作的時候,兩個執行緒同時讀一個資源沒有任何問題,所以應該允許多個執行緒能在同時讀取共享資源。但是如果有一個執行緒想去寫這些共享資源,就不應該再有其它執行緒對該資源進行讀或寫(譯者註:也就是說:讀-讀能共存,讀-寫不能共存,寫-寫不能共存)。這就需要一個讀/寫鎖來解決這個問題。

  Java5在java.util.concurrent包中已經包含了讀寫鎖。

package com.zn.lockTest;    import java.util.HashMap;  import java.util.Map;  import java.util.concurrent.locks.Lock;  import java.util.concurrent.locks.ReentrantReadWriteLock;    public class ReadWriteLock {      //創建一個集合      static Map<String,String> map=new HashMap<String,String>();      //創建一個讀寫鎖      static ReentrantReadWriteLock lock=new ReentrantReadWriteLock();      //獲取讀鎖      static Lock readLock=lock.readLock();      //獲取寫鎖      static Lock writeLock=lock.writeLock();      //寫操作      public Object put(String key,String value){          writeLock.lock();          try {              System.out.println("Write正在執行寫操作~");              Thread.sleep(100);              String put = map.put(key, value);              System.out.println("Write寫操作執行完畢~");              return put;          } catch (InterruptedException e) {              e.printStackTrace();          }finally {              writeLock.unlock();          }          return null;        }        //寫操作      public Object get(String key){          readLock.lock();          try {              System.out.println("Read正在執行讀操作~");              Thread.sleep(100);              String value = map.get(key);              System.out.println("Read讀操作執行完畢~");              return value;          } catch (InterruptedException e) {              e.printStackTrace();          }finally {              readLock.unlock();          }          return null;        }        public static void main(String[] args) {          ReadWriteLock lock=new ReadWriteLock();          for (int i = 0; i < 10; i++) {              int finalI = i;              new Thread(()->{                  try {                      //寫操作                      lock.put(finalI +"","value"+finalI);                      //讀操作                      System.out.println(lock.get(finalI+""));                  } catch (Exception e) {                      e.printStackTrace();                  }              }).start();          }        }  }

控制台效果:

  

八、BlockingQueue阻塞隊列的實現方式

  阻塞隊列(BlockingQueue)是一個支援兩個附加操作的隊列,這兩個附加的操作是:

    在隊列為空時,獲取元素的執行緒會等待隊列變為非空;

    當隊列滿時,存儲元素的執行緒會等待隊列可用;

  阻塞隊列常用於生產者和消費者的場景,生產者是往隊列里添加元素的執行緒,消費者是從隊列里拿元素的執行緒。阻塞隊列就是生產者存放元素的容器,而消費者也只從容器拿元素;

  在java中,BlockingQueue的介面位於java.util.concurrent包中,阻塞隊列是執行緒安全的;

  在新增呢的concurrent包中,BlockingQueue很好的解決了多執行緒中,如何高效安全“傳輸”數據的問題,通過這些高效並且執行緒安全的隊列類,為我們快速搭建高品質的多執行緒程式帶來極大的便利;

  常用的隊列主要由以下兩種:

    先進先出(FIFO)先插入的隊列的元素也最先出隊列,類似於排隊的功能,從某種程度上來說這種隊列也體現了一種公平性;

    後進後出(LIFO)後插入隊列的元素最先出隊列,這種隊列優先處理最近發生的事件;

1.ArrayBlockingQueue

  ArrayBlockingQueue是一個有邊界的阻塞隊列,它的內部實現是一個數組,有邊界意思就是它的容量是有限的,我們必須在其初始化的時候執行它的容量大小,容量大小一旦執行就不可改變;

  ArrayBlockingQueue是以先進先出的方式存儲數據,最新插入的對象是尾部,最新移除的對象是頭部;

package com.zn.queueTest;    import java.util.concurrent.ArrayBlockingQueue;  import java.util.concurrent.TimeUnit;    public class ArrayBlockingQueueTest {      public static void main(String[] args) throws InterruptedException {          ArrayBlockingQueue<String> arrays=new ArrayBlockingQueue<String>(3);          arrays.add("張三");          arrays.add("李四");          arrays.add("王五");            //添加阻塞隊列          arrays.offer("趙六",1, TimeUnit.SECONDS);            //poll方法相當於消費了隊列中的數據,隊列的數據就會刪除          System.out.println(arrays.poll());          System.out.println(arrays.poll());          System.out.println(arrays.poll());          System.out.println(arrays.poll());      }  }

控制台效果:

  

如果先出隊一條數據,此時被阻塞的數據就可以添加進來:

package com.zn.queueTest;    import java.util.concurrent.ArrayBlockingQueue;  import java.util.concurrent.TimeUnit;    public class ArrayBlockingQueueTest {      //出隊一條數據      public static void main(String[] args) throws InterruptedException {          ArrayBlockingQueue<String> arrays=new ArrayBlockingQueue<String>(3);          arrays.add("張三");          arrays.add("李四");          arrays.add("王五");          //出隊一條數據          System.out.println(arrays.poll());            //添加阻塞隊列          arrays.offer("趙六",1, TimeUnit.SECONDS);            //poll方法相當於消費了隊列中的數據,隊列的數據就會刪除          System.out.println(arrays.poll());          System.out.println(arrays.poll());          System.out.println(arrays.poll());      }  }

控制台效果:

  

2.LinkedBlockingQueue

  LinkedBlockingQueue阻塞隊列大小的配置時可選的,如果我們初始化時指定大小,它就是有邊界的,如果不指定,它就是無邊界的。說是無邊界,其實是採用了默認大小為Integer.MAX_VALUE容量,它的內部是一個鏈表;

  和ArrayBlockingQueue一樣,LinkedBlockingQueue也是以先進先出的方式存儲數據,最新插入的對象是尾部,最新移除的對象是頭部;

package com.zn.queueTest;    import java.util.concurrent.LinkedBlockingQueue;    public class LinkedBlockingQueueTest {      public static void main(String[] args) throws InterruptedException {          LinkedBlockingQueue linkedBlockingQueue=new LinkedBlockingQueue(3);          linkedBlockingQueue.add("A");          linkedBlockingQueue.add("B");          linkedBlockingQueue.add("C");          System.out.println(linkedBlockingQueue.poll());          System.out.println(linkedBlockingQueue.size());      }  }

控制台效果:

  

3.PriorityBlockingQueue

  PriorityBlockingQueue是一個沒有邊界的隊列,它的排序規則和java.util.PriorityQueue一樣。需要注意,PriorityBlockingQueue中國允許插入null對象;

  所有插入PriorityBlockingQueue的對象必須實現java.lang.Comparable介面,隊列優先順序的排序規則就是按照我們對這個介面的實現來定義的;

  另外,我們可以從PriorityBlockingQueue獲得一個迭代器Iterator,但這個迭代器並不保證按照優先順序順序進行迭代;

package com.zn.queueTest;    import java.util.concurrent.PriorityBlockingQueue;    public class PriorityBlockingQueueTest {      public static void main(String[] args) throws InterruptedException {          PriorityBlockingQueue<String> priorityBlockingQueue=new PriorityBlockingQueue<String>(3);          priorityBlockingQueue.add("AA");          priorityBlockingQueue.add("BB");          priorityBlockingQueue.add("CC");          System.out.println(priorityBlockingQueue.poll());          System.out.println(priorityBlockingQueue.size());      }  }

控制台效果:

   

4.SynchronousQueue

  SynchronousQueue隊列內部僅容納一個元素,當一個執行緒插入一個元素後會被阻塞,除非這個元素被另一個執行緒消費;

九、ConcurrentLinkedQueue

  ConcurrentLinkedQueue:是一個適用於高並發場景下的隊列,通過無鎖的方式,實現了高並髮狀態下的高性能,通常ConcurrentLinkedQueue性能好於BlockingQueue,它是一個基於鏈接節點的無界執行緒安全隊列。該隊列的元素遵循先進先出的原則。頭是最先加入的,尾是最近加入的,該隊列不允許null元素;

  ConcurrentLinkedQueue重要方法:

    add()和offer()都是加入元素的方法(在ConcurrentLinkedQueue中這兩個方法沒有任務區別);

    poll()和peek()都是取頭元素節點,區別在於前者會刪除元素,後者不會;

package com.zn.queueTest;    import java.util.concurrent.ConcurrentLinkedQueue;    public class ConcurrentLinkedQueueTest {      public static void main(String[] args) {          //準備隊列          ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();          //存放數據          queue.offer("張三");          queue.offer("李四");          queue.offer("王五");            //獲取隊列中數據個數          System.out.println("隊列中當前有:"+queue.size()+"個數據~");          //獲取隊列中頭數據  poll()方法相當於消費了隊列中的數據,隊列數據會隨之刪除          System.out.println("獲取隊列中的數據:"+queue.poll());          System.out.println("隊列中當前有:"+queue.size()+"個數據~");          //獲取隊列中數據,但是不會刪除          System.out.println("獲取隊列中的數據:"+queue.peek());          System.out.println("獲取隊列中的數據:"+queue.peek());          System.out.println("隊列中當前有:"+queue.size()+"個數據~");        }  }

控制台效果: