2020金三銀四衝擊BAT必備面試題(上篇):集合類+阻塞隊列+鎖


2020金三銀四衝擊BAT必備面試題(上篇):集合類+阻塞隊列+鎖

前言:

沒有前言,懶得寫!都是帶答案的面試真題,文末附:Java面試寶典+架構學習視頻+編程書籍

2020金三銀四衝擊BAT必備面試題(上篇):集合類+阻塞隊列+鎖

一、集合類

1. ArrayList的擴容機制

  1. 每次擴容是原來容量的1.5倍,通過移位的方法實現。
  2. 使用copyOf的方式進行擴容。

擴容算法是首先獲取到擴容前容器的大小。然後通過oldCapacity + (oldCapacity >> 1) 來計算擴容後的容器大小newCapacity。這裡用到了>> 右移運算,即容量增大原來的1.5倍。還要注意的是,這裡擴充容量時,用的時Arrays.copyOf方法,其內部也是使用的System.arraycopy方法。

區別:

  • arraycopy()需要目標數組,將原數組拷貝到你自己定義的數組裡,而且可以選擇拷貝的起點和長度以及放入新數組中的位置。
  • copyOf()是系統自動在內部新建一個數組,並返回該數組。

2. 數組和ArrayList的區別

  • 數組可以包含基本類型,ArrayList成員只能是對象。
  • 數組大小是固定的,ArrayList可以動態擴容。

3. ArrayList和LinkedList的區別

  • 線程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保證線程安全;
  • 數據結構:LinkedList 是基於雙向鏈表實現的,ArrayList 是基於數組實現的。
  • 快速隨機訪問:ArrayList 支持隨機訪問,所以查詢速度更快,LinkedList 添加、插入、刪除元素速度更快。
  • 內存空間佔用:ArrayList的空間浪費主要體現在在list列表的結尾會預留一定的容量空間,LinkedList使用Node來存儲數據每個Node中不僅存儲元素的值,還存儲了前一個 Node 的引用和後一個 Node 的引用,佔用內存更多。
  • 遍歷方式選擇:實現了RandomAccess接口的list,優先選擇普通for循環 ,其次foreach,未實現RandomAccess接口的list, 優先選擇iterator遍歷(foreach遍歷底層也是通過iterator實現的),大size的數據,千萬不要使用普通for循環。

4. 如何創建同步的List

可以通過Collections.sychronizeList將list轉換成同步list,或者直接使用CopyOnWriteArrayList。

5. CopyOnWriteArrayList

  • 讀時不加鎖,寫入時加鎖,寫入時創建一個新入組將老數組拷貝進入新數組,並將數據加入新數組。
  • 只能保證最終一致性。

6. Vector

ArrayList線程安全的一個版本,底層通過synchronize加鎖實現線程安全。

7. HashMap擴容機制

HashMap使用resize()方法來進行擴容,計算table數組的新容量和Node在新數組中的新位置,將舊數組中的值複製到新數組中,從而實現自動擴容。

  • 當空的HashMap實例添加元素時,會以默認容量16為table數組的長度擴容,此時 threshold = 16 * 0.75 = 12。
  • 當不為空的HashMap實例添加新元素數組容量不夠時,會以舊容量的2倍進行擴容,當然擴容也是大小限制的,擴容後的新容量要小於等於規定的最大容量,使用新容量創建新table數組,然後就是數組元素Node的複製了,計算Node位置的方法是 index = (n-1) & hash,這樣計算的好處是,Node在新數組中的位置要麼保持不變,要麼是原來位置加上舊數組的容量值,在新數組中的位置都是可以預期的(有規律的),並且鏈表上Node的順序也不會發生改變。

8. HashMap為什麼不是線程安全的

  • 沒有鎖操作,兩個線程操作同一個hashMap會出現線程安全的問題,可能會導致數據丟失。
  • resize的時候會出現死鎖,以為hash衝突之後採用鏈地址法解決hash衝突,但是兩個線程都進行擴容的時候,鏈表使用頭插法,導致出現循環引用,出現死鎖。1.8之後 鏈表都是採用尾插法。避免了死循環的問題。

9. 為什麼HashMap的hashCode要高16位異或hashCode

因為元素所處位置只與低n位相關,高16位與hashcode進行異或是為了減少碰撞。

