並發編程相關面試題四
- 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()+"個數據~"); } }
控制台效果:


