java 中鎖有哪些
1.公平和非公平鎖
- 是什麼
- **公平鎖:**是指多個線程按照申請的順序來獲取值
- **非公平鎖:**是值多個線程獲取值的順序並不是按照申請鎖的順序,有可能後申請的線程比先申請的線程優先獲取鎖,在高併發的情況下,可能會造成優先級翻轉或者飢餓現象
- 兩者區別
- **公平鎖:**在併發環境中,每一個線程在獲取鎖時會先查看此鎖維護的等待隊列,如果爲空,或者當前線程是等待隊列的第一個就佔有鎖,否者就會加入到等待隊列中,以後會按照 FIFO 的規則獲取鎖
- **非公平鎖:**一上來就嘗試佔有鎖,如果失敗在進行排隊
2.可重入鎖和不可重入鎖
-
是什麼
- **可重入鎖:**指的是同一個線程外層函數獲得鎖之後,內層仍然能獲取到該鎖,在同一個線程在外層方法獲取鎖的時候,在進入內層方法或會自動獲取該鎖
- 不可重入鎖: 所謂不可重入鎖,即若當前線程執行某個方法已經獲取了該鎖,那麼在方法中嘗試再次獲取鎖時,就會獲取不到被阻塞
-
代碼實現
-
可重入鎖
public class ReentrantLock { boolean isLocked = false; Thread lockedBy = null; int lockedCount = 0; public synchronized void lock() throws InterruptedException { Thread thread = Thread.currentThread(); while (isLocked && lockedBy != thread) { wait(); } isLocked = true; lockedCount++; lockedBy = thread; } public synchronized void unlock() { if (Thread.currentThread() == lockedBy) { lockedCount--; if (lockedCount == 0) { isLocked = false; notify(); } } } }
測試
public class Count { // NotReentrantLock lock = new NotReentrantLock(); ReentrantLock lock = new ReentrantLock(); public void print() throws InterruptedException{ lock.lock(); doAdd(); lock.unlock(); } private void doAdd() throws InterruptedException { lock.lock(); // do something System.out.println("ReentrantLock"); lock.unlock(); } public static void main(String[] args) throws InterruptedException { Count count = new Count(); count.print(); } }
發現可以輸出 ReentrantLock,我們設計兩個線程調用 print() 方法,第一個線程調用 print() 方法獲取鎖,進入 lock() 方法,由於初始 lockedBy 是 null,所以不會進入 while 而掛起當前線程,而是是增量 lockedCount 並記錄 lockBy 爲第一個線程。接着第一個線程進入 doAdd() 方法,由於同一進程,所以不會進入 while 而掛起,接着增量 lockedCount,當第二個線程嘗試lock,由於 isLocked=true,所以他不會獲取該鎖,直到第一個線程調用兩次 unlock() 將 lockCount 遞減爲0,纔將標記爲 isLocked 設置爲 false。
-
不可重入鎖
public class NotReentrantLock { private boolean isLocked = false; public synchronized void lock() throws InterruptedException { while (isLocked) { wait(); } isLocked = true; } public synchronized void unlock() { isLocked = false; notify(); } }
測試
public class Count { NotReentrantLock lock = new NotReentrantLock(); public void print() throws InterruptedException{ lock.lock(); doAdd(); lock.unlock(); } private void doAdd() throws InterruptedException { lock.lock(); // do something lock.unlock(); } public static void main(String[] args) throws InterruptedException { Count count = new Count(); count.print(); } }
當前線程執行print()方法首先獲取lock,接下來執行doAdd()方法就無法執行doAdd()中的邏輯,必須先釋放鎖。這個例子很好的說明了不可重入鎖。
-
-
synchronized 和 ReentrantLock 都是可重入鎖
-
synchronzied
public class SynchronziedDemo { private synchronized void print() { doAdd(); } private synchronized void doAdd() { System.out.println("doAdd..."); } public static void main(String[] args) { SynchronziedDemo synchronziedDemo = new SynchronziedDemo(); synchronziedDemo.print(); // doAdd... } }
上面可以說明 synchronized 是可重入鎖。
-
ReentrantLock
public class ReentrantLockDemo { private Lock lock = new ReentrantLock(); private void print() { lock.lock(); doAdd(); lock.unlock(); } private void doAdd() { lock.lock(); lock.lock(); System.out.println("doAdd..."); lock.unlock(); lock.unlock(); } public static void main(String[] args) { ReentrantLockDemo reentrantLockDemo = new ReentrantLockDemo(); reentrantLockDemo.print(); } }
上面例子可以說明 ReentrantLock 是可重入鎖,而且在 #doAdd 方法中加兩次鎖和解兩次鎖也可以。
-
3.自旋鎖
- 是指定嘗試獲取鎖的線程不會立即堵塞,而是採用循環的方式去嘗試獲取鎖,這樣的好處是減少線程上線文切換的消耗,缺點就是循環會消耗 CPU。
- 手動實現自旋鎖
public class SpinLock {
private AtomicReference<Thread> atomicReference = new AtomicReference<>();
private void lock () {
System.out.println(Thread.currentThread() + " coming...");
while (!atomicReference.compareAndSet(null, Thread.currentThread())) {
// loop
}
}
private void unlock() {
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread, null);
System.out.println(thread + " unlock...");
}
public static void main(String[] args) throws InterruptedException {
SpinLock spinLock = new SpinLock();
new Thread(() -> {
spinLock.lock();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hahaha");
spinLock.unlock();
}).start();
Thread.sleep(1);
new Thread(() -> {
spinLock.lock();
System.out.println("hehehe");
spinLock.unlock();
}).start();
}
}
輸出:
Thread[Thread-0,5,main] coming...
Thread[Thread-1,5,main] coming...
hahaha
Thread[Thread-0,5,main] unlock...
hehehe
Thread[Thread-1,5,main] unlock...
4.獨佔鎖(寫鎖)/共享鎖(讀鎖)
-
是什麼
- **獨佔鎖:**指該鎖一次只能被一個線程持有
- **共享鎖:**該鎖可以被多個線程持有
-
對於 ReentrantLock 和 synchronized 都是獨佔鎖;對與 ReentrantReadWriteLock 其讀鎖是共享鎖而寫鎖是獨佔鎖。讀鎖的共享可保證併發讀是非常高效的,讀寫、寫讀和寫寫的過程是互斥的。
-
讀寫鎖例子
-
public class MyCache { private volatile Map<String, Object> map = new HashMap<>(); private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); WriteLock writeLock = lock.writeLock(); ReadLock readLock = lock.readLock(); public void put(String key, Object value) { try { writeLock.lock(); System.out.println(Thread.currentThread().getName() + " 正在寫入..."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } map.put(key, value); System.out.println(Thread.currentThread().getName() + " 寫入完成,寫入結果是 " + value); } finally { writeLock.unlock(); } } public void get(String key) { try { readLock.lock(); System.out.println(Thread.currentThread().getName() + " 正在讀..."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } Object res = map.get(key); System.out.println(Thread.currentThread().getName() + " 讀取完成,讀取結果是 " + res); } finally { readLock.unlock(); } } }
測試
public class ReadWriteLockDemo { public static void main(String[] args) { MyCache cache = new MyCache(); for (int i = 0; i < 5; i++) { final int temp = i; new Thread(() -> { cache.put(temp + "", temp + ""); }).start(); } for (int i = 0; i < 5; i++) { final int temp = i; new Thread(() -> { cache.get(temp + ""); }).start(); } } } 輸出結果 Thread-0 正在寫入... Thread-0 寫入完成,寫入結果是 0 Thread-1 正在寫入... Thread-1 寫入完成,寫入結果是 1 Thread-2 正在寫入... Thread-2 寫入完成,寫入結果是 2 Thread-3 正在寫入... Thread-3 寫入完成,寫入結果是 3 Thread-4 正在寫入... Thread-4 寫入完成,寫入結果是 4 Thread-5 正在讀... Thread-7 正在讀... Thread-8 正在讀... Thread-6 正在讀... Thread-9 正在讀... Thread-5 讀取完成,讀取結果是 0 Thread-7 讀取完成,讀取結果是 2 Thread-8 讀取完成,讀取結果是 3 Thread-6 讀取完成,讀取結果是 1 Thread-9 讀取完成,讀取結果是 4
能保證讀寫、寫讀和寫寫的過程是互斥的時候是獨享的,讀讀的時候是共享的。