異或是兩者相同返回0 不相同返回1。

10. 為什麼HashMap的容量要是2的N次冪

  • 取模時分配更均勻。
  • 擴容成本更低。

2^n下有特性:x%2^n=x&(2^n-1)只有2的冪次方才有此特性。

11. ConcurrentHashMap的實現

  • jdk1.7之前,使用分段鎖來實現,默認支持的併發度為16,segment繼承自reetrantlock,segment充當鎖角色。每個segment中包含一個小的hash表。size方法將segment的count相加,計算兩次,如果兩次結果相同,說明計算準確,否則每個segment重新加鎖計算。
  • jdk1.8之後取消分段鎖的設計,採用CAS+Synchronized保證線程安全。主要是鎖住鏈表的頭結點。size方法使用一個volatile變量baseCount記錄元素個數,當插入新數據或者刪除數據的時候會更新baseCount的值。

12. ConcurrentHashMap1.7與1.8異同

  • 1.8取消了分段鎖,鎖的粒度更小,減少併發衝突的概率。
  • 1.8採用了鏈表+紅黑樹的實現方式,對查詢的提升很大。

13. 為什麼ConcurrentHashMap讀操作不加鎖

  • ConcurrentHashMap只保證最終一致性,並不能保證強一致性。
  • 對於value使用valitile關鍵字,保證內存可見,能夠被多線程同時讀,並且不會讀到過期的值。根據java內存模型的hanpends-befor原則,對volatitle的寫入操作先於讀操作,即使兩個線程同時讀取和寫入同一個變量,也能是get操作拿到最新值
  • Node使用volatitle關鍵字標識是為了數組擴容時的可見性。

14. LinkedHashMap的實現

基於hashMap和雙向鏈表實現的,線程不安全。

15. HashSet的實現

  • 底層是通過hashMap實現的。
  • 判斷兩個對象是否相等,先判斷hashCode是否相等,如果相等再判斷equals,這就是為什麼重寫equals方法要重寫hashCode方法。

16. TreeMap的實現

底層使用紅黑樹實現。根據鍵值進行排序,key必須實現Compareable接口或者構造TreeMap時傳入Comparetor。

17. TreeSet的實現

底層使用TreeMap實現,即使用紅黑樹進行實現。
Set判斷兩個元素是否相等,先判斷hashCode再使用equals

18. 解決Hash衝突的方法

  • 開放定址法
  • 鏈地址法
  • 再hash法

19. List、Map、Set存儲的null值

  • list null值,加幾個存幾個。
  • set null值 只存一個。
  • map只存在一個null值對。

20. 平衡二叉樹AVL與紅黑樹的區別

  • 平衡二叉樹是高度平衡的,每次的插入和刪除,都要進行rebalance操作。
  • 紅黑樹不是高度平衡的。

紅黑樹定義:

  • 節點是紅色或黑色。
  • 根節點是黑色。
  • 每條路徑上的黑色節點數目相同。
  • 子節點和父節點的顏色不相同。


2020金三銀四衝擊BAT必備面試題(上篇):集合類+阻塞隊列+鎖

二. 阻塞隊列

1. 五種阻塞隊列介紹

  • ArrayBlockingQueue:有界隊列,底層使用數組實現,併發控制使用ReentrantLock控制,不管是插入操作還是讀取操作,都需要獲取鎖之後才能執行。
  • LinkedBlockingQueue:底層基於單向鏈表實現,既可以當做有界隊列,也可以當做無界隊列使用。使用兩個ReentrantLock實現併發控制:takelock和putlock。
  • SynchronousQueue:底層使用單向鏈表實現,只有一個元素,同步的意思是一個寫操作必須等到一個讀操作之後才返回,指的是讀寫線程的同步。
  • PriorityBlockingQueue:帶排序的阻塞隊列的實現,使用數組進行實現。併發控制使用ReentrantLock,隊列為無界隊列。
    有初始化參數指定隊列大小,但是會自動擴容。使用最小堆來實現排序。
  • DelayedQueue:DelayedQueue是使用PriorityBlockingQueue和Delayed實現的,內部定義了一個優先級隊列,當調用offer的時候,把Delayed對象加入隊列中,使用take先把first對象拿出來(peek),如果沒有到達閾值,進行await處理。

