Synchronized 和 Lock
1. synchronized
1.1. synchronized 缺陷
1.1.1. 缺陷一
如果一個程式碼塊被synchronized修飾了,當一個執行緒獲取了對應的鎖並執行該程式碼塊時,其他執行緒便只能一直等待,等待獲取鎖的執行緒釋放鎖,而這裡獲取鎖的執行緒釋放鎖只會有兩種情況:
- 獲取鎖的執行緒執行完了,然後釋放鎖。
- 執行緒執行發生異常,自動釋放鎖。
根據上邊兩點,可以看出synchronized執行緒如果被阻塞了,會導致其他執行緒便只能乾巴巴地等待。
1.1.2. 缺陷二
當多個執行緒同時進行讀寫操作時,為了防止讀寫出現安全問題,通常會對讀寫操作都加上 synchronized 以保證執行緒安全。但是會產生以下兩種情況:
- 寫執行緒加 synchronized 是沒有問題的,這保證了每次只能一個執行緒去寫。
- 但是讀資料時,往往是需要支援多個執行緒同時讀的,一旦加了 synchronized 會導致每次讀資料時,只能有一條執行緒去讀資料。
根據上邊的情況,會對讀鎖產生疑問:為什麼讀執行緒要加鎖?讀執行緒又不修改資料,只需要加個 volatile 關鍵字保證可見性不行嗎?
至於讀執行緒要不要加鎖,可以看下邊的程式碼。
public class Test { volatile static int num = 0; static final int max = 2; public synchronized static void write() { num++; if (num > max) { num = 0; } } public static void read() { if (num > max) { System.err.println(String.format("資料異常,ThreadName=%s, num = %s", Thread.currentThread().getName(), num)); System.exit(0); } System.out.println(String.format("ThreadName=%s, num = %s", Thread.currentThread().getName(), num)); } public static void main(String[] args) { // 迴圈 3 次,建立 3個寫執行緒,3個讀執行緒 IntStream.range(0, 3).forEach(i -> { new Thread(() -> { while (true) { write(); } }, "write" + i).start(); new Thread(() -> { while (true) { read(); } }, "read" + i).start(); }); } }
上邊的結果看得出來,不加讀鎖,會導致讀到了正在修改中的資料,但是加了讀鎖 synchronized 又導致每次只能一個執行緒去讀,這明顯看得出 synchronized 滿足不了多執行緒的讀寫需求。
1.2. Synchronized 和 Lock 區別
- synchronized是java內建關鍵字,Lock是個java類;
- synchronized無法判斷是否獲取鎖的狀態,Lock可以判斷是否獲取到鎖;
- synchronized會自動釋放鎖,Lock需在finally中手工釋放鎖;
2. Lock 鎖
什麼是 Lock?
Lock 是一個介面主要方法有以下
public interface Lock { // 用來獲取鎖。如果鎖已被其他執行緒獲取,則進行等待。 void lock(); // 用來獲取鎖。但是可以被interrupt()方法中斷獲取鎖,直接丟擲異常。 void lockInterruptibly() throws InterruptedException; // 用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他執行緒獲取),則返回false boolean tryLock(); // 和tryLock()方法是類似的,只不過區別在於這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就返回false boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 釋放鎖 void unlock(); // Condition中的await()方法相當於Object的wait()方法,Condition中的signal()方法相當於Object的notify()方法 Condition newCondition(); }
lock() 和 unlock() 方法
lock()方法是平常使用得最多的一個方法,就是用來獲取鎖。如果鎖已被其他執行緒獲取,則進行等待。
採用Lock,必須主動去釋放鎖。一般來說,使用Lock必須在try{}catch{}中執行,並且將unlock放在finally中,以保證鎖一定被被釋放。
通常使用Lock是以下面這種形式去使用的:
Lock lock = new ReentrantLock();// ReentrantLock是可重入鎖
lock.lock();// 獲得鎖
try{
//處理任務
}finally{
lock.unlock(); //釋放鎖
}
tryLock() 方法
tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他執行緒獲取),則返回false,也就說這個方法無論如何都會立即返回。在拿不到鎖時不會一直在那等待。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是類似的,只不過區別在於這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就返回false。
一般情況下tryLock獲取鎖時是這樣使用的:
Lock lock = new ReentrantLock();// ReentrantLock是可重入鎖
if(lock.tryLock()) {
try{
//處理任務
}finally{
lock.unlock(); //釋放鎖
}
}else {
//如果不能獲取鎖,則直接做其他事情
}
lockInterruptibly() 方法
lockInterruptibly()方法比較特殊,當通過這個方法去獲取鎖時,如果執行緒正在等待獲取鎖,則呼叫interrupt()方法能夠中斷執行緒的等待狀態(中斷等待獲取鎖的狀態)。
由於lockInterruptibly()方法中申明瞭丟擲異常,所以lock.lockInterruptibly()必須放在try塊中或者在呼叫方法的中宣告丟擲InterruptedException。
因此lockInterruptibly()一般的使用形式如下:
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}
或者
public void method() {
try {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
} catch (InterruptedException e) {
//...
}
}
lockInterruptibly() 加鎖和 lock() 加鎖被 interrupt() 中斷對比
lock() 方法會忽略interrupt()方法的中斷請求,繼續等待獲取鎖直到成功,成功獲取鎖之後再丟擲異常。
lockInterruptibly() 和 lock() 不同,lockInterruptibly() 直接丟擲中斷異常立即響應中斷
public class Test {
static Lock lock = new ReentrantLock();
public static void write() {
try {
lock.lock(); // 使用 lock 加鎖
// lock.lockInterruptibly(); // 使用 lockInterruptibly 加鎖
try {
System.out.println(Thread.currentThread().getName() + ">>>得到鎖");
Thread.sleep(3000);
} catch (Exception e) {
System.out.println(Thread.currentThread().getName() + "===先得到鎖,再丟擲異常");
}finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + "<<<釋放鎖");
}
} catch (Exception e1) {
System.out.println(Thread.currentThread().getName() + "+++不等待獲取鎖了,直接丟擲異常");
}
}
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
write();
}, "t1");
Thread t2 = new Thread(() -> {
write();
}, "t2");
t1.start();
Thread.sleep(10);
t2.start();
t2.interrupt();// 中斷 t2 執行緒
}
}
使用 lock() 加鎖列印結果如下:
使用 lockInterruptibly() 加鎖列印結果如下:
3. ReadWriteLock 讀寫鎖
什麼是 ReadWriteLock?
ReadWriteLock 是讀寫鎖,也是一個介面,在它裡面只定義了兩個方法
public interface ReadWriteLock {
// 讀鎖
Lock readLock();
// 寫鎖
Lock writeLock();
}
readLock() 和 writeLock() 的使用方式
-
寫鎖:寫鎖只能有一個執行緒寫,當 write1 執行緒佔用寫鎖之後,其他寫執行緒 write* 以及讀執行緒 read* 都需要等待,只有當 write1 執行緒釋放鎖之後,其他的 write* 以及 read* 執行緒才能嘗試獲取鎖。
-
讀鎖:讀鎖可以多個執行緒同時讀,當 read1 執行緒佔用讀鎖之後,其他讀執行緒 read* 也能同時進入鎖中讀取資料。但是在 read1 執行緒釋放鎖之前,所有寫執行緒 write* 都需要等待。
上邊已經說過 synchronized 滿足不了多執行緒的讀寫需求。這次可以使用 ReadWriteLock 來解決多執行緒的讀寫問題。
由下邊的程式和結果看得出來,加讀寫鎖之後,多執行緒讀寫資料不會存在任何安全問題。
public class Test {
static volatile int num = 0;
static final int max = 2;
static ReadWriteLock rwl = new ReentrantReadWriteLock(); // 可重入讀寫鎖
public static void write() {
rwl.writeLock().lock();// 寫鎖,每次只能一個執行緒寫
try {
num++;
if (num > max) {
num = 0;
}
} finally {
rwl.writeLock().unlock();
}
}
public static void read() {
rwl.readLock().lock();// 讀鎖,可以多個執行緒同時讀
try {
if (num > max) {
System.err.println(String.format("資料異常,ThreadName=%s, num = %s", Thread.currentThread().getName(), num));
System.exit(0);
}
System.out.println(String.format("ThreadName=%s, num = %s", Thread.currentThread().getName(), num));
} finally {
rwl.readLock().unlock();
}
}
public static void main(String[] args) {
// 迴圈 3 次,建立 3個寫執行緒,3個讀執行緒
IntStream.range(0, 3).forEach(i -> {
new Thread(() -> {
while (true) {
write();
}
}, "write" + i).start();
new Thread(() -> {
while (true) {
read();
}
}, "read" + i).start();
});
}
}
4. 鎖相關概念
4.1. 可重入鎖
像synchronized、ReentrantLock、ReentrantReadWriteLock都是可重入鎖。
舉個簡單的例子,當一個執行緒執行到某個synchronized方法時,比如說method1,而在method1中會呼叫另外一個synchronized方法method2,此時執行緒不必重新去申請鎖,而是可以直接執行方法method2。
class MyClass {
public synchronized void method1() {
method2();
}
public synchronized void method2() {
}
}
上述程式碼中的兩個方法method1和method2都用synchronized修飾了,假如某一時刻,執行緒A執行到了method1,此時執行緒A獲取了這個物件的鎖,而由於method2也是synchronized方法,假如synchronized不具備可重入性,此時執行緒A需要重新申請鎖。但是這就會造成一個問題,因為執行緒A已經持有了該物件的鎖,而又在申請獲取該物件的鎖,這樣就會執行緒A一直等待永遠不會獲取到的鎖。
4.2. 可中斷鎖
顧名思義,就是可以響應中斷的鎖。
在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。
如果執行緒A獲取了鎖,執行緒B正在等待獲取該鎖,可能由於等待時間過長,執行緒B不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的執行緒中中斷它,這種就是可中斷鎖。在前面演示lockInterruptibly()的用法時已經體現了Lock的可中斷性。
4.3. 公平/非公平鎖
比如同時有多個執行緒在等待一個鎖,當這個鎖被釋放時,等待時間最久的執行緒(最先請求的執行緒)會獲得該所,這種就是公平鎖。
- synchronized:非公平鎖
- ReentrantLock:可以非公平鎖,也可以公平鎖
- ReentrantReadWriteLock:可以非公平鎖,也可以公平鎖
ReentrantLock 和 ReentrantReadWriteLock 怎麼實現公平鎖?
ReentrantLock 和 ReentrantReadWriteLock 要實現公平鎖,可以看它們的原始碼,在建立物件時可以指定使用公平鎖還是非公平鎖
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
從上邊的構造方法可以看出,建立公平鎖可以使用以下方式
ReentrantLock lock = new ReentrantLock(true);
另外在ReentrantLock類中定義了很多方法,比如:
isFair() //判斷鎖是否是公平鎖
isLocked() //判斷鎖是否被任何執行緒獲取了
isHeldByCurrentThread() //判斷鎖是否被當前執行緒獲取了
hasQueuedThreads() //判斷是否有執行緒在等待該鎖
在ReentrantReadWriteLock中也有類似的方法,同樣也可以設定為公平鎖和非公平鎖。不過要注意,ReentrantReadWriteLock並未實現Lock介面,它實現的是ReadWriteLock介面。