多執行緒中的各種鎖
1. 公平鎖、非公平鎖
1.1 概念:
公平鎖就是先來後到、非公平鎖就是允許加塞 Lock lock = new ReentrantLock(Boolean fair);
預設非公平
- 公平鎖是指多個執行緒按照申請鎖的順序來獲取鎖,類似排隊打飯。
- 非公平鎖是指多個執行緒獲取鎖的順序並不是按照申請鎖的順序,有可能後申請的執行緒優先獲取鎖,在高併發的情況下,有可能會造成優先順序反轉或者節現象。
1.2 兩者區別?
-
公平鎖:
Threads acquire a fair lock in the order in which they requested it
公平鎖,就是很公平,在併發環境中,每個執行緒在獲取鎖時,會先檢視此鎖維護的等待佇列,如果為空,或者當前執行緒就是等待佇列的第一個,就佔有鎖,否則就會加入到等待佇列中,以後會按照FIFO的規則從佇列中取到自己
-
非公平鎖:
a nonfair lock permits barging: threads requesting a lock can jump ahead of the queue of waiting threads if the lock happens to be available when it is requested
非公平鎖比較粗魯,上來就直接嘗試佔有額,如果嘗試失敗,就再採用類似公平鎖那種方式。
1.3 如何體現公平非公平?
- 對Java ReentrantLock而言,通過建構函式指定該鎖是否公平,預設是非公平鎖,非公平鎖的優點在於吞吐量比公平鎖大
- 對Synchronized而言,是一種非公平鎖
其中ReentrantLock和Synchronized預設都是非公平鎖,預設都是可重入鎖
2. 可重入鎖(遞迴鎖)
前文提到ReentrantLock和Synchronized預設都是非公平鎖,預設都是可重入鎖,那麼什麼是可重入鎖?
2.1 概念
指的時同一執行緒外層函式獲得鎖之後,內層遞迴函式仍然能獲取該鎖的程式碼,在同一個執行緒在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖,也就是說,執行緒可以進入任何一個它已經擁有的鎖所同步著的程式碼塊
2.2 為什麼要用到可重入鎖?
- 可重入鎖最大的作用是避免死鎖
- ReentrantLock/Synchronized 就是一個典型的可重入鎖
2.3 程式碼驗證可重入鎖?
首先我們先驗證ReentrantLock
package com.yuxue.juc.lockDemo;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 嘗試驗證ReentrantLock鎖的可重入性的Demo
* */
public class ReentrantLockDemo {
public static void main(String[] args) {
mobile mobile = new mobile();
new Thread(mobile,"t1").start();
new Thread(mobile,"t2").start();
}
}
/**
* 輔助類mobile,首先繼承了Runnable介面,可以重寫run方法
* 內部主要有兩個方法
* run方法首先呼叫第一個方法
* */
class mobile implements Runnable {
Lock lock = new ReentrantLock();
//run方法首先呼叫第一個方法
@Override
public void run() {
testMethod01();
}
//第一個方法,目的是首先讓執行緒進入方法1
public void testMethod01() {
//加鎖
lock.lock();
try {
//驗證執行緒進入方法1
System.out.println(Thread.currentThread().getName() + "\t" + "get in the method1");
//休眠
Thread.sleep(2000);
//進入方法2
testMethod02();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//第二個方法。目的是驗證ReentrantLock是否是可重入鎖
public void testMethod02() {
lock.lock();
try {
System.out.println("==========" + Thread.currentThread().getName() + "\t" + "leave the method1 get in the method2");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
因為同一個執行緒在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖,也就是說,執行緒可以進入任何一個它已經擁有的鎖所同步著的程式碼塊
之後觀察輸出結果為:
t1 get in the method1
==========t1 leave the method1 get in the method2
t2 get in the method1
==========t2 leave the method1 get in the method2
意味著執行緒t1進入方法1之後,再進入方法2,也就是說進入內層方法自動獲取鎖,之後釋放方法2的那把鎖,再釋放方法1的那把鎖,這之後執行緒t2才能獲取到方法1的鎖,才可以進入方法1
同樣地,如果我們在方法1中再加一把鎖,不給其解鎖,也就是
那麼結果會是怎麼呢?我們執行程式碼可以得到
我們發現執行緒是停不下來的,執行緒t1進入方法1加了兩把鎖,之後進入t2,但是退出t1的方法過程中沒有解鎖,這就導致了t2執行緒無法拿到鎖,也就驗證了鎖重入的問題
那麼為了驗證是同一把鎖,我們在方法1對其加鎖兩次,方法2對其解鎖兩次可以嗎?這鎖是相同的嗎?也就意味著:
我們再次執行,發現結果為:
t1 get in the method1
==========t1 leave the method1 get in the method2
t2 get in the method1
==========t2 leave the method1 get in the method2
也就側面驗證了加鎖的是同一把鎖,更驗證了我們的鎖重入問題
那麼對於synchronized鎖呢?程式碼以及結果如下所示:
package com.yuxue.juc.lockDemo;
public class SynchronizedDemo {
public static void main(String[] args) throws InterruptedException {
phone phone = new phone();
new Thread(()->{
phone.phoneTest01();
},"t1").start();
Thread.sleep(1000);
new Thread(()->{
phone.phoneTest01();
},"t2").start();
}
}
class phone{
public synchronized void phoneTest01(){
System.out.println(Thread.currentThread().getName()+"\t invoked phoneTest01()");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
phoneTest02();
}
public synchronized void phoneTest02() {
System.out.println(Thread.currentThread().getName()+"\t -----invoked phoneTest02()");
}
}
結果為:
t1 invoked phoneTest01()
t1 -----invoked phoneTest02()
t2 invoked phoneTest01()
t2 -----invoked phoneTest02()
上述兩個實驗可以驗證我們的ReentrantLock以及synchronized都是可重入鎖!
3. 自旋鎖
3.1 自旋鎖概念
是指嘗試獲取鎖的執行緒不會立即阻塞,而是採用迴圈的方式去嘗試獲取鎖,這樣的好處是減少執行緒上下文切換的消耗,缺點是迴圈會消耗CPU
就是我們CAS一文當中提到的這段程式碼:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
其中while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4))
這行程式碼更體現出了自旋鎖的核心,也就是當我嘗試去拿鎖的時候,一直迴圈,直到拿到鎖為止
3.2 手寫一個自旋鎖試試?
下面的
AtomicReference
可以去看CAS那篇有詳細講解
package com.yuxue.juc.lockDemo;
import java.util.concurrent.atomic.AtomicReference;
/**
* 實現自旋鎖
* 自旋鎖好處,迴圈比較獲取直到成功為止,沒有類似wait的阻塞
*
* 通過CAS操作完成自旋鎖,t1執行緒先進來呼叫mylock方法自己持有鎖2秒鐘,
* t2隨後進來發現當前有執行緒持有鎖,不是null,所以只能通過自旋等待,直到t1釋放鎖後t2隨後搶到
*/
public class SpinLockDemo {
public static void main(String[] args) {
//資源類
SpinLock spinLock = new SpinLock();
//t1執行緒
new Thread(()->{
//加鎖
spinLock.myLock();
try {
//休眠
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//解鎖
spinLock.myUnlock();
},"t1").start();
//這裡主執行緒休眠,為了讓t1首先得到並加鎖
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//t2執行緒
new Thread(()->{
spinLock.myLock();
try {
//這裡休眠時間較長是為了讓輸出結果更加視覺化
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLock.myUnlock();
},"t2").start();
}
}
class SpinLock {
//構造原子引用類
AtomicReference<Thread> atomicReference = new AtomicReference<>();
//自己的加鎖方法
public void myLock() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "\t" + "come in myLock");
//自旋核心程式碼!!當期望值是null並且主記憶體值也為null,就將其設定為自己的thread,否則就死迴圈,也就是一直自旋
while (!atomicReference.compareAndSet(null, thread)) {
}
}
public void myUnlock() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "\t" + "===== come in myUnlock");
//解鎖,用完之後,當期望值是自己的thread主實體記憶體的值也是自己的,也就是被自己的執行緒佔用
//用完之後解鎖,將主實體記憶體中Thread的地方設定為空,供其他執行緒使用
atomicReference.compareAndSet(thread, null);
}
}
結果為:
//t1以及t2同時進入myLock方法,爭奪鎖使用權
t1 come in myLock
t2 come in myLock
//t1使用完首先釋放鎖
t1 ===== come in myUnlock
//t2使用完釋放鎖,但是在5秒之後,因為在程式中我們讓其休眠了5s
t2 ===== come in myUnlock
4. 讀寫鎖
分為:獨佔鎖(寫鎖)/共享鎖(讀鎖)/互斥鎖
4.1 概念
-
獨佔鎖:指該鎖一次只能被一個執行緒所持有,對ReentrantLock和Synchronized而言都是獨佔鎖
-
共享鎖:只該鎖可被多個執行緒所持有
ReentrantReadWriteLock
其讀鎖是共享鎖,寫鎖是獨佔鎖 -
互斥鎖:讀鎖的共享鎖可以保證併發讀是非常高效的,讀寫、寫讀、寫寫的過程是互斥的
4.2 程式碼
首先是沒有讀寫鎖的程式碼,定義了一個資源類,裡面底層資料結構為HashMap,之後多個執行緒對其寫以及讀操作
package com.yuxue.juc.lockDemo;
import java.util.HashMap;
import java.util.Map;
class MyCache {
//定義快取當中的資料結構為Map型,鍵為String,值為Object型別
private volatile Map<String, Object> map = new HashMap<>();
//向Map中新增元素的方法
public void setMap(String key, Object value) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "\t" + " put value:" + value);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
//底層的map直接put
map.put(key, value);
System.out.println(thread.getName() + "\t" + "put value successful");
}
//向Map中取出元素
public void getMap(String key) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "\t" + "get the value");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
//底層的map直接get,並且返回值為Object型別
Object retValue = map.get(key);
System.out.println(thread.getName() + "\t" + "get the value successful, is " + retValue);
}
}
/**
* 多個執行緒同時讀一個資源類沒有任何問題,所以為了滿足併發量,讀取共享資源應該可以同時進行。 * 但是
* 如果有一個執行緒想取寫共享資源來,就不應該允許其他執行緒可以對資源進行讀或寫
* 總結
* 讀讀能共存
* 讀寫不能共存
* 寫寫不能共存
*/
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(() -> {
//5個執行緒分別填值
myCache.setMap(tempInt + "", tempInt + "");
}, "thread-" + i).start();
}
//建立5個執行緒
for (int i = 0; i < 5; i++) {
final int tempInt = i;
new Thread(() -> {
//5個執行緒取值
myCache.getMap(tempInt + "");
}, "thread-" + i).start();
}
}
}
結果為:
thread-0 put value:0
thread-1 put value:1
thread-2 put value:2
thread-3 put value:3
thread-4 put value:4
thread-0 get the value
thread-1 get the value
thread-2 get the value
thread-3 get the value
thread-4 get the value
thread-0 put value successful
thread-1 put value successful
thread-2 put value successful
thread-3 put value successful
thread-4 put value successful
...
上述執行結果看似沒有問題,但是違背了寫鎖最核心的本質,也就是如果有一個執行緒想取寫共享資源來,就不應該允許其他執行緒可以對資源進行讀或寫
所以出現問題,此時就需要用到我們的讀寫鎖,我們對我們自己的myLock()
以及myUnlock()
方法進行修改為使用讀寫鎖的版本:
class MyCache {
//定義快取當中的資料結構為Map型,鍵為String,值為Object型別
private volatile Map<String, Object> map = new HashMap<>();
//讀寫鎖
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
/**
* 寫操作:原子+獨佔
* 整個過程必須是一個完整的統一體,中間不許被分割,不許被打斷 *
* @param key
* @param value
* */
//向Map中新增元素的方法
public void setMap(String key, Object value) {
try {
readWriteLock.writeLock().lock();
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "\t" + " put value:" + value);
Thread.sleep(300);
//底層的map直接put
map.put(key, value);
System.out.println(thread.getName() + "\t" + "put value successful");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
//向Map中取出元素
public void getMap(String key) {
try {
readWriteLock.readLock().lock();
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "\t" + "get the value");
Thread.sleep(300);
//底層的map直接get,並且返回值為Object型別
Object retValue = map.get(key);
System.out.println(thread.getName() + "\t" + "get the value successful, is " + retValue);
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
}
之後執行結果變成:
thread-1 put value:1
thread-1 put value successful
thread-0 put value:0
thread-0 put value successful
thread-2 put value:2
thread-2 put value successful
thread-3 put value:3
thread-3 put value successful
thread-4 put value:4
thread-4 put value successful
thread-0 get the value
thread-1 get the value
thread-2 get the value
thread-4 get the value
thread-3 get the value
thread-2 get the value successful, is 2
thread-3 get the value successful, is 3
thread-0 get the value successful, is 0
thread-4 get the value successful, is 4
thread-1 get the value successful, is 1
也就對應上了寫鎖獨佔,必須當每一個寫鎖對應寫操作完成之後,才可以進行下一次寫操作,但是對於讀操作,就可以多個執行緒共享,一起去快取中讀取資料