1. 程式人生 > >談談 Java 中的那些“瑣”事

談談 Java 中的那些“瑣”事

# 一、公平鎖&非公平鎖 ## 是什麼 - 公平鎖:執行緒按照申請鎖的順序來獲取鎖;在併發環境中,每個執行緒都會被加到等待佇列中,按照 FIFO 的順序獲取鎖。 ![](https://gitee.com/songjilong/FigureBed/raw/master/img/20200921102537.png) - 非公平鎖:執行緒不按照申請鎖的順序來獲取鎖;一上來就嘗試佔有鎖,如果佔有失敗,則按照公平鎖的方式等待。 ![](https://gitee.com/songjilong/FigureBed/raw/master/img/20200921102601.png) **通俗來講,公平鎖就相當於現實中的排隊,先來後到;非公平鎖就是無秩序,誰搶到是誰的;** ## 優缺點 公平鎖 - 優:執行緒按照順序獲取鎖,不會出現餓死現象(注:餓死現象是指一個執行緒的CPU執行時間都被其他執行緒佔用,導致得不到CPU執行)。 - 缺:整體吞吐效率相對非公平鎖要低,等待佇列中除第一個執行緒以外的所有執行緒都會阻塞,CPU 喚醒執行緒的開銷比非公平鎖要大。 非公平鎖 - 優:可以減少喚起執行緒上下文切換的消耗,整體吞吐量比公平鎖高。 - 缺:在高併發環境下可能造成執行緒優先順序反轉和餓死現象。 ## Java中的公平&非公平鎖 在 Java 中,**synchronized 是典型的非公平鎖**,而 **ReentrantLock 既可以是公平鎖也可以是非公平鎖**,可以在初始化的時候指定。 檢視 ReentrantLock 的原始碼會發現,初始化時可以傳入 true 或 false,來得到公平或非公平鎖。 ```java //原始碼 //預設為非公平 public ReentrantLock() { sync = new NonfairSync(); } public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); } ``` ```java 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 就不需要重新獲取鎖了。 ![](https://gitee.com/songjilong/FigureBed/raw/master/img/20200922101619.png) ## 優缺點 - 優:可以一定程度上避免死鎖 - 缺:暫時不知道 ## Java中的可重入鎖 synchronized和ReentrantLock都是典型的可重入鎖 **synchronized** ```java 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** ```java 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鎖問題](https://www.cnblogs.com/songjilong/p/12817071.html),搞懂八鎖問題,可以更深刻的理解 synchronized 鎖的範圍 ## 實現一個不可重入鎖 ```java public class UnReentrantLockDemo { private AtomicReference 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,否則什麼都不做 ```java 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() 方法為例 ```java //獲取並增加,相當於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 問題: ```java public class ABATest { public static void main(String[] args) { showABA(); } /** * 重現ABA問題 */ private static void showABA() { AtomicReference 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 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(); } } ``` ## 動手實現一個自旋鎖 ```java public class SpinLockDemo { /** * 初始值為 null */ AtomicReference 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 中的讀鎖是共享鎖、寫鎖是獨佔鎖 ```java class MyCache { private volatile Map 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(); } } ``` ```java 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();