juc包下你常用的類?
線程池相關:
ThreadPoolExecutor
:最核心的線程池類,用于創建和管理線程池。通過它可以靈活地配置線程池的參數,如核心線程數、最大線程數、任務隊列等,以滿足不同的并發處理需求。Executors
:線程池工廠類,提供了一系列靜態方法來創建不同類型的線程池,如newFixedThreadPool
(創建固定線程數的線程池)、newCachedThreadPool
(創建可緩存線程池)、newSingleThreadExecutor
(創建單線程線程池)等,方便開發者快速創建線程池。
并發集合類:
ConcurrentHashMap
:線程安全的哈希映射表,用于在多線程環境下高效地存儲和訪問鍵值對。它采用了分段鎖等技術,允許多個線程同時訪問不同的段,提高了并發性能,在高并發場景下比傳統的Hashtable
性能更好。CopyOnWriteArrayList
:線程安全的列表,在對列表進行修改操作時,會創建一個新的底層數組,將修改操作應用到新數組上,而讀操作仍然可以在舊數組上進行,從而實現了讀寫分離,提高了并發讀的性能,適用于讀多寫少的場景。
同步工具類:
CountDownLatch
:允許一個或多個線程等待其他一組線程完成操作后再繼續執行。它通過一個計數器來實現,計數器初始化為線程的數量,每個線程完成任務后調用countDown
方法將計數器減一,當計數器為零時,等待的線程可以繼續執行。常用于多個線程完成各自任務后,再進行匯總或下一步操作的場景。CyclicBarrier
:讓一組線程互相等待,直到所有線程都到達某個屏障點后,再一起繼續執行。與CountDownLatch
不同的是,CyclicBarrier
可以重復使用,當所有線程都通過屏障后,計數器會重置,可以再次用于下一輪的等待。適用于多個線程需要協同工作,在某個階段完成后再一起進入下一個階段的場景。Semaphore
:信號量,用于控制同時訪問某個資源的線程數量。它維護了一個許可計數器,線程在訪問資源前需要獲取許可,如果有可用許可,則獲取成功并將許可計數器減一,否則線程需要等待,直到有其他線程釋放許可。常用于控制對有限資源的訪問,如數據庫連接池、線程池中的線程數量等。
原子類:
AtomicInteger
:原子整數類,提供了對整數類型的原子操作,如自增、自減、比較并交換等。通過硬件級別的原子指令來保證操作的原子性和線程安全性,避免了使用鎖帶來的性能開銷,在多線程環境下對整數進行計數、狀態標記等操作非常方便。AtomicReference
:原子引用類,用于對對象引用進行原子操作。可以保證在多線程環境下,對對象的更新操作是原子性的,即要么全部成功,要么全部失敗,不會出現數據不一致的情況。常用于實現無鎖數據結構或需要對對象進行原子更新的場景。
#怎么保證多線程安全?
- synchronized關鍵字:可以使用
synchronized
關鍵字來同步代碼塊或方法,確保同一時刻只有一個線程可以訪問這些代碼。對象鎖是通過synchronized
關鍵字鎖定對象的監視器(monitor)來實現的。
public synchronized void someMethod() { /* ... */ }public void anotherMethod() {synchronized (someObject) {/* ... */}
}
- volatile關鍵字:
volatile
關鍵字用于變量,確保所有線程看到的是該變量的最新值,而不是可能存儲在本地寄存器中的副本。
public volatile int sharedVariable;
- Lock接口和ReentrantLock類:
java.util.concurrent.locks.Lock
接口提供了比synchronized
更強大的鎖定機制,ReentrantLock
是一個實現該接口的例子,提供了更靈活的鎖管理和更高的性能。
private final ReentrantLock lock = new ReentrantLock();public void someMethod() {lock.lock();try {/* ... */} finally {lock.unlock();}
}
- 原子類:Java并發庫(
java.util.concurrent.atomic
)提供了原子類,如AtomicInteger
、AtomicLong
等,這些類提供了原子操作,可以用于更新基本類型的變量而無需額外的同步。
示例:
AtomicInteger counter = new AtomicInteger(0);int newValue = counter.incrementAndGet();
- 線程局部變量:
ThreadLocal
類可以為每個線程提供獨立的變量副本,這樣每個線程都擁有自己的變量,消除了競爭條件。
ThreadLocal<Integer> threadLocalVar = new ThreadLocal<>();threadLocalVar.set(10);
int value = threadLocalVar.get();
- 并發集合:使用
java.util.concurrent
包中的線程安全集合,如ConcurrentHashMap
、ConcurrentLinkedQueue
等,這些集合內部已經實現了線程安全的邏輯。 - JUC工具類: 使用
java.util.concurrent
包中的一些工具類可以用于控制線程間的同步和協作。例如:Semaphore
和CyclicBarrier
等。
#Java中有哪些常用的鎖,在什么場景下使用?
Java中的鎖是用于管理多線程并發訪問共享資源的關鍵機制。鎖可以確保在任意給定時間內只有一個線程可以訪問特定的資源,從而避免數據競爭和不一致性。Java提供了多種鎖機制,可以分為以下幾類:
- 內置鎖(synchronized):Java中的
synchronized
關鍵字是內置鎖機制的基礎,可以用于方法或代碼塊。當一個線程進入synchronized
代碼塊或方法時,它會獲取關聯對象的鎖;當線程離開該代碼塊或方法時,鎖會被釋放。如果其他線程嘗試獲取同一個對象的鎖,它們將被阻塞,直到鎖被釋放。其中,syncronized加鎖時有無鎖、偏向鎖、輕量級鎖和重量級鎖幾個級別。偏向鎖用于當一個線程進入同步塊時,如果沒有任何其他線程競爭,就會使用偏向鎖,以減少鎖的開銷。輕量級鎖使用線程棧上的數據結構,避免了操作系統級別的鎖。重量級鎖則涉及操作系統級的互斥鎖。 - ReentrantLock:
java.util.concurrent.locks.ReentrantLock
是一個顯式的鎖類,提供了比synchronized
更高級的功能,如可中斷的鎖等待、定時鎖等待、公平鎖選項等。ReentrantLock
使用lock()
和unlock()
方法來獲取和釋放鎖。其中,公平鎖按照線程請求鎖的順序來分配鎖,保證了鎖分配的公平性,但可能增加鎖的等待時間。非公平鎖不保證鎖分配的順序,可以減少鎖的競爭,提高性能,但可能造成某些線程的饑餓。 - 讀寫鎖(ReadWriteLock):
java.util.concurrent.locks.ReadWriteLock
接口定義了一種鎖,允許多個讀取者同時訪問共享資源,但只允許一個寫入者。讀寫鎖通常用于讀取遠多于寫入的情況,以提高并發性。 - 樂觀鎖和悲觀鎖:悲觀鎖(Pessimistic Locking)通常指在訪問數據前就鎖定資源,假設最壞的情況,即數據很可能被其他線程修改。
synchronized
和ReentrantLock
都是悲觀鎖的例子。樂觀鎖(Optimistic Locking)通常不鎖定資源,而是在更新數據時檢查數據是否已被其他線程修改。樂觀鎖常使用版本號或時間戳來實現。 - 自旋鎖:自旋鎖是一種鎖機制,線程在等待鎖時會持續循環檢查鎖是否可用,而不是放棄CPU并阻塞。通常可以使用CAS來實現。這在鎖等待時間很短的情況下可以提高性能,但過度自旋會浪費CPU資源。
#怎么在實踐中用鎖的?
Java提供了多種鎖的實現,包括synchronized
關鍵字、java.util.concurrent.locks
包下的Lock
接口及其具體實現如ReentrantLock
、ReadWriteLock
等。下面我們來看看這些鎖的使用方式。
synchronized
synchronized
關鍵字可以用于方法或代碼塊,它是Java中最早的鎖實現,使用起來非常簡單。
示例:synchronized方法
public class Counter {private int count = 0;public synchronized void increment() {count++;}public synchronized int getCount() {return count;}
}
示例:synchronized代碼塊
public class Counter {private Object lock = new Object();private int count = 0;public void increment() {synchronized (lock) {count++;}}
}
- 使用
Lock
接口
Lock
接口提供了比synchronized
更靈活的鎖操作,包括嘗試鎖、可中斷鎖、定時鎖等。ReentrantLock
是Lock
接口的一個實現。
示例:使用ReentrantLock
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class Counter {private Lock lock = new ReentrantLock();private int count = 0;public void increment() {lock.lock();try {count++;} finally {lock.unlock();}}
}
- 使用
ReadWriteLock
ReadWriteLock
接口提供了一種讀寫鎖的實現,允許多個讀操作同時進行,但寫操作是獨占的。
示例:使用ReadWriteLock
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;public class Cache {private ReadWriteLock lock = new ReentrantReadWriteLock();private Lock readLock = lock.readLock();private Lock writeLock = lock.writeLock();private Object data;public Object readData() {readLock.lock();try {return data;} finally {readLock.unlock();}}public void writeData(Object newData) {writeLock.lock();try {data = newData;} finally {writeLock.unlock();}}
}
#Java 并發工具你知道哪些?
Java 中一些常用的并發工具,它們位于?java.util.concurrent
?包中,常見的有:
- CountDownLatch:CountDownLatch 是一個同步輔助類,它允許一個或多個線程等待其他線程完成操作。它使用一個計數器進行初始化,調用?
countDown()
?方法會使計數器減一,當計數器的值減為 0 時,等待的線程會被喚醒。可以把它想象成一個倒計時器,當倒計時結束(計數器為 0)時,等待的事件就會發生。示例代碼:
import java.util.concurrent.CountDownLatch;public class CountDownLatchExample {public static void main(String[] args) throws InterruptedException {int numberOfThreads = 3;CountDownLatch latch = new CountDownLatch(numberOfThreads);// 創建并啟動三個工作線程for (int i = 0; i < numberOfThreads; i++) {new Thread(() -> {System.out.println(Thread.currentThread().getName() + " 正在工作");try {Thread.sleep(1000); // 模擬工作時間} catch (InterruptedException e) {e.printStackTrace();}latch.countDown(); // 完成工作,計數器減一System.out.println(Thread.currentThread().getName() + " 完成工作");}).start();}System.out.println("主線程等待工作線程完成");latch.await(); // 主線程等待,直到計數器為 0System.out.println("所有工作線程已完成,主線程繼續執行");}
}
- CyclicBarrier:CyclicBarrier 允許一組線程互相等待,直到到達一個公共的屏障點。當所有線程都到達這個屏障點后,它們可以繼續執行后續操作,并且這個屏障可以被重置循環使用。與?
CountDownLatch
?不同,CyclicBarrier
?側重于線程間的相互等待,而不是等待某些操作完成。示例代碼:
import java.util.concurrent.CyclicBarrier;public class CyclicBarrierExample {public static void main(String[] args) {int numberOfThreads = 3;CyclicBarrier barrier = new CyclicBarrier(numberOfThreads, () -> {System.out.println("所有線程都到達了屏障,繼續執行后續操作");});for (int i = 0; i < numberOfThreads; i++) {new Thread(() -> {try {System.out.println(Thread.currentThread().getName() + " 正在運行");Thread.sleep(1000); // 模擬運行時間barrier.await(); // 等待其他線程System.out.println(Thread.currentThread().getName() + " 已經通過屏障");} catch (Exception e) {e.printStackTrace();}}).start();}}
}
- Semaphore:Semaphore 是一個計數信號量,用于控制同時訪問某個共享資源的線程數量。通過?
acquire()
?方法獲取許可,使用?release()
?方法釋放許可。如果沒有許可可用,線程將被阻塞,直到有許可被釋放。可以用來限制對某些資源(如數據庫連接池、文件操作等)的并發訪問量。代碼如下:
import java.util.concurrent.Semaphore;public class SemaphoreExample {public static void main(String[] args) {Semaphore semaphore = new Semaphore(2); // 允許 2 個線程同時訪問for (int i = 0; i < 5; i++) {new Thread(() -> {try {semaphore.acquire(); // 獲取許可System.out.println(Thread.currentThread().getName() + " 獲得了許可");Thread.sleep(2000); // 模擬資源使用System.out.println(Thread.currentThread().getName() + " 釋放了許可");semaphore.release(); // 釋放許可} catch (InterruptedException e) {e.printStackTrace();}}).start();}}
}
- Future 和 Callable:Callable 是一個類似于?
Runnable
?的接口,但它可以返回結果,并且可以拋出異常。Future 用于表示一個異步計算的結果,可以通過它來獲取?Callable
?任務的執行結果或取消任務。代碼如下:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;public class FutureCallableExample {public static void main(String[] args) throws Exception {ExecutorService executorService = Executors.newSingleThreadExecutor();Callable<Integer> callable = () -> {System.out.println(Thread.currentThread().getName() + " 開始執行 Callable 任務");Thread.sleep(2000); // 模擬耗時操作return 42; // 返回結果};Future<Integer> future = executorService.submit(callable);System.out.println("主線程繼續執行其他任務");try {Integer result = future.get(); // 等待 Callable 任務完成并獲取結果System.out.println("Callable 任務的結果: " + result);} catch (Exception e) {e.printStackTrace();}executorService.shutdown();}
}
- ConcurrentHashMap:ConcurrentHashMap 是一個線程安全的哈希表,它允許多個線程同時進行讀操作,在一定程度上支持并發的修改操作,避免了?
HashMap
?在多線程環境下需要使用?synchronized
?或?Collections.synchronizedMap()
?進行同步的性能問題。代碼如下:
import java.util.concurrent.ConcurrentHashMap;public class ConcurrentHashMapExample {public static void main(String[] args) {ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();map.put("key1", 1);map.put("key2", 2);// 并發讀操作map.forEach((key, value) -> System.out.println(key + ": " + value));// 并發寫操作map.computeIfAbsent("key3", k -> 3);}
}
#CountDownLatch 是做什么的講一講?
CountDownLatch 是 Java 并發包(java.util.concurrent
)中的一個同步工具類,用于讓一個或多個線程等待其他線程完成操作后再繼續執行。
其核心是通過一個計數器(Counter)實現線程間的協調,常用于多線程任務的分階段控制或主線程等待多個子線程就緒的場景,核心原理:
- 初始化計數器:創建?
CountDownLatch
?時指定一個初始計數值(如?N
)。 - 等待線程阻塞:調用?
await()
?的線程會被阻塞,直到計數器變為 0。 - 任務完成通知:其他線程完成任務后調用?
countDown()
,使計數器減 1。 - 喚醒等待線程:當計數器減到 0 時,所有等待的線程會被喚醒。
主線程等待所有子線程就緒后啟動,代碼例子如下:
// 主線程啟動多個子線程執行任務,等待全部完成后統計結果
public class MainThreadWaitExample {public static void main(String[] args) throws InterruptedException {int threadCount = 3;CountDownLatch latch = new CountDownLatch(threadCount);for (int i = 0; i < threadCount; i++) {new Thread(() -> {try {System.out.println(Thread.currentThread().getName() + " 執行任務");Thread.sleep(1000);latch.countDown(); // 任務完成,計數器-1} catch (InterruptedException e) {e.printStackTrace();}}, "Worker-" + i).start();}latch.await(); // 主線程等待所有子線程完成任務System.out.println("所有任務已完成");}
}
#synchronized和reentrantlock及其應用場景?
synchronized 工作原理
synchronized是Java提供的原子性內置鎖,這種內置的并且使用者看不到的鎖也被稱為監視器鎖,
使用synchronized之后,會在編譯之后在同步的代碼塊前后加上monitorenter和monitorexit字節碼指令,他依賴操作系統底層互斥鎖實現。他的作用主要就是實現原子性操作和解決共享變量的內存可見性問題。
執行monitorenter指令時會嘗試獲取對象鎖,如果對象沒有被鎖定或者已經獲得了鎖,鎖的計數器+1。此時其他競爭鎖的線程則會進入等待隊列中。執行monitorexit指令時則會把計數器-1,當計數器值為0時,則鎖釋放,處于等待隊列中的線程再繼續競爭鎖。
synchronized是排它鎖,當一個線程獲得鎖之后,其他線程必須等待該線程釋放鎖后才能獲得鎖,而且由于Java中的線程和操作系統原生線程是一一對應的,線程被阻塞或者喚醒時時會從用戶態切換到內核態,這種轉換非常消耗性能。
從內存語義來說,加鎖的過程會清除工作內存中的共享變量,再從主內存讀取,而釋放鎖的過程則是將工作內存中的共享變量寫回主內存。
實際上大部分時候我認為說到monitorenter就行了,但是為了更清楚的描述,還是再具體一點。
如果再深入到源碼來說,synchronized實際上有兩個隊列waitSet和entryList。
- 當多個線程進入同步代碼塊時,首先進入entryList
- 有一個線程獲取到monitor鎖后,就賦值給當前線程,并且計數器+1
- 如果線程調用wait方法,將釋放鎖,當前線程置為null,計數器-1,同時進入waitSet等待被喚醒,調用notify或者notifyAll之后又會進入entryList競爭鎖
- 如果線程執行完畢,同樣釋放鎖,計數器-1,當前線程置為null
reentrantlock工作原理
ReentrantLock 的底層實現主要依賴于 AbstractQueuedSynchronizer(AQS)這個抽象類。AQS 是一個提供了基本同步機制的框架,其中包括了隊列、狀態值等。
ReentrantLock 在 AQS 的基礎上通過內部類 Sync 來實現具體的鎖操作。不同的 Sync 子類實現了公平鎖和非公平鎖的不同邏輯:
- 可中斷性: ReentrantLock 實現了可中斷性,這意味著線程在等待鎖的過程中,可以被其他線程中斷而提前結束等待。在底層,ReentrantLock 使用了與 LockSupport.park() 和 LockSupport.unpark() 相關的機制來實現可中斷性。
- 設置超時時間: ReentrantLock 支持在嘗試獲取鎖時設置超時時間,即等待一定時間后如果還未獲得鎖,則放棄鎖的獲取。這是通過內部的 tryAcquireNanos 方法來實現的。
- 公平鎖和非公平鎖: 在直接創建 ReentrantLock 對象時,默認情況下是非公平鎖。公平鎖是按照線程等待的順序來獲取鎖,而非公平鎖則允許多個線程在同一時刻競爭鎖,不考慮它們申請鎖的順序。公平鎖可以通過在創建 ReentrantLock 時傳入 true 來設置,例如:
ReentrantLock fairLock = new ReentrantLock(true);
- 多個條件變量: ReentrantLock 支持多個條件變量,每個條件變量可以與一個 ReentrantLock 關聯。這使得線程可以更靈活地進行等待和喚醒操作,而不僅僅是基于對象監視器的 wait() 和 notify()。多個條件變量的實現依賴于 Condition 接口,例如:
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// 使用下面方法進行等待和喚醒
condition.await();
condition.signal();
- 可重入性: ReentrantLock 支持可重入性,即同一個線程可以多次獲得同一把鎖,而不會造成死鎖。這是通過內部的 holdCount 計數來實現的。當一個線程多次獲取鎖時,holdCount 遞增,釋放鎖時遞減,只有當 holdCount 為零時,其他線程才有機會獲取鎖。
應用場景的區別
synchronized:
- 簡單同步需求: 當你需要對代碼塊或方法進行簡單的同步控制時,
synchronized
是一個很好的選擇。它使用起來簡單,不需要額外的資源管理,因為鎖會在方法退出或代碼塊執行完畢后自動釋放。 - 代碼塊同步: 如果你想對特定代碼段進行同步,而不是整個方法,可以使用
synchronized
代碼塊。這可以讓你更精細地控制同步的范圍,從而減少鎖的持有時間,提高并發性能。 - 內置鎖的使用:?
synchronized
關鍵字使用對象的內置鎖(也稱為監視器鎖),這在需要使用對象作為鎖對象的情況下很有用,尤其是在對象狀態與鎖保護的代碼緊密相關時。
ReentrantLock:
- 高級鎖功能需求:?
ReentrantLock
提供了synchronized
所不具備的高級功能,如公平鎖、響應中斷、定時鎖嘗試、以及多個條件變量。當你需要這些功能時,ReentrantLock
是更好的選擇。 - 性能優化: 在高度競爭的環境中,
ReentrantLock
可以提供比synchronized
更好的性能,因為它提供了更細粒度的控制,如嘗試鎖定和定時鎖定,可以減少線程阻塞的可能性。 - 復雜同步結構: 當你需要更復雜的同步結構,如需要多個條件變量來協調線程之間的通信時,
ReentrantLock
及其配套的Condition
對象可以提供更靈活的解決方案。
綜上,synchronized
適用于簡單同步需求和不需要額外鎖功能的場景,而ReentrantLock
適用于需要更高級鎖功能、性能優化或復雜同步邏輯的情況。選擇哪種同步機制取決于具體的應用需求和性能考慮。
#除了用synchronized,還有什么方法可以實現線程同步?
- 使用
ReentrantLock
類:ReentrantLock
是一個可重入的互斥鎖,相比synchronized
提供了更靈活的鎖定和解鎖操作。它還支持公平鎖和非公平鎖,以及可以響應中斷的鎖獲取操作。 - 使用
volatile
關鍵字:雖然volatile
不是一種鎖機制,但它可以確保變量的可見性。當一個變量被聲明為volatile
后,線程將直接從主內存中讀取該變量的值,這樣就能保證線程間變量的可見性。但它不具備原子性。 - 使用
Atomic
類:Java提供了一系列的原子類,例如AtomicInteger
、AtomicLong
、AtomicReference
等,用于實現對單個變量的原子操作,這些類在實現細節上利用了CAS(Compare-And-Swap)算法,可以用來實現無鎖的線程安全。
#synchronized鎖靜態方法和普通方法區別?
鎖的對象不同:
- 普通方法:鎖的是當前對象實例(
this
)。同一對象實例的?synchronized
?普通方法,同一時間只能被一個線程訪問;不同對象實例間互不影響,可被不同線程同時訪問各自的同步普通方法。 - 靜態方法:鎖的是當前類的?
Class
?對象。由于類的?Class
?對象全局唯一,無論多少個對象實例,該靜態同步方法同一時間只能被一個線程訪問。
作用范圍不同:
- 普通方法:僅對同一對象實例的同步方法調用互斥,不同對象實例的同步普通方法可并行執行。
- 靜態方法:對整個類的所有實例的該靜態方法調用都互斥,一個線程進入靜態同步方法,其他線程無法進入同一類任何實例的該方法。
多實例場景影響不同:
- 普通方法:多線程訪問不同對象實例的同步普通方法時,可同時執行。
- 靜態方法:不管有多少對象實例,同一時間僅一個線程能執行該靜態同步方法。
#synchronized和reentrantlock區別?
synchronized 和 ReentrantLock 都是 Java 中提供的可重入鎖:
- 用法不同:synchronized 可用來修飾普通方法、靜態方法和代碼塊,而 ReentrantLock 只能用在代碼塊上。
- 獲取鎖和釋放鎖方式不同:synchronized 會自動加鎖和釋放鎖,當進入 synchronized 修飾的代碼塊之后會自動加鎖,當離開 synchronized 的代碼段之后會自動釋放鎖。而 ReentrantLock 需要手動加鎖和釋放鎖
- 鎖類型不同:synchronized 屬于非公平鎖,而 ReentrantLock 既可以是公平鎖也可以是非公平鎖。
- 響應中斷不同:ReentrantLock 可以響應中斷,解決死鎖的問題,而 synchronized 不能響應中斷。
- 底層實現不同:synchronized 是 JVM 層面通過監視器實現的,而 ReentrantLock 是基于 AQS 實現的。
#怎么理解可重入鎖?
可重入鎖是指同一個線程在獲取了鎖之后,可以再次重復獲取該鎖而不會造成死鎖或其他問題。當一個線程持有鎖時,如果再次嘗試獲取該鎖,就會成功獲取而不會被阻塞。
ReentrantLock實現可重入鎖的機制是基于線程持有鎖的計數器。
- 當一個線程第一次獲取鎖時,計數器會加1,表示該線程持有了鎖。在此之后,如果同一個線程再次獲取鎖,計數器會再次加1。每次線程成功獲取鎖時,都會將計數器加1。
- 當線程釋放鎖時,計數器會相應地減1。只有當計數器減到0時,鎖才會完全釋放,其他線程才有機會獲取鎖。
這種計數器的設計使得同一個線程可以多次獲取同一個鎖,而不會造成死鎖或其他問題。每次獲取鎖時,計數器加1;每次釋放鎖時,計數器減1。只有當計數器減到0時,鎖才會完全釋放。
ReentrantLock通過這種計數器的方式,實現了可重入鎖的機制。它允許同一個線程多次獲取同一個鎖,并且能夠正確地處理鎖的獲取和釋放,避免了死鎖和其他并發問題。
#synchronized 支持重入嗎?如何實現的?
synchronized是基于原子性的內部鎖機制,是可重入的,因此在一個線程調用synchronized方法的同時在其方法體內部調用該對象另一個synchronized方法,也就是說一個線程得到一個對象鎖后再次請求該對象鎖,是允許的,這就是synchronized的可重入性。
synchronized底層是利用計算機系統mutex Lock實現的。每一個可重入鎖都會關聯一個線程ID和一個鎖狀態status。
當一個線程請求方法時,會去檢查鎖狀態。
- 如果鎖狀態是0,代表該鎖沒有被占用,使用CAS操作獲取鎖,將線程ID替換成自己的線程ID。
- 如果鎖狀態不是0,代表有線程在訪問該方法。此時,如果線程ID是自己的線程ID,如果是可重入鎖,會將status自增1,然后獲取到該鎖,進而執行相應的方法;如果是非重入鎖,就會進入阻塞隊列等待。
在釋放鎖時,
- 如果是可重入鎖的,每一次退出方法,就會將status減1,直至status的值為0,最后釋放該鎖。
- 如果非可重入鎖的,線程退出方法,直接就會釋放該鎖。
#syncronized鎖升級的過程講一下
具體的鎖升級的過程是:無鎖->偏向鎖->輕量級鎖->重量級鎖。
- 無鎖:這是沒有開啟偏向鎖的時候的狀態,在JDK1.6之后偏向鎖的默認開啟的,但是有一個偏向延遲,需要在JVM啟動之后的多少秒之后才能開啟,這個可以通過JVM參數進行設置,同時是否開啟偏向鎖也可以通過JVM參數設置。
- 偏向鎖:這個是在偏向鎖開啟之后的鎖的狀態,如果還沒有一個線程拿到這個鎖的話,這個狀態叫做匿名偏向,當一個線程拿到偏向鎖的時候,下次想要競爭鎖只需要拿線程ID跟MarkWord當中存儲的線程ID進行比較,如果線程ID相同則直接獲取鎖(相當于鎖偏向于這個線程),不需要進行CAS操作和將線程掛起的操作。
- 輕量級鎖:在這個狀態下線程主要是通過CAS操作實現的。將對象的MarkWord存儲到線程的虛擬機棧上,然后通過CAS將對象的MarkWord的內容設置為指向Displaced Mark Word的指針,如果設置成功則獲取鎖。在線程出臨界區的時候,也需要使用CAS,如果使用CAS替換成功則同步成功,如果失敗表示有其他線程在獲取鎖,那么就需要在釋放鎖之后將被掛起的線程喚醒。
- 重量級鎖:當有兩個以上的線程獲取鎖的時候輕量級鎖就會升級為重量級鎖,因為CAS如果沒有成功的話始終都在自旋,進行while循環操作,這是非常消耗CPU的,但是在升級為重量級鎖之后,線程會被操作系統調度然后掛起,這可以節約CPU資源。
了解完 4 種鎖狀態之后,我們就可以整體的來看一下鎖升級的過程了。?
?線程A進入 synchronized 開始搶鎖,JVM 會判斷當前是否是偏向鎖的狀態,如果是就會根據 Mark Word 中存儲的線程 ID 來判斷,當前線程A是否就是持有偏向鎖的線程。如果是,則忽略 check,線程A直接執行臨界區內的代碼。
但如果 Mark Word 里的線程不是線程 A,就會通過自旋嘗試獲取鎖,如果獲取到了,就將 Mark Word 中的線程 ID 改為自己的;如果競爭失敗,就會立馬撤銷偏向鎖,膨脹為輕量級鎖。
后續的競爭線程都會通過自旋來嘗試獲取鎖,如果自旋成功那么鎖的狀態仍然是輕量級鎖。然而如果競爭失敗,鎖會膨脹為重量級鎖,后續等待的競爭的線程都會被阻塞。
#JVM對Synchornized的優化?
synchronized 核心優化方案主要包含以下 4 個:
- 鎖膨脹:synchronized 從無鎖升級到偏向鎖,再到輕量級鎖,最后到重量級鎖的過程,它叫做鎖膨脹也叫做鎖升級。JDK 1.6 之前,synchronized 是重量級鎖,也就是說 synchronized 在釋放和獲取鎖時都會從用戶態轉換成內核態,而轉換的效率是比較低的。但有了鎖膨脹機制之后,synchronized 的狀態就多了無鎖、偏向鎖以及輕量級鎖了,這時候在進行并發操作時,大部分的場景都不需要用戶態到內核態的轉換了,這樣就大幅的提升了 synchronized 的性能。
- 鎖消除:指的是在某些情況下,JVM 虛擬機如果檢測不到某段代碼被共享和競爭的可能性,就會將這段代碼所屬的同步鎖消除掉,從而到底提高程序性能的目的。
- 鎖粗化:將多個連續的加鎖、解鎖操作連接在一起,擴展成一個范圍更大的鎖。
- 自適應自旋鎖:指通過自身循環,嘗試獲取鎖的一種方式,優點在于它避免一些線程的掛起和恢復操作,因為掛起線程和恢復線程都需要從用戶態轉入內核態,這個過程是比較慢的,所以通過自旋的方式可以一定程度上避免線程掛起和恢復所造成的性能開銷。
#介紹一下AQS
AQS全稱為AbstractQueuedSynchronizer,是Java中的一個抽象類。 AQS是一個用于構建鎖、同步器、協作工具類的工具類(框架)。
AQS核心思想是,如果被請求的共享資源空閑,那么就將當前請求資源的線程設置為有效的工作線程,將共享資源設置為鎖定狀態;如果共享資源被占用,就需要一定的阻塞等待喚醒機制來保證鎖分配。這個機制主要用的是CLH隊列的變體實現的,將暫時獲取不到鎖的線程加入到隊列中。
CLH:Craig、Landin and Hagersten隊列,是單向鏈表,AQS中的隊列是CLH變體的虛擬雙向隊列(FIFO),AQS是通過將每條請求共享資源的線程封裝成一個節點來實現鎖的分配。
主要原理圖如下:?
AQS使用一個Volatile的int類型的成員變量來表示同步狀態,通過內置的FIFO隊列來完成資源獲取的排隊工作,通過CAS完成對State值的修改。
AQS廣泛用于控制并發流程的類,如下圖:
其中Sync
是這些類中都有的內部類,其結構如下:
可以看到:Sync
是AQS
的實現。?AQS
主要完成的任務:
- 同步狀態(比如說計數器)的原子性管理;
- 線程的阻塞和解除阻塞;
- 隊列的管理。
AQS原理
AQS最核心的就是三大部分:
- 狀態:state;
- 控制線程搶鎖和配合的FIFO隊列(雙向鏈表);
- 期望協作工具類去實現的獲取/釋放等重要方法(重寫)。
狀態state
- 這里state的具體含義,會根據具體實現類的不同而不同:比如在Semapore里,他表示剩余許可證的數量;在CountDownLatch里,它表示還需要倒數的數量;在ReentrantLock中,state用來表示“鎖”的占有情況,包括可重入計數,當state的值為0的時候,標識該Lock不被任何線程所占有。
- state是volatile修飾的,并被并發修改,所以修改state的方法都需要保證線程安全,比如getState、setState以及compareAndSetState操作來讀取和更新這個狀態。這些方法都依賴于unsafe類。
FIFO隊列
- 這個隊列用來存放“等待的線程,AQS就是“排隊管理器”,當多個線程爭用同一把鎖時,必須有排隊機制將那些沒能拿到鎖的線程串在一起。當鎖釋放時,鎖管理器就會挑選一個合適的線程來占有這個剛剛釋放的鎖。
- AQS會維護一個等待的線程隊列,把線程都放到這個隊列里,這個隊列是雙向鏈表形式。
實現獲取/釋放等方法
- 這里的獲取和釋放方法,是利用AQS的協作工具類里最重要的方法,是由協作類自己去實現的,并且含義各不相同;
- 獲取方法:獲取操作會以來state變量,經常會阻塞(比如獲取不到鎖的時候)。在Semaphore中,獲取就是acquire方法,作用是獲取一個許可證; 而在CountDownLatch里面,獲取就是await方法,作用是等待,直到倒數結束;
- 釋放方法:在Semaphore中,釋放就是release方法,作用是釋放一個許可證; 在CountDownLatch里面,獲取就是countDown方法,作用是將倒數的數減一;
- 需要每個實現類重寫tryAcquire和tryRelease等方法。
#CAS 和 AQS 有什么關系?
CAS 和 AQS 兩者的區別:
- CAS 是一種樂觀鎖機制,它包含三個操作數:內存位置(V)、預期值(A)和新值(B)。CAS 操作的邏輯是,如果內存位置 V 的值等于預期值 A,則將其更新為新值 B,否則不做任何操作。整個過程是原子性的,通常由硬件指令支持,如在現代處理器上,
cmpxchg
?指令可以實現 CAS 操作。 - AQS 是一個用于構建鎖和同步器的框架,許多同步器如?
ReentrantLock
、Semaphore
、CountDownLatch
?等都是基于 AQS 構建的。AQS 使用一個?volatile
?的整數變量?state
?來表示同步狀態,通過內置的?FIFO
?隊列來管理等待線程。它提供了一些基本的操作,如?acquire
(獲取資源)和?release
(釋放資源),這些操作會修改?state
?的值,并根據?state
?的值來判斷線程是否可以獲取或釋放資源。AQS 的?acquire
?操作通常會先嘗試獲取資源,如果失敗,線程將被添加到等待隊列中,并阻塞等待。release
?操作會釋放資源,并喚醒等待隊列中的線程。
CAS 和 AQS 兩者的聯系:
- CAS 為 AQS 提供原子操作支持:AQS 內部使用 CAS 操作來更新?
state
?變量,以實現線程安全的狀態修改。在?acquire
?操作中,當線程嘗試獲取資源時,會使用 CAS 操作嘗試將?state
?從一個值更新為另一個值,如果更新失敗,說明資源已被占用,線程會進入等待隊列。在?release
?操作中,當線程釋放資源時,也會使用 CAS 操作將?state
?恢復到相應的值,以保證狀態更新的原子性。
#如何用 AQS 實現一個可重入的公平鎖?
AQS 實現一個可重入的公平鎖的詳細步驟:
- 繼承 AbstractQueuedSynchronizer:創建一個內部類繼承自?
AbstractQueuedSynchronizer
,重寫?tryAcquire
、tryRelease
、isHeldExclusively
?等方法,這些方法將用于實現鎖的獲取、釋放和判斷鎖是否被當前線程持有。 - 實現可重入邏輯:在?
tryAcquire
?方法中,檢查當前線程是否已經持有鎖,如果是,則增加鎖的持有次數(通過?state
?變量);如果不是,嘗試使用 CAS操作來獲取鎖。 - 實現公平性:在?
tryAcquire
?方法中,按照隊列順序來獲取鎖,即先檢查等待隊列中是否有線程在等待,如果有,當前線程必須進入隊列等待,而不是直接競爭鎖。 - 創建鎖的外部類:創建一個外部類,內部持有?
AbstractQueuedSynchronizer
?的子類對象,并提供?lock
?和?unlock
?方法,這些方法將調用?AbstractQueuedSynchronizer
?子類中的方法。
import java.util.concurrent.locks.AbstractQueuedSynchronizer;public class FairReentrantLock {private static class Sync extends AbstractQueuedSynchronizer {// 判斷鎖是否被當前線程持有protected boolean isHeldExclusively() {return getExclusiveOwnerThread() == Thread.currentThread();}// 嘗試獲取鎖protected boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {// 公平性檢查:檢查隊列中是否有前驅節點,如果有,則當前線程不能獲取鎖if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}} else if (current == getExclusiveOwnerThread()) {// 可重入邏輯:如果是當前線程持有鎖,則增加持有次數int nextc = c + acquires;if (nextc < 0) {throw new Error("Maximum lock count exceeded");}setState(nextc);return true;}return false;}// 嘗試釋放鎖protected boolean tryRelease(int releases) {int c = getState() - releases;if (Thread.currentThread()!= getExclusiveOwnerThread()) {throw new IllegalMonitorStateException();}boolean free = false;if (c == 0) {free = true;setExclusiveOwnerThread(null);}setState(c);return free;}// 提供一個條件變量,用于實現更復雜的同步需求,這里只是簡單實現ConditionObject newCondition() {return new ConditionObject();}}private final Sync sync = new Sync();// 加鎖方法public void lock() {sync.acquire(1);}// 解鎖方法public void unlock() {sync.release(1);}// 判斷當前線程是否持有鎖public boolean isLocked() {return sync.isHeldExclusively();}// 提供一個條件變量,用于實現更復雜的同步需求,這里只是簡單實現public Condition newCondition() {return sync.newCondition();}
}
代碼解釋:
內部類 Sync:
-
isHeldExclusively
:使用?getExclusiveOwnerThread
?方法檢查當前鎖是否被當前線程持有。 -
tryAcquire
:-
首先獲取當前鎖的狀態?
c
。 -
如果?
c
?為 0,表示鎖未被持有,此時進行公平性檢查,通過?hasQueuedPredecessors
?檢查是否有前驅節點在等待隊列中。如果沒有,使用?compareAndSetState
?嘗試將狀態設置為?acquires
(通常為 1),并設置當前線程為鎖的持有線程。 -
如果?
c
?不為 0,說明鎖已被持有,檢查是否為當前線程持有。如果是,增加鎖的持有次數(可重入),但要防止溢出。
-
-
tryRelease
:-
先將狀態減?
releases
(通常為 1)。 -
檢查當前線程是否為鎖的持有線程,如果不是,拋出異常。
-
如果狀態減為 0,說明鎖被完全釋放,將持有線程設為?
null
。
-
-
newCondition
:創建一個?ConditionObject
?用于更復雜的同步操作,如等待 / 通知機制。
外部類 FairReentrantLock:
lock
?方法:調用?sync.acquire(1)
?嘗試獲取鎖。unlock
?方法:調用?sync.release(1)
?釋放鎖。isLocked
?方法:調用?sync.isHeldExclusively
?判斷鎖是否被當前線程持有。newCondition
?方法:調用?sync.newCondition
?提供條件變量。
#Threadlocal作用,原理,具體里面存的key value是啥,會有什么問題,如何解決?
ThreadLocal
是Java中用于解決線程安全問題的一種機制,它允許創建線程局部變量,即每個線程都有自己獨立的變量副本,從而避免了線程間的資源共享和同步問題。
從內存結構圖,我們可以看到:
- Thread類中,有個ThreadLocal.ThreadLocalMap 的成員變量。
- ThreadLocalMap內部維護了Entry數組,每個Entry代表一個完整的對象,key是ThreadLocal本身,value是ThreadLocal的泛型對象值。
ThreadLocal的作用
- 線程隔離:
ThreadLocal
為每個線程提供了獨立的變量副本,這意味著線程之間不會相互影響,可以安全地在多線程環境中使用這些變量而不必擔心數據競爭或同步問題。 - 降低耦合度:在同一個線程內的多個函數或組件之間,使用
ThreadLocal
可以減少參數的傳遞,降低代碼之間的耦合度,使代碼更加清晰和模塊化。 - 性能優勢:由于
ThreadLocal
避免了線程間的同步開銷,所以在大量線程并發執行時,相比傳統的鎖機制,它可以提供更好的性能。
ThreadLocal的原理
ThreadLocal
的實現依賴于Thread
類中的一個ThreadLocalMap
字段,這是一個存儲ThreadLocal
變量本身和對應值的映射。每個線程都有自己的ThreadLocalMap
實例,用于存儲該線程所持有的所有ThreadLocal
變量的值。
當你創建一個ThreadLocal
變量時,它實際上就是一個ThreadLocal
對象的實例。每個ThreadLocal
對象都可以存儲任意類型的值,這個值對每個線程來說是獨立的。
-
當調用
ThreadLocal
的get()
方法時,ThreadLocal
會檢查當前線程的ThreadLocalMap
中是否有與之關聯的值。 -
如果有,返回該值;
-
如果沒有,會調用
initialValue()
方法(如果重寫了的話)來初始化該值,然后將其放入ThreadLocalMap
中并返回。 -
當調用
set()
方法時,ThreadLocal
會將給定的值與當前線程關聯起來,即在當前線程的ThreadLocalMap
中存儲一個鍵值對,鍵是ThreadLocal
對象自身,值是傳入的值。 -
當調用
remove()
方法時,會從當前線程的ThreadLocalMap
中移除與該ThreadLocal
對象關聯的條目。
可能存在的問題
當一個線程結束時,其ThreadLocalMap
也會隨之銷毀,但是ThreadLocal
對象本身不會立即被垃圾回收,直到沒有其他引用指向它為止。
因此,在使用ThreadLocal
時需要注意,如果不顯式調用remove()
方法,或者線程結束時未正確清理ThreadLocal
變量,可能會導致內存泄漏,因為ThreadLocalMap
會持續持有ThreadLocal
變量的引用,即使這些變量不再被其他地方引用。
因此,實際應用中需要在使用完ThreadLocal
變量后調用remove()
方法釋放資源。
#悲觀鎖和樂觀鎖的區別?
- 樂觀鎖: 就像它的名字一樣,對于并發間操作產生的線程安全問題持樂觀狀態,樂觀鎖認為競爭不總 是會發生,因此它不需要持有鎖,將比較-替換這兩個動作作為一個原子操作嘗試去修改內存中的變量,如果失敗則表示發生沖突,那么就應該有相應的重試邏輯。
- 悲觀鎖: 還是像它的名字一樣,對于并發間操作產生的線程安全問題持悲觀狀態,悲觀鎖認為競爭總 是會發生,因此每次對某資源進行操作時,都會持有一個獨占的鎖,就像 synchronized,不管三七二十一,直接上了鎖就操作資源了。
#Java中想實現一個樂觀鎖,都有哪些方式?
- CAS(Compare and Swap)操作:?CAS 是樂觀鎖的基礎。Java 提供了 java.util.concurrent.atomic 包,包含各種原子變量類(如 AtomicInteger、AtomicLong),這些類使用 CAS 操作實現了線程安全的原子操作,可以用來實現樂觀鎖。
- 版本號控制:增加一個版本號字段記錄數據更新時候的版本,每次更新時遞增版本號。在更新數據時,同時比較版本號,若當前版本號和更新前獲取的版本號一致,則更新成功,否則失敗。
- 時間戳:使用時間戳記錄數據的更新時間,在更新數據時,在比較時間戳。如果當前時間戳大于數據的時間戳,則說明數據已經被其他線程更新,更新失敗。
#CAS 有什么缺點?
CAS的缺點主要有3點:
- ABA問題:ABA的問題指的是在CAS更新的過程中,當讀取到的值是A,然后準備賦值的時候仍然是A,但是實際上有可能A的值被改成了B,然后又被改回了A,這個CAS更新的漏洞就叫做ABA。只是ABA的問題大部分場景下都不影響并發的最終效果。Java中有AtomicStampedReference來解決這個問題,他加入了預期標志和更新后標志兩個字段,更新時不光檢查值,還要檢查當前的標志是否等于預期標志,全部相等的話才會更新。
- 循環時間長開銷大:自旋CAS的方式如果長時間不成功,會給CPU帶來很大的開銷。
- 只能保證一個共享變量的原子操作:只對一個共享變量操作可以保證原子性,但是多個則不行,多個可以通過AtomicReference來處理或者使用鎖synchronized實現。
#為什么不能所有的鎖都用CAS?
CAS操作是基于循環重試的機制,如果CAS操作一直未能成功,線程會一直自旋重試,占用CPU資源。在高并發情況下,大量線程自旋會導致CPU資源浪費。
#CAS 有什么問題,Java是怎么解決的?
會有 ABA 的問題,變量值在操作過程中先被其他線程從?A?修改為?B,又被改回?A,CAS 無法感知中途變化,導致操作誤判為“未變更”。比如:
- 線程1讀取變量為
A
,準備改為C
。 - 此時線程2將變量
A
→B
→A
。 - 線程1的CAS執行時發現仍是
A
,但狀態已丟失中間變化。
Java 提供的工具類會在 CAS 操作中增加版本號(Stamp)或標記,每次修改都更新版本號,使得即使值相同也能識別變更歷史。比如,可以用 AtomicStampedReference 來解決 ABA 問題,通過比對值和版本號識別ABA問題。
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0);// 嘗試修改值并更新版本號
boolean success = ref.compareAndSet(100, 200, 0, 1);
// 前提:當前值=100 且 版本號=0,才會更新為(200,1)
#voliatle關鍵字有什么作用?
volatite作用有 2 個:
-
保證變量對所有線程的可見性。當一個變量被聲明為volatile時,它會保證對這個變量的寫操作會立即刷新到主存中,而對這個變量的讀操作會直接從主存中讀取,從而確保了多線程環境下對該變量訪問的可見性。這意味著一個線程修改了volatile變量的值,其他線程能夠立刻看到這個修改,不會受到各自線程工作內存的影響。
-
禁止指令重排序優化。volatile關鍵字在Java中主要通過內存屏障來禁止特定類型的指令重排序。
-
1)寫-寫(Write-Write)屏障:在對volatile變量執行寫操作之前,會插入一個寫屏障。這確保了在該變量寫操作之前的所有普通寫操作都已完成,防止了這些寫操作被移到volatile寫操作之后。
-
2)讀-寫(Read-Write)屏障:在對volatile變量執行讀操作之后,會插入一個讀屏障。它確保了對volatile變量的讀操作之后的所有普通讀操作都不會被提前到volatile讀之前執行,保證了讀取到的數據是最新的。
-
3)寫-讀(Write-Read)屏障:這是最重要的一個屏障,它發生在volatile寫之后和volatile讀之前。這個屏障確保了volatile寫操作之前的所有內存操作(包括寫操作)都不會被重排序到volatile讀之后,同時也確保了volatile讀操作之后的所有內存操作(包括讀操作)都不會被重排序到volatile寫之前。
-
#指令重排序的原理是什么?
在執行程序時,為了提高性能,處理器和編譯器常常會對指令進行重排序,但是重排序要滿足下面 2 個條件才能進行:
- 在單線程環境下不能改變程序運行的結果
- 存在數據依賴關系的不允許重排序。
所以重排序不會對單線程有影響,只會破壞多線程的執行語義。
我們看這個例子,A和C之間存在數據依賴關系,同時B和C之間也存在數據依賴關系。因此在最終執行的指令序列中,C不能被重排序到A和B的前面,如果C排到A和B的前面,那么程序的結果將會被改變。但A和B之間沒有數據依賴關系,編譯器和處理器可以重排序A和B之間的執行順序。
#volatile可以保證線程安全嗎?
volatile關鍵字可以保證可見性,但不能保證原子性,因此不能完全保證線程安全。volatile關鍵字用于修飾變量,當一個線程修改了volatile修飾的變量的值,其他線程能夠立即看到最新的值,從而避免了線程之間的數據不一致。
但是,volatile并不能解決多線程并發下的復合操作問題,比如i++這種操作不是原子操作,如果多個線程同時對i進行自增操作,volatile不能保證線程安全。對于復合操作,需要使用synchronized關鍵字或者Lock來保證原子性和線程安全。
#volatile和sychronized比較?
Synchronized解決了多線程訪問共享資源時可能出現的競態條件和數據不一致的問題,保證了線程安全性。Volatile解決了變量在多線程環境下的可見性和有序性問題,確保了變量的修改對其他線程是可見的。
- Synchronized: Synchronized是一種排他性的同步機制,保證了多個線程訪問共享資源時的互斥性,即同一時刻只允許一個線程訪問共享資源。通過對代碼塊或方法添加Synchronized關鍵字來實現同步。
- Volatile: Volatile是一種輕量級的同步機制,用來保證變量的可見性和禁止指令重排序。當一個變量被聲明為Volatile時,線程在讀取該變量時會直接從內存中讀取,而不會使用緩存,同時對該變量的寫操作會立即刷回主內存,而不是緩存在本地內存中。
#什么是公平鎖和非公平鎖?
- 公平鎖:?指多個線程按照申請鎖的順序來獲取鎖,線程直接進入隊列中排隊,隊列中的第一個線程才能獲得鎖。公平鎖的優點在于各個線程公平平等,每個線程等待一段時間后,都有執行的機會,而它的缺點就在于整體執行速度更慢,吞吐量更小。
- 非公平鎖:?多個線程加鎖時直接嘗試獲取鎖,能搶到鎖到直接占有鎖,搶不到才會到等待隊列的隊尾等待。非公平鎖的優勢就在于整體執行速度更快,吞吐量更大,但同時也可能產生線程饑餓問題,也就是說如果一直有線程插隊,那么在等待隊列中的線程可能長時間得不到運行。
#非公平鎖吞吐量為什么比公平鎖大?
- 公平鎖執行流程:獲取鎖時,先將線程自己添加到等待隊列的隊尾并休眠,當某線程用完鎖之后,會去喚醒等待隊列中隊首的線程嘗試去獲取鎖,鎖的使用順序也就是隊列中的先后順序,在整個過程中,線程會從運行狀態切換到休眠狀態,再從休眠狀態恢復成運行狀態,但線程每次休眠和恢復都需要從用戶態轉換成內核態,而這個狀態的轉換是比較慢的,所以公平鎖的執行速度會比較慢。
- 非公平鎖執行流程:當線程獲取鎖時,會先通過 CAS 嘗試獲取鎖,如果獲取成功就直接擁有鎖,如果獲取鎖失敗才會進入等待隊列,等待下次嘗試獲取鎖。這樣做的好處是,獲取鎖不用遵循先到先得的規則,從而避免了線程休眠和恢復的操作,這樣就加速了程序的執行效率。
#Synchronized是公平鎖嗎?
Synchronized不屬于公平鎖,ReentrantLock是公平鎖。
#ReentrantLock是怎么實現公平鎖的?
我們來看一下公平鎖與非公平鎖的加鎖方法的源碼。公平鎖的鎖獲取源碼如下:
protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (!hasQueuedPredecessors() && //這里判斷了 hasQueuedPredecessors()compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}} else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) {throw new Error("Maximum lock count exceeded");}setState(nextc);return true;}return false;
}
非公平鎖的鎖獲取源碼如下:
final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (compareAndSetState(0, acquires)) { //這里沒有判斷 hasQueuedPredecessors()setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}
通過對比,我們可以明顯的看出公平鎖與非公平鎖的 lock() 方法唯一的區別就在于公平鎖在獲取鎖時多了一個限制條件:hasQueuedPredecessors() 為 false,這個方法就是判斷在等待隊列中是否已經有線程在排隊了。
這也就是公平鎖和非公平鎖的核心區別,如果是公平鎖,那么一旦已經有線程在排隊了,當前線程就不再嘗試獲取鎖;對于非公平鎖而言,無論是否已經有線程在排隊,都會嘗試獲取一下鎖,獲取不到的話,再去排隊。這里有一個特例需要我們注意,針對 tryLock() 方法,它不遵守設定的公平原則。
例如,當有線程執行 tryLock() 方法的時候,一旦有線程釋放了鎖,那么這個正在 tryLock 的線程就能獲取到鎖,即使設置的是公平鎖模式,即使在它之前已經有其他正在等待隊列中等待的線程,簡單地說就是 tryLock 可以插隊。
看它的源碼就會發現:
public boolean tryLock() {return sync.nonfairTryAcquire(1);}
這里調用的就是 nonfairTryAcquire(),表明了是不公平的,和鎖本身是否是公平鎖無關。綜上所述,公平鎖就是會按照多個線程申請鎖的順序來獲取鎖,從而實現公平的特性。
非公平鎖加鎖時不考慮排隊等待情況,直接嘗試獲取鎖,所以存在后申請卻先獲得鎖的情況,但由此也提高了整體的效率。
#什么情況會產生死鎖問題?如何解決?
死鎖只有同時滿足以下四個條件才會發生:
- 互斥條件:互斥條件是指多個線程不能同時使用同一個資源。
- 持有并等待條件:持有并等待條件是指,當線程 A 已經持有了資源 1,又想申請資源 2,而資源 2 已經被線程 C 持有了,所以線程 A 就會處于等待狀態,但是線程 A 在等待資源 2 的同時并不會釋放自己已經持有的資源 1。
- 不可剝奪條件:不可剝奪條件是指,當線程已經持有了資源 ,在自己使用完之前不能被其他線程獲取,線程 B 如果也想使用此資源,則只能在線程 A 使用完并釋放后才能獲取。
- 環路等待條件:環路等待條件指的是,在死鎖發生的時候,兩個線程獲取資源的順序構成了環形鏈。
例如,線程 A 持有資源 R1 并試圖獲取資源 R2,而線程 B 持有資源 R2 并試圖獲取資源 R1,此時兩個線程相互等待對方釋放資源,從而導致死鎖。
public class DeadlockExample {private static final Object resource1 = new Object();private static final Object resource2 = new Object();public static void main(String[] args) {Thread threadA = new Thread(() -> {synchronized (resource1) {System.out.println("Thread A acquired resource1");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized (resource2) {System.out.println("Thread A acquired resource2");}}});Thread threadB = new Thread(() -> {synchronized (resource2) {System.out.println("Thread B acquired resource2");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized (resource1) {System.out.println("Thread B acquired resource1");}}});threadA.start();threadB.start();}
}
避免死鎖問題就只需要破環其中一個條件就可以,最常見的并且可行的就是使用資源有序分配法,來破環環路等待條件。
那什么是資源有序分配法呢?線程 A 和 線程 B 獲取資源的順序要一樣,當線程 A 是先嘗試獲取資源 A,然后嘗試獲取資源 B 的時候,線程 B 同樣也是先嘗試獲取資源 A,然后嘗試獲取資源 B。也就是說,線程 A 和 線程 B 總是以相同的順序申請自己想要的資源。