談談 Java 中的那些“瑣”事

農夫三拳有點疼~ 發表於 2020-09-22

一、公平鎖&非公平鎖

是什麼

  • 公平鎖:執行緒按照申請鎖的順序來獲取鎖;在併發環境中,每個執行緒都會被加到等待佇列中,按照 FIFO 的順序獲取鎖。

    談談 Java 中的那些“瑣”事

  • 非公平鎖:執行緒不按照申請鎖的順序來獲取鎖;一上來就嘗試佔有鎖,如果佔有失敗,則按照公平鎖的方式等待。

    談談 Java 中的那些“瑣”事

通俗來講,公平鎖就相當於現實中的排隊,先來後到;非公平鎖就是無秩序,誰搶到是誰的;

優缺點

公平鎖

  • 優:執行緒按照順序獲取鎖,不會出現餓死現象(注:餓死現象是指一個執行緒的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 中的那些“瑣”事

優缺點

  • 優:可以一定程度上避免死鎖
  • 缺:暫時不知道

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();
    }
}