2. poll和peek的區別

都用於取隊列的頭結點,poll會刪除頭結點,peek不會刪除頭結點。

3. LinkedBlockingQueue

  • 是先進先出隊列FIFO。
  • 採用ReentrantLock保證線程安全

3.1、功能

3.1.1、增加

增加有三種方式,前提:隊列滿

2020金三銀四衝擊BAT必備面試題(上篇):集合類+阻塞隊列+鎖

3.1.2、刪除
刪除有三種方式,前提:隊列為空

2020金三銀四衝擊BAT必備面試題(上篇):集合類+阻塞隊列+鎖

3.2、簡單分析

  • LinkedBlockingQueue是一個阻塞隊列,內部由兩個ReentrantLock來實現出入隊列的線程安全,由各自的Condition對象的await和signal來實現等待和喚醒功能。
  • 基於單向鏈表的、範圍任意的(其實是有界的)、FIFO 阻塞隊列。
  • 頭結點和尾結點一開始總是指向一個哨兵的結點,它不持有實際數據,當隊列中有數據時,頭結點仍然指向這個哨兵,尾結點指向有效數據的最後一個結點。這樣做的好處在於,與計數器 count 結合後,對隊頭、隊尾的訪問可以獨立進行,而不需要判斷頭結點與尾結點的關係。
2020金三銀四衝擊BAT必備面試題(上篇):集合類+阻塞隊列+鎖

3.2.1、節點與屬性

<code>//鏈表節點內部類
static class Node {
//節點元素
E item;
Node next;
Node(E x) {
item = x;
}
}
//容量界限,如果未設定,則為Integer最大值
private final int capacity;
//當前元素個數
private final AtomicInteger count = new AtomicInteger();
//鏈表的頭:head.item == null
transient Node head;
//鏈表的尾:last.next == null
private transient Node last;
//take,poll等獲取鎖
private final ReentrantLock takeLock = new ReentrantLock();
//等待任務的等待隊列
private final Condition notEmpty = takeLock.newCondition();
//put,offer等插入鎖
private final ReentrantLock putLock = new ReentrantLock();
//等待插入的等待隊列
private final Condition notFull = putLock.newCondition();
/<code>

3.2.2、插入線程與獲取線程的相互通知

signalNotEmpty()方法,在插入線程發現隊列為空時調用,告知獲取線程需要等待。
signalNotFull()方法,在獲取線程發現隊列已滿時調用,告知插入線程需要等待。

<code>//表示等待take。put/offer調用,否則通常不會鎖定takeLock。
private void signalNotEmpty() {
//獲取takeLock
final ReentrantLock takeLock = this.takeLock;
//鎖定takeLock
takeLock.lock();
try {
//喚醒take線程等待隊列
notEmpty.signal();
} finally {
//釋放鎖
takeLock.unlock();
}
}
//表示等待put,take/poll 調用
private void signalNotFull() {
//獲取putLock
final ReentrantLock putLock = this.putLock;
//鎖定putLock
putLock.lock();
try {
//喚醒插入線程等待隊列
notFull.signal();
} finally {
//釋放鎖
putLock.unlock();
}
}/<code>

3.2.3、入隊與出隊操作

enqueue()方法只能在持有 putLock 鎖下執行,dequeue()在持有 takeLock 鎖下執行。

<code>//在隊列尾部插入
private void enqueue(Node node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
//last.next指向當前node
//尾指針後移
last = last.next = node;
}
//移除隊列頭
private E dequeue() {
// assert takeLock.isHeldByCurrentThread();
// assert head.item == null;
//保存頭指針
Node h = head;
//獲取當前鏈表第一個元素
Node first = h.next;
//頭指針的next指向自己
h.next = h; // help GC
//頭指針指向第一個元素
head = first;
//獲取第一個元素的值
E x = first.item;
//將第一個元素的值置空
first.item = null;
//返回第一個元素的值
return x;
}
/<code>

3.2.4、對兩把鎖的加鎖與釋放

在需要對兩把鎖同時加鎖時,把加鎖的順序與釋放的順序封裝成方法,確保所有地方都是一致的。而且獲取鎖時都是不響應中斷的,一直獲取直到加鎖成功,這就避免了第一把鎖加鎖成功,而第二把鎖加鎖失敗導致鎖不釋放的風險。

