一、公平鎖&非公平鎖
是什麼
-
公平鎖:執行緒按照申請鎖的順序來獲取鎖;在併發環境中,每個執行緒都會被加到等待佇列中,按照 FIFO 的順序獲取鎖。
-
非公平鎖:執行緒不按照申請鎖的順序來獲取鎖;一上來就嘗試佔有鎖,如果佔有失敗,則按照公平鎖的方式等待。
通俗來講,公平鎖就相當於現實中的排隊,先來後到;非公平鎖就是無秩序,誰搶到是誰的;
優缺點
公平鎖
- 優:執行緒按照順序獲取鎖,不會出現餓死現象(注:餓死現象是指一個執行緒的CPU執行時間都被其他執行緒佔用,導致得不到CPU執行)。
- 缺:整體吞吐效率相對非公平鎖要低,等待佇列中除第一個執行緒以外的所有執行緒都會阻塞,CPU 喚醒執行緒的開銷比非公平鎖要大。
非公平鎖
- 優:可以減少喚起執行緒上下文切換的消耗,整體吞吐量比公平鎖高。
- 缺:在高併發環境下可能造成執行緒優先順序反轉和餓死現象。
Java中的公平&非公平鎖
在 Java 中,synchronized 是典型的非公平鎖,而 ReentrantLock 既可以是公平鎖也可以是非公平鎖,可以在初始化的時候指定。
檢視 ReentrantLock 的原始碼會發現,初始化時可以傳入 true 或 false,來得到公平或非公平鎖。
//原始碼
//預設為非公平
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
public class FairLockDemo {
public static void main(String[] args) {
//公平鎖
Lock fairLock = new ReentrantLock(true);
//非公平鎖
Lock unFairLock = new ReentrantLock(false);
}
}
二、可重入鎖
是什麼
可重入鎖也叫遞迴鎖,是指執行緒可以進入任何一個它已經擁有的鎖所同步的程式碼塊。通俗來講,就好比你開啟了你家的大門,就可以隨意的進入客廳、廚房、衛生間......
如下圖,執行緒 M1 和 M2 是被同一把鎖同步的方法,M1 中呼叫了 M2,那麼執行緒 A 訪問 M1 時,再訪問 M2 就不需要重新獲取鎖了。
優缺點
- 優:可以一定程度上避免死鎖
- 缺:暫時不知道
Java中的可重入鎖
synchronized和ReentrantLock都是典型的可重入鎖
synchronized
public class ReentrantDemo1 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> {
phone.sendSMS();
}).start();
new Thread(() -> {
phone.sendSMS();
}).start();
}
}
class Phone {
public synchronized void sendSMS() {
System.out.println(Thread.currentThread().getId() + ":sendSMS()");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
sendEmail();
}
public synchronized void sendEmail() {
System.out.println(Thread.currentThread().getId() + ":sendEmail()");
}
}
ReentrantLock
public class ReentrantDemo2 {
public static void main(String[] args) {
User user = new User();
new Thread(() -> {
user.getName();
}).start();
new Thread(() -> {
user.getName();
}).start();
}
}
class User {
Lock lock = new ReentrantLock();
public void getName() {
lock.lock();
try {
System.out.println(Thread.currentThread().getId() + ":getName()");
TimeUnit.SECONDS.sleep(1);
getAge();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void getAge() {
lock.lock();
try {
System.out.println(Thread.currentThread().getId() + ":getAge()");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
八鎖問題
點選檢視我之前的部落格 多執行緒之8鎖問題,搞懂八鎖問題,可以更深刻的理解 synchronized 鎖的範圍
實現一個不可重入鎖
public class UnReentrantLockDemo {
private AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void lock() {
Thread current = Thread.currentThread();
//自旋
while(!atomicReference.compareAndSet(null, current)) {
}
}
public void unlock() {
Thread current = Thread.currentThread();
atomicReference.compareAndSet(current, null);
}
}
三、自旋鎖
是什麼
嘗試獲取鎖的執行緒不會立即阻塞,而是以迴圈的方式不斷嘗試獲取鎖
優缺點
- 優:減少執行緒上下文切換的消耗
- 缺:迴圈消耗CPU
Java中的自旋鎖
CAS:CompareAndSwap,比較並交換,它是一種樂觀鎖。
CAS 中有三個引數:記憶體值V、舊的預期值A、要修改的新值B;只有當預期值A與記憶體值V相等時,才會將記憶體值V修改為新值B,否則什麼都不做
public class CASTest {
public static void main(String[] args) {
AtomicInteger a1 = new AtomicInteger(1);
//V=1, A=1, B=2
//V=A,所以修改成功,此時V=2
System.out.println(a1.compareAndSet(1, 2) + "," + a1.get());
//V=2, A=1, B=2
//V!=A,修改失敗,返回false
System.out.println(a1.compareAndSet(1, 2) + "," + a1.get());
}
}
原始碼解析:以 AtomicInteger 中的 getAndIncrement() 方法為例
//獲取並增加,相當於i++操作
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
//呼叫UnSafe類中的getAndAddInt()方法
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;
}
CAS 也存在一些問題:
- 如果一直交換不成功,會一直迴圈,開銷大
- 只能保證一個共享變數的原子操作
- ABA 問題:即 A 被修改為 B,又被改為 A,雖然值沒發生變化,但這種操作還是存在一定風險的
可以通過加時間戳或版本號的方式解決 ABA 問題:
public class ABATest {
public static void main(String[] args) {
showABA();
}
/**
* 重現ABA問題
*/
private static void showABA() {
AtomicReference<String> atomicReference = new AtomicReference<>("A");
//執行緒X,模擬ABA問題
new Thread(() -> {
atomicReference.compareAndSet("A", "B");
atomicReference.compareAndSet("B", "A");
}, "執行緒X").start();
//執行緒Y睡眠一會兒,等待X執行完
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicReference.compareAndSet("A", "C");
System.out.println("最終結果:" + atomicReference.get());
}, "執行緒Y").start();
}
/**
* 解決ABA問題
*/
private static void solveABA() {
//初始版本號為1
AtomicStampedReference<String> asr = new AtomicStampedReference<>("A", 1);
new Thread(() -> {
asr.compareAndSet("A", "B", 1, 2);
asr.compareAndSet("B", "A", 2, 3);
}, "執行緒X").start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
asr.compareAndSet("A", "C", 1, 2);
System.out.println(asr.getReference() + ":" + asr.getStamp());
}, "執行緒Y").start();
}
}
動手實現一個自旋鎖
public class SpinLockDemo {
/**
* 初始值為 null
*/
AtomicReference<Thread> atomicReference = new AtomicReference<>(null);
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(() -> {
spinLockDemo.lock();
spinLockDemo.unLock();
}, "執行緒A").start();
new Thread(() -> {
spinLockDemo.lock();
spinLockDemo.unLock();
}, "執行緒B").start();
}
public void lock() {
//獲取當前執行緒物件
Thread thread = Thread.currentThread();
do {
System.out.println(thread.getName() + "嘗試獲取鎖...");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//當賦值成功才會跳出迴圈
} while (!atomicReference.compareAndSet(null, thread));
}
public void unLock() {
//獲取當前執行緒物件
Thread thread = Thread.currentThread();
//置為null,相當於釋放鎖
atomicReference.compareAndSet(thread, null);
System.out.println(thread.getName() + "釋放鎖...");
}
}
四、共享鎖&獨佔鎖
是什麼
- 共享鎖:也可稱為讀鎖,可被多個執行緒持有
- 獨佔鎖:也可稱為寫鎖,只能被一個執行緒持有,synchronized和ReentrantLock都是獨佔鎖
- 互斥:讀讀共享、讀寫互斥、寫寫互斥
優缺點
讀寫分離,適用於大量讀、少量寫的場景,效率高
java中的共享鎖&獨佔鎖
ReentrantReadWriteLock 中的讀鎖是共享鎖、寫鎖是獨佔鎖
class MyCache {
private volatile Map<String, Object> map = new HashMap<>();
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
/**
* 寫鎖控制寫入
*/
public void put(String key, Object value) {
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "開始寫入...");
//睡一會兒
TimeUnit.SECONDS.sleep(1);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "寫入完成...");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock();
}
}
/**
* 讀鎖控制讀取
*/
public Object get(String key) {
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "開始讀取...");
//睡一會兒
TimeUnit.SECONDS.sleep(1);
Object value = map.get(key);
System.out.println(Thread.currentThread().getName() + "讀取結束...value=" + value);
return value;
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.readLock().unlock();
}
return null;
}
public void clear() {
map.clear();
}
}
public class ReentrantReadWriteLockDemo {
public static void main(String[] args) {
MyCache cache = new MyCache();
for (int i = 1; i <= 5; i++) {
int finalI = i;
new Thread(() -> {
cache.put(String.valueOf(finalI), String.valueOf(finalI));
cache.get(String.valueOf(finalI));
}, "執行緒" + i).start();
}
cache.clear();
}
}