Java 的鎖
公平鎖和非公平鎖
概念
公平鎖
是指多個線程按照申請鎖的順序來獲取鎖,類似於排隊買飯,先來後到,先來先服務,就是公平的,也就是隊列
非公平鎖
是指多個線程獲取鎖的順序,並不是按照申請鎖的順序,有可能申請的線程比先申請的線程優先獲取鎖,在高併發環境下,有可能造成優先級翻轉,或者飢餓的線程(也就是某個線程一直得不到鎖)
如何創建
併發包中ReentrantLock的創建可以指定析構函數的boolean類型來得到公平鎖或者非公平鎖,默認是非公平鎖
/**
* 創建一個可重入鎖,true 表示公平鎖,false 表示非公平鎖。默認非公平鎖
*/
Lock lock = new ReentrantLock(true);
兩者區別
公平鎖:就是很公平,在併發環境中,每個線程在獲取鎖時會先查看此鎖維護的等待隊列,如果爲空,或者當前線程是等待隊列中的第一個,就佔用鎖,否者就會加入到等待隊列中,以後安裝FIFO的規則從隊列中取到自己
非公平鎖: 非公平鎖比較粗魯,上來就直接嘗試佔有鎖,如果嘗試失敗,就再採用類似公平鎖那種方式。
題外話
Java ReenttrantLock通過構造函數指定該鎖是否公平,默認是非公平鎖,因爲非公平鎖的優點在於吞吐量比公平鎖大,對於synchronized而言,也是一種非公平鎖.
可重入鎖和遞歸鎖ReentrantLock
概念
可重入鎖就是遞歸鎖
指的是同一線程外層函數獲得鎖之後,內層遞歸函數仍然能獲取到該鎖的代碼,在同一線程在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖
也就是說:線程可以進入任何一個它已經擁有的鎖所同步的代碼塊
ReentrantLock / Synchronized 就是一個典型的可重入鎖.
代碼
可重入鎖就是,在一個method1方法中加入一把鎖,方法2也加鎖了,那麼他們擁有的是同一把鎖.
public synchronized void method1() {
method2();
}
public synchronized void method2() {
}
也就是說我們只需要進入method1後,那麼它也能直接進入method2方法,因爲他們所擁有的鎖,是同一把。
作用
可重入鎖的最大作用就是避免死鎖
可重入鎖驗證
證明Synchronized
/**
* 可重入鎖(也叫遞歸鎖)
* 指的是同一線程外層函數獲得鎖之後,內層遞歸函數仍然能獲取到該鎖的代碼,在同一線程在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖
*
* 也就是說:`線程可以進入任何一個它已經擁有的鎖所同步的代碼塊`
* @author: 輕狂書生FS
* @create: 2020-04-15-12:12
*/
/**
* 資源類
*/
class Phone {
/**
* 發送短信
* @throws Exception
*/
public synchronized void sendSMS() throws Exception{
System.out.println(Thread.currentThread().getName() + "\t invoked sendSMS()");
// 在同步方法中,調用另外一個同步方法
sendEmail();
}
/**
* 發郵件
* @throws Exception
*/
public synchronized void sendEmail() throws Exception{
System.out.println(Thread.currentThread().getId() + "\t invoked sendEmail()");
}
}
public class ReenterLockDemo {
public static void main(String[] args) {
Phone phone = new Phone();
// 兩個線程操作資源列
new Thread(() -> {
try {
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
}, "t1").start();
new Thread(() -> {
try {
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
}, "t2").start();
}
}
在這裏,我們編寫了一個資源類phone,擁有兩個加了synchronized的同步方法,分別是sendSMS 和 sendEmail,我們在sendSMS方法中,調用sendEmail。最後在主線程同時開啓了兩個線程進行測試,最後得到的結果爲:
t1 invoked sendSMS()
t1 invoked sendEmail()
t2 invoked sendSMS()
t2 invoked sendEmail()
這就說明當 t1 線程進入sendSMS的時候,擁有了一把鎖,同時t2線程無法進入,直到t1線程拿着鎖,執行了sendEmail 方法後,才釋放鎖,這樣t2才能夠進入
t1 invoked sendSMS() t1線程在外層方法獲取鎖的時候
t1 invoked sendEmail() t1在進入內層方法會自動獲取鎖
t2 invoked sendSMS() t2線程在外層方法獲取鎖的時候
t2 invoked sendEmail() t2在進入內層方法會自動獲取鎖
證明ReentrantLock
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 資源類
*/
class Phone implements Runnable{
Lock lock = new ReentrantLock();
/**
* set進去的時候,就加鎖,調用set方法的時候,能否訪問另外一個加鎖的set方法
*/
public void getLock() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t get Lock");
setLock();
} finally {
lock.unlock();
}
}
public void setLock() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t set Lock");
} finally {
lock.unlock();
}
}
@Override
public void run() {
getLock();
}
}
public class ReenterLockDemo {
public static void main(String[] args) {
Phone phone = new Phone();
/**
* 因爲Phone實現了Runnable接口
*/
Thread t3 = new Thread(phone, "t3");
Thread t4 = new Thread(phone, "t4");
t3.start();
t4.start();
}
}
現在我們使用ReentrantLock進行驗證,首先資源類實現了Runnable接口,重寫Run方法,裏面調用get方法,get方法在進入的時候,就加了鎖
public void getLock() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t get Lock");
setLock();
} finally {
lock.unlock();
}
}
然後在方法裏面,又調用另外一個加了鎖的setLock方法
public void setLock() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t set Lock");
} finally {
lock.unlock();
}
}
最後輸出結果我們能發現,結果和加synchronized方法是一致的,都是在外層的方法獲取鎖之後,線程能夠直接進入裏層.
t3 get Lock
t3 set Lock
t4 get Lock
t4 set Lock
當我們在getLock方法加兩把鎖會是什麼情況呢? (阿里面試)
public void getLock() {
lock.lock();
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t get Lock");
setLock();
} finally {
lock.unlock();
lock.unlock();
}
}
得到結果
t3 get Lock
t3 set Lock
也就是說程序直接卡死,線程不能出來,也就說明我們申請幾把鎖,最後需要解除幾把鎖
當我們只加一把鎖,但是用兩把鎖來解鎖的時候,又會出現什麼情況呢?
public void getLock() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t get Lock");
setLock();
} finally {
lock.unlock();
lock.unlock();
}
}
這個時候,運行程序會直接報錯
t3 get Lock
t3 set Lock
t4 get Lock
t4 set Lock
Exception in thread "t3" Exception in thread "t4" java.lang.IllegalMonitorStateException
at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
at com.moxi.interview.study.thread.Phone.getLock(ReenterLockDemo.java:52)
at com.moxi.interview.study.thread.Phone.run(ReenterLockDemo.java:67)
at java.lang.Thread.run(Thread.java:745)
java.lang.IllegalMonitorStateException
at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
at com.moxi.interview.study.thread.Phone.getLock(ReenterLockDemo.java:52)
at com.moxi.interview.study.thread.Phone.run(ReenterLockDemo.java:67)
at java.lang.Thread.run(Thread.java:745)
自旋鎖
概念
自旋鎖:spinlock,是指嘗試獲取鎖的線程不會立即阻塞,而是採用循環的方式去嘗試獲取鎖,這樣的好處是減少線程上下文切換的消耗,缺點是循環會消耗CPU
原來提到的比較並交換,底層使用的就是自旋,自旋就是多次嘗試,多次訪問,不會阻塞的狀態就是自旋。
優缺點
優點:循環比較獲取直到成功爲止,沒有類似於wait的阻塞
缺點:當不斷自旋的線程越來越多的時候,會因爲執行while循環不斷的消耗CPU資源
手寫自旋鎖
通過CAS操作完成自旋鎖,A線程先進來調用myLock方法自己持有鎖5秒,B隨後進來發現當前有線程持有鎖,不是null,所以只能通過自旋等待,直到A釋放鎖後B隨後搶到
/**
* 手寫一個自旋鎖
*
* 循環比較獲取直到成功爲止,沒有類似於wait的阻塞
*
* 通過CAS操作完成自旋鎖,A線程先進來調用myLock方法自己持有鎖5秒,B隨後進來發現當前有線程持有鎖,不是null,所以只能通過自旋等待,直到A釋放鎖後B隨後搶到
* @author: 輕狂書生FS
* @create: 2020-04-15-15:46
*/
public class SpinLockDemo {
// 現在的泛型裝的是Thread,原子引用線程
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void myLock() {
// 獲取當前進來的線程
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "\t come in ");
// 開始自旋,期望值是null,更新值是當前線程,如果是null,則更新爲當前線程,否者自旋
while(!atomicReference.compareAndSet(null, thread)) {
}
}
/**
* 解鎖
*/
public void myUnLock() {
// 獲取當前進來的線程
Thread thread = Thread.currentThread();
// 自己用完了後,把atomicReference變成null
atomicReference.compareAndSet(thread, null);
System.out.println(Thread.currentThread().getName() + "\t invoked myUnlock()");
}
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
// 啓動t1線程,開始操作
new Thread(() -> {
// 開始佔有鎖
spinLockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 開始釋放鎖
spinLockDemo.myUnLock();
}, "t1").start();
// 讓main線程暫停1秒,使得t1線程,先執行
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 1秒後,啓動t2線程,開始佔用這個鎖
new Thread(() -> {
// 開始佔有鎖
spinLockDemo.myLock();
// 開始釋放鎖
spinLockDemo.myUnLock();
}, "t2").start();
}
}
最後輸出結果
t1 come in
.....五秒後.....
t1 invoked myUnlock()
t2 come in
t2 invoked myUnlock()
首先輸出的是 t1 come in
然後1秒後,t2線程啓動,發現鎖被t1佔有,所有不斷的執行 compareAndSet方法,來進行比較,直到t1釋放鎖後,也就是5秒後,t2成功獲取到鎖,然後釋放。
獨佔鎖(寫鎖) / 共享鎖(讀鎖) / 互斥鎖
概念
獨佔鎖:指該鎖一次只能被一個線程所持有。對ReentrantLock和Synchronized而言都是獨佔鎖
共享鎖:指該鎖可以被多個線程鎖持有
對ReentrantReadWriteLock其讀鎖是共享,其寫鎖是獨佔
寫的時候只能一個人寫,但是讀的時候,可以多個人同時讀
爲什麼會有寫鎖和讀鎖
原來我們使用ReentrantLock創建鎖的時候,是獨佔鎖,也就是說一次只能一個線程訪問,但是有一個讀寫分離場景,讀的時候想同時進行,因此原來獨佔鎖的併發性就沒這麼好了,因爲讀鎖並不會造成數據不一致的問題,因此可以多個人共享讀
多個線程 同時讀一個資源類沒有任何問題,所以爲了滿足併發量,讀取共享資源應該可以同時進行,
但是如果一個線程想去寫共享資源,就不應該再有其它線程可以對該資源進行讀或寫
- 讀-讀:能共存
- 讀-寫:不能共存
- 寫-寫:不能共存
代碼實現
實現一個讀寫緩存的操作,假設開始沒有加鎖的時候,會出現什麼情況
/**
* 讀寫鎖
* 多個線程 同時讀一個資源類沒有任何問題,所以爲了滿足併發量,讀取共享資源應該可以同時進行
* 但是,如果一個線程想去寫共享資源,就不應該再有其它線程可以對該資源進行讀或寫
*
* @author: 輕狂書生FS
* @create: 2020-04-15-16:59
*/
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
/**
* 資源類
*/
class MyCache {
private volatile Map<String, Object> map = new HashMap<>();
// private Lock lock = null;
/**
* 定義寫操作
* 滿足:原子 + 獨佔
* @param key
* @param value
*/
public void put(String key, Object value) {
System.out.println(Thread.currentThread().getName() + "\t 正在寫入:" + key);
try {
// 模擬網絡擁堵,延遲0.3秒
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t 寫入完成");
}
public void get(String key) {
System.out.println(Thread.currentThread().getName() + "\t 正在讀取:");
try {
// 模擬網絡擁堵,延遲0.3秒
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object value = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t 讀取完成:" + value);
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
// 線程操作資源類,5個線程寫
for (int i = 0; i < 5; i++) {
// lambda表達式內部必須是final
final int tempInt = i;
new Thread(() -> {
myCache.put(tempInt + "", tempInt + "");
}, String.valueOf(i)).start();
}
// 線程操作資源類, 5個線程讀
for (int i = 0; i < 5; i++) {
// lambda表達式內部必須是final
final int tempInt = i;
new Thread(() -> {
myCache.get(tempInt + "");
}, String.valueOf(i)).start();
}
}
}
我們分別創建5個線程寫入緩存
// 線程操作資源類,5個線程寫
for (int i = 0; i < 5; i++) {
// lambda表達式內部必須是final
final int tempInt = i;
new Thread(() -> {
myCache.put(tempInt + "", tempInt + "");
}, String.valueOf(i)).start();
}
5個線程讀取緩存,
// 線程操作資源類, 5個線程讀
for (int i = 0; i < 5; i++) {
// lambda表達式內部必須是final
final int tempInt = i;
new Thread(() -> {
myCache.get(tempInt + "");
}, String.valueOf(i)).start();
}
最後運行結果:
0 正在寫入:0
4 正在寫入:4
3 正在寫入:3
1 正在寫入:1
2 正在寫入:2
0 正在讀取:
1 正在讀取:
2 正在讀取:
3 正在讀取:
4 正在讀取:
2 寫入完成
4 寫入完成
4 讀取完成:null
0 寫入完成
3 讀取完成:null
0 讀取完成:null
1 寫入完成
3 寫入完成
1 讀取完成:null
2 讀取完成:null
我們可以看到,在寫入的時候,寫操作都沒其它線程打斷了,這就造成了,還沒寫完,其它線程又開始寫,這樣就造成數據不一致
解決方法
上面的代碼是沒有加鎖的,這樣就會造成線程在進行寫入操作的時候,被其它線程頻繁打斷,從而不具備原子性,這個時候,我們就需要用到讀寫鎖來解決了
/**
* 創建一個讀寫鎖
* 它是一個讀寫融爲一體的鎖,在使用的時候,需要轉換
*/
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
當我們在進行寫操作的時候,就需要轉換成寫鎖
// 創建一個寫鎖
rwLock.writeLock().lock();
// 寫鎖 釋放
rwLock.writeLock().unlock();
當們在進行讀操作的時候,在轉換成讀鎖
// 創建一個讀鎖
rwLock.readLock().lock();
// 讀鎖 釋放
rwLock.readLock().unlock();
這裏的讀鎖和寫鎖的區別在於,寫鎖一次只能一個線程進入,執行寫操作,而讀鎖是多個線程能夠同時進入,進行讀取的操作
完整代碼:
/**
* 讀寫鎖
* 多個線程 同時讀一個資源類沒有任何問題,所以爲了滿足併發量,讀取共享資源應該可以同時進行
* 但是,如果一個線程想去寫共享資源,就不應該再有其它線程可以對該資源進行讀或寫
*
* @author: 輕狂書生FS
* @create: 2020-03-15-16:59
*/
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 資源類
*/
class MyCache {
/**
* 緩存中的東西,必須保持可見性,因此使用volatile修飾
*/
private volatile Map<String, Object> map = new HashMap<>();
/**
* 創建一個讀寫鎖
* 它是一個讀寫融爲一體的鎖,在使用的時候,需要轉換
*/
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
/**
* 定義寫操作
* 滿足:原子 + 獨佔
* @param key
* @param value
*/
public void put(String key, Object value) {
// 創建一個寫鎖
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 正在寫入:" + key);
try {
// 模擬網絡擁堵,延遲0.3秒
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t 寫入完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 寫鎖 釋放
rwLock.writeLock().unlock();
}
}
/**
* 獲取
* @param key
*/
public void get(String key) {
// 讀鎖
rwLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 正在讀取:");
try {
// 模擬網絡擁堵,延遲0.3秒
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object value = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t 讀取完成:" + value);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 讀鎖釋放
rwLock.readLock().unlock();
}
}
/**
* 清空緩存
*/
public void clean() {
map.clear();
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
// 線程操作資源類,5個線程寫
for (int i = 1; i <= 5; i++) {
// lambda表達式內部必須是final
final int tempInt = i;
new Thread(() -> {
myCache.put(tempInt + "", tempInt + "");
}, String.valueOf(i)).start();
}
// 線程操作資源類, 5個線程讀
for (int i = 1; i <= 5; i++) {
// lambda表達式內部必須是final
final int tempInt = i;
new Thread(() -> {
myCache.get(tempInt + "");
}, String.valueOf(i)).start();
}
}
}
運行結果:
1 正在寫入:1
1 寫入完成
2 正在寫入:2
2 寫入完成
3 正在寫入:3
3 寫入完成
4 正在寫入:4
4 寫入完成
5 正在寫入:5
5 寫入完成
2 正在讀取:
3 正在讀取:
1 正在讀取:
4 正在讀取:
5 正在讀取:
2 讀取完成:2
1 讀取完成:1
4 讀取完成:4
3 讀取完成:3
5 讀取完成:5
從運行結果我們可以看出,寫入操作是一個一個線程進行執行的,並且中間不會被打斷,而讀操作的時候,是同時5個線程進入,然後併發讀取操作。