<code>//鎖定putLock和takeLock
void fullyLock() {
putLock.lock();
takeLock.lock();
}
//與fullyLock的加鎖順序相反,先解鎖takeLock,再解鎖putLock
void fullyUnlock() {
takeLock.unlock();
putLock.unlock();
}/<code>

3.3、源碼解讀
簡單介紹一下LinkedBlockingQueue中API的源碼,如構造方法,新增,獲取,刪除,drainTo。

3.3.1、構造函數
LinkedBlockingQueue有三個構造方法,其中無參構造儘量少用,因為容量為Integer的最大值,操作不當會出現內存溢出。

<code>public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
//參數校驗
if (capacity <= 0) throw new IllegalArgumentException();

//設置容量
this.capacity = capacity;
//首尾節點指向一個空節點
last = head = new Node(null);
}
public LinkedBlockingQueue(Collection extends E> c) {
this(Integer.MAX_VALUE);
//獲取putLock
final ReentrantLock putLock = this.putLock;
//鎖定
putLock.lock(); // Never contended, but necessary for visibility
try {
int n = 0;
for (E e : c) {
if (e == null)
throw new NullPointerException();
if (n == capacity)
throw new IllegalStateException("Queue full");
enqueue(new Node(e));
++n;
}
count.set(n);
} finally {
putLock.unlock();
}
}
/<code>

3.3.2、offer(E e)

將給定的元素設置到隊列中,如果設置成功返回true, 否則返回false。 e的值不能為空,否則拋出空指針異常。

<code>//如果可以在不超過隊列容量的情況下立即插入指定的元素到隊列的尾部,成功後返回true,如果隊列已滿,返回false。當使用容量受限的隊列時,此方法通常比方法BlockingQueue#add更可取,後者只能通過拋出異常才能插入元素。 

public boolean offer(E e) {
//非空判斷
if (e == null) throw new NullPointerException();
//計數器
final AtomicInteger count = this.count;
//如果隊列已滿,直接返回插入失敗
if (count.get() == capacity)
return false;
int c = -1;
//新建節點
Node node = new Node(e);
//獲取插入鎖
final ReentrantLock putLock = this.putLock;
//鎖定
putLock.lock();
try {
//如果隊列未滿
if (count.get() < capacity) {
//插入隊列
enqueue(node);
//計數
c = count.getAndIncrement();
//還有空餘空間
if (c + 1 < capacity)
//喚醒插入線程
notFull.signal();
}
} finally {
//解鎖
putLock.unlock();
}
//如果隊列為空
if (c == 0)
//通知獲取線程阻塞
signalNotEmpty();
//返回成功或者插入失敗
return c >= 0;
}
/<code>

3.3.3、put(E e)

將元素設置到隊列中,如果隊列中沒有多餘的空間,該方法會一直阻塞,直到隊列中有多餘的空間。

<code>public void put(E e) throws InterruptedException {
//不可以插入空元素
if (e == null) throw new NullPointerException();

//所有put/take/etc中的約定都是預先設置本地var
//除非設置,否則保持計數為負數表示失敗。
int c = -1;
//新建節點
Node node = new Node(e);
//獲取putLock
final ReentrantLock putLock = this.putLock;
//獲取計數器
final AtomicInteger count = this.count;
//可中斷加鎖,即在鎖獲取過程中不處理中斷狀態,而是直接拋出中斷異常,由上層調用者處理中斷。
putLock.lockInterruptibly();
try {
/*
* 注意count在wait守衛線程中使用,即使它沒有被鎖保護。
* 這是因為count只能在此時減少(所有其他put都被鎖定關閉),
* 如果它從容量更改,我們(或其他一些等待put)將收到信號。
* 類似地,count在其他等待守衛線程中的所有其他用途也是如此。

*/
//只要當前隊列已滿
while (count.get() == capacity) {
//通知插入線程等待
notFull.await();
}
//插入隊列
enqueue(node);
//數量加1
c = count.getAndIncrement();
//如果隊列增加1個元素還未滿
if (c + 1 < capacity)
//喚醒插入進程
notFull.signal();
} finally {
//解鎖
putLock.unlock();
}
//如果隊列中沒有元素了
if (c == 0)
//通知獲取線程等待
signalNotEmpty();
}
/<code>

