談談 Java 中的那些「瑣」事
一、公平鎖&非公平鎖
是什麼
-
公平鎖:線程按照申請鎖的順序來獲取鎖;在並發環境中,每個線程都會被加到等待隊列中,按照 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();
}
}