3.3.4、peek()

非阻塞的獲取隊列中的第一個元素,不出隊列。

<code>public E peek() {
//隊列為空,直接返回
if (count.get() == 0)
return null;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
//獲取第一個元素,非哨兵

Node first = head.next;
//元素為空,返回null
if (first == null)
return null;
else
//返回第一個元素值
return first.item;
} finally {
takeLock.unlock();
}
}
/<code>

3.3.5、poll()
非阻塞的獲取隊列中的值,未獲取到返回null。

<code>public E poll() {
final AtomicInteger count = this.count;
//隊列為空,直接返回
if (count.get() == 0)
return null;
E x = null;
int c = -1;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
//隊列非空,獲取隊列中元素
if (count.get() > 0) {
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
}
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}/<code>

3.3.6、remove(Object o)
從隊列中移除指定的值。將兩把鎖都鎖定。

<code>public boolean remove(Object o) {
//不支持null
if (o == null) return false;
//鎖定兩個鎖
fullyLock();
try {
//迭代隊列
for (Node trail = head, p = trail.next;
p != null;
trail = p, p = p.next) {
//通過equals方法匹配待刪除元素
if (o.equals(p.item)) {
//移除p節點
unlink(p, trail);
//成功
return true;
}
}
//失敗
return false;
} finally {
//解鎖
fullyUnlock();
}
}
// 將內部節點p與前一個跟蹤斷開連接
void unlink(Node p, Node trail) {
// assert isFullyLocked();
// p.next is not changed, to allow iterators that are
// traversing p to maintain their weak-consistency guarantee.
//p節點內容置空
p.item = null;
//trail節點的next指向p的next
trail.next = p.next;
//如果p是隊尾

if (last == p)
//trail變為隊尾
last = trail;
//如果隊列已滿
if (count.getAndDecrement() == capacity)
//通知插入線程阻塞
notFull.signal();
}
/<code>

3.3.7、clear()

清空隊列。

<code>//原子性地從隊列中刪除所有元素。此調用返回後,隊列將為空。
public void clear() {
//鎖定
fullyLock();
try {
//清空數據,幫助垃圾回收
for (Node p, h = head; (p = h.next) != null; h = p) {
h.next = h;
p.item = null;
}
head = last;
// assert head.item == null && head.next == null;
//如果容量為0
if (count.getAndSet(0) == capacity)
//喚醒插入線程
notFull.signal();
} finally {
//解鎖
fullyUnlock();
}
}
/<code>

3.3.8、drainTo(Collection c)
將隊列中值,全部移除,併發設置到給定的集合中。

<code>public int drainTo(Collection super E> c, int maxElements) {
//各種判斷
if (c == null)
throw new NullPointerException();
if (c == this)
throw new IllegalArgumentException();
if (maxElements <= 0)
return 0;
boolean signalNotFull = false;
//鎖
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
//獲取要轉移的數量
int n = Math.min(maxElements, count.get());
// count.get provides visibility to first n Nodes
Node h = head;
int i = 0;
try {
//組裝集合
while (i < n) {
Node p = h.next;
c.add(p.item);
p.item = null;
h.next = h;
h = p;
++i;
}
return n;
} finally {
// Restore invariants even if c.add() threw
if (i > 0) {
// assert h.item == null;
head = h;
signalNotFull = (count.getAndAdd(-i) == capacity);
}
}
} finally {
takeLock.unlock();
if (signalNotFull)

signalNotFull();
}
}
/<code>


2020金三銀四衝擊BAT必備面試題(上篇):集合類+阻塞隊列+鎖

三. 鎖

1. 鎖狀態
鎖的狀態只能升級不能降級。

  • 無鎖:沒有鎖對資源進行鎖定,所有線程都能訪問並修改同一個資源,但同時只有一個線程能修改成功。其他修改失敗的線程會不斷重試,直到修改成功,如CAS原理和應用是無鎖的實現。
  • 偏向鎖:偏向鎖是指一段同步代碼一直被一個線程訪問,那個該線程會自動獲取鎖,降低獲取鎖的代價。
  • 輕量級鎖:是指當鎖是偏向鎖的時候,被另外的線程所訪問,偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高性能。通過cas操作和自旋來解決加鎖問題,自旋超過一定的次數或者已經有一個線程在自旋,又來一個線程獲取鎖時,輕量級鎖會升級為重量級鎖。
  • 重量級鎖:升級為重量級鎖,等待鎖的線程都會進入阻塞狀態。

2. 樂觀鎖與悲觀鎖

  • 樂觀鎖,每次拿數據的時候認為別人都不會修改,在更新的時候再判斷在此期間有沒有更新數據,可以使用版本號等機制,適合讀取多場景,提高性能。
  • 悲觀鎖,每次拿數據都認為別人會修改,都會上鎖,可以使用synchronized、獨佔鎖Lock、讀寫鎖等機制,適合寫多的場景,保證寫入操作正確。

3. 自旋鎖與適應性自旋鎖

  • 自旋鎖:指當一個線程在獲取鎖的時候,如果鎖已經被其他線程獲取,那麼該線程將循環等待,然後不斷判斷鎖是否能獲取成功,直到獲取到鎖才退出循環。
  • 優點:線程不進行上下文切換,減少了上下文切換的時間。
    存在的問題:如果線程持有鎖的時間較長,其他線程進入循環,消耗cpu。
  • 自適應自旋鎖:指的是自旋的時間不固定,由前一個在同一個鎖上自旋的時間和鎖擁有者的狀態來決定。如果在同一個對象上,剛剛通過自旋成功獲取過鎖,且持有鎖的線程正在運行中,那麼虛擬機就會認為這次自旋很有可能再次成功。反之自旋操作很少成功獲取鎖,那麼後面獲取這個鎖可能直接省略掉自旋的過程,直接阻塞線程。

4. 公平鎖與非公平鎖

  • 公平鎖是指多個線程按照申請鎖的順序直接進入隊列排隊,隊列中的第一個線程才能獲取鎖。
  • 非公平鎖是指線程先嚐試獲取鎖,獲取不到進入隊列中排隊,如果能獲取到,則無需阻塞直接獲取鎖。

5. 重入鎖與非重入鎖

重入鎖:同一個線程在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖,前提是鎖對象是相同的。

6. 共享鎖與排他鎖

  • 共享鎖是指一個鎖可以被多個線程鎖持有。
  • 排它鎖或者叫獨享鎖或者互斥鎖 指鎖一次只能被一個線程所持有。

7. 讀寫鎖

  • 讀鎖是共享的,寫鎖是獨佔的。
  • 讀讀之間不會互斥,讀寫互斥,寫寫互斥,讀寫鎖提高了讀的性能。

8. CAS

CompareAndSwap比較與交換,是一種無鎖算法,原子類使用了CAS實現了樂觀鎖。

帶來的問題:

  • ABA問題:解決思路在變量前面加版本號,每次變量更新的時候都將版本號+1,每次更新的時候要求版本>=當前版本(AtomicStampedReference)

循環時間長開銷大,CAS操作如果長時間執行不成功,會導致其一直自旋,cpu消耗大。

  • 只能保證一個共享變量的原子操作。
  • 可以把多個變量放在一個對象裡面進行CAS操作。

9. 鎖優化

9.1 鎖升級

  • 偏向鎖的升級:線程A獲取鎖對象時,會在java對象頭和棧幀中記錄偏向的線程A的id,線程A再次獲取鎖時,只需要比較java頭中的線程id與當前Id是否相等,如果一致則無需通過cas加鎖解鎖。如果不一致,說明有線程B來獲取鎖,那麼要判斷java頭中偏向鎖的線程是否存活,如果沒有存活,鎖對象被置為無鎖狀態,線程B可將鎖對象置為B的偏向鎖。如果存活,則查看A是否還需要繼續持有對當前鎖,如果不需要持有,則將鎖置為無鎖狀態,偏向新的線程,如果還繼續持有鎖對象,則暫停A線程,撤銷偏向鎖,將鎖升級為輕量級鎖。
  • 輕量級鎖的升級:線程A獲取輕量級鎖時會把鎖的對象頭複製到自己的線程棧針中,然後通過cas把對象頭中的內容替換為A所記錄的地址。此時線程B也想獲取鎖,發現A已經獲取鎖,那麼線程B就自旋等待。等到自旋次數到了或者線程A正在執行,線程B自旋等待,此時來了線程C來競爭鎖對象,這個時候輕量級鎖就會膨脹為重量級鎖。重量級鎖會把未獲得到鎖對象的線程全部變為阻塞狀態該,防止cpu空轉。

9.2 鎖粗化

將多個連續的加鎖,解鎖操作連接在一起,擴展成為一個範圍更大的鎖,避免頻繁的加解鎖操作。

9.3 鎖消除

通過逃逸分析,去除不可能存在共享資源競爭的鎖,通過這種方式消除沒有必要的鎖。

10. synchronized底層實現

  • synchronized通過Monitor實現同步,Monitor依賴於底層操作系統互斥鎖來實現線程同步。
  • java對象頭是由markword(標記字段)和klass point(類型指針)組成。markword存儲對象的hashcode,分代年齡和鎖標誌位信息。Klass point 指向對象元數據的指針,虛擬機通過這個指針來確定對象是哪個類的實例。
  • synchronized修飾同步代碼塊,是使用monitorenter和monitorexit來控制的,通過java對象頭中的鎖計數器。
  • 修飾方法時會將方法標識為ACCSYNCHRONIZE,JVM通過這個標誌來判斷方法是不是同步方法。

11. synchronized與ReentrantLock的區別

  • 兩者都是悲觀鎖,可重入鎖。
  • ReentrantLock 可中斷,可以實現公平鎖,可以綁定多個條件。
  • ReentrantLock需要顯示的調用鎖和釋放鎖,synchronized屬於java關鍵字,不需要顯式的釋放。

12. volatile關鍵字

  • 保證變量內存可見。
  • 禁止指令重排序。

volatile和synchronized的區別:

  • volatile不會阻塞,synchronized會阻塞。
  • volatile保證數據的內存可見性但不能保證原子性,synchronized兩者都能保證。
  • volatile主要解決變量在線程之間的可見性,而synchronized主要解決多線程訪問資源的同步性。

13. Atomic原子類實現

使用cas操作 + volatile + native方法保證同步。

14. AQS

AQS(AbstractQueuedSynchronizer)內部維護的是一個FIFO的雙向同步隊列,如果當前線程競爭鎖失敗,AQS會把當前線程以及等待狀態信息構造成一個Node加入到同步隊列中,同時在阻塞該線程。當獲取鎖的線程釋放鎖以後,會從隊列中喚醒一個阻塞的節點線程。使用內部的一個state來控制是否獲取鎖,當state=0時表示無鎖狀態,state>0時表示已經有線程獲取了鎖。

15. AQS的組件

  • semaphore 可指定多個線程同時訪問某個共享資源。
  • countDownLatch 一個線程A等待其他線程執行完成之後才繼續執行。
  • cyclicBarrier 一組線程等待至某個狀態之後同時執行。

countDownLatch和CyclicBarrier的區別

  • countDownLatch是一個線程等一組線程執行完成之後才執行, cyclicBarrier是一組線程互相等待至某個狀態之後,同時執行。
  • countDownLatch不能複用,cyclicBarrier可以重用。

16. 鎖降級

鎖降級是指將寫鎖降級為讀鎖,這個過程就是當前線程已經獲取到寫鎖的時候,再獲取到讀鎖,隨後釋放寫鎖的過程,這麼做的目的為的就是保證數據的可見性。

17. 逃逸分析

  • 逃逸分析就是分析對象的動態作用域,當一個對象在方法中被定義後,他可能被外部方法所引用,作為參數傳遞到其他方法中,成為方法逃逸,賦值給類變量或者可以被其他線程訪問的實例變量成為線程逃逸。
  • 使用逃逸分析,編譯器可以對代碼做優化。比如:同步省略(鎖消除),將堆分配轉化為棧分配,標量替換。
  • 使用逃逸分析的缺點,沒法保證逃逸分析的性能一定高於其他性能。極端的話經過逃逸分析後,所有的對象都逃逸了,那麼逃逸分析的過程就浪費了。

Java讀者福利:JavaBAT大廠面試真題+進階架構師學習視頻+編程書籍+對標年薪50W架構師進階路線路都已收集好!需要的朋友幫忙轉發一下,然後私信我【888】即可免費領取!

2020金三銀四衝擊BAT必備面試題(上篇):集合類+阻塞隊列+鎖


分享到:


相關文章: