2019年Java大廠面試題講解(周陽) 之Java 的鎖

公平鎖和非公平鎖

概念

公平鎖
是指多個線程按照申請鎖的順序來獲取鎖,類似於排隊買飯,先來後到,先來先服務,就是公平的,也就是隊列

非公平鎖
是指多個線程獲取鎖的順序,並不是按照申請鎖的順序,有可能申請的線程比先申請的線程優先獲取鎖,在高併發環境下,有可能造成優先級翻轉,或者飢餓的線程(也就是某個線程一直得不到鎖)

如何創建

併發包中ReentrantLock的創建可以指定析構函數的boolean類型來得到公平鎖或者非公平鎖,默認是非公平鎖

/**
* 創建一個可重入鎖,true 表示公平鎖,false 表示非公平鎖。默認非公平鎖
*/
Lock lock = new ReentrantLock(true);

兩者區別

公平鎖:就是很公平,在併發環境中,每個線程在獲取鎖時會先查看此鎖維護的等待隊列,如果爲空,或者當前線程是等待隊列中的第一個,就佔用鎖,否者就會加入到等待隊列中,以後安裝FIFO的規則從隊列中取到自己

非公平鎖: 非公平鎖比較粗魯,上來就直接嘗試佔有鎖,如果嘗試失敗,就再採用類似公平鎖那種方式。

題外話

Java ReenttrantLock通過構造函數指定該鎖是否公平,默認是非公平鎖,因爲非公平鎖的優點在於吞吐量比公平鎖大,對於synchronized而言,也是一種非公平鎖.

可重入鎖和遞歸鎖ReentrantLock

概念

可重入鎖就是遞歸鎖
指的是同一線程外層函數獲得鎖之後,內層遞歸函數仍然能獲取到該鎖的代碼,在同一線程在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖

也就是說:線程可以進入任何一個它已經擁有的鎖所同步的代碼塊

ReentrantLock / Synchronized 就是一個典型的可重入鎖.

代碼

可重入鎖就是,在一個method1方法中加入一把鎖,方法2也加鎖了,那麼他們擁有的是同一把鎖.

public synchronized void method1() {
	method2();
}

public synchronized void method2() {

}

也就是說我們只需要進入method1後,那麼它也能直接進入method2方法,因爲他們所擁有的鎖,是同一把。

作用

可重入鎖的最大作用就是避免死鎖

可重入鎖驗證

證明Synchronized

/**
 * 可重入鎖(也叫遞歸鎖)
 * 指的是同一線程外層函數獲得鎖之後,內層遞歸函數仍然能獲取到該鎖的代碼,在同一線程在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖
 *
 * 也就是說:`線程可以進入任何一個它已經擁有的鎖所同步的代碼塊`
 * @author: 輕狂書生FS
 * @create: 2020-04-15-12:12
 */

/**
 * 資源類
 */
class Phone {

    /**
     * 發送短信
     * @throws Exception
     */
    public synchronized void sendSMS() throws Exception{
        System.out.println(Thread.currentThread().getName() + "\t invoked sendSMS()");

        // 在同步方法中,調用另外一個同步方法
        sendEmail();
    }

    /**
     * 發郵件
     * @throws Exception
     */
    public synchronized void sendEmail() throws Exception{
        System.out.println(Thread.currentThread().getId() + "\t invoked sendEmail()");
    }
}
public class ReenterLockDemo {


    public static void main(String[] args) {
        Phone phone = new Phone();

        // 兩個線程操作資源列
        new Thread(() -> {
            try {
                phone.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "t1").start();

        new Thread(() -> {
            try {
                phone.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "t2").start();
    }
}

在這裏,我們編寫了一個資源類phone,擁有兩個加了synchronized的同步方法,分別是sendSMS 和 sendEmail,我們在sendSMS方法中,調用sendEmail。最後在主線程同時開啓了兩個線程進行測試,最後得到的結果爲:

t1	 invoked sendSMS()
t1	 invoked sendEmail()
t2	 invoked sendSMS()
t2	 invoked sendEmail()

這就說明當 t1 線程進入sendSMS的時候,擁有了一把鎖,同時t2線程無法進入,直到t1線程拿着鎖,執行了sendEmail 方法後,才釋放鎖,這樣t2才能夠進入

t1	 invoked sendSMS()      t1線程在外層方法獲取鎖的時候
t1	 invoked sendEmail()    t1在進入內層方法會自動獲取鎖

t2	 invoked sendSMS()      t2線程在外層方法獲取鎖的時候
t2	 invoked sendEmail()    t2在進入內層方法會自動獲取鎖

證明ReentrantLock

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 資源類
 */
class Phone implements Runnable{

    Lock lock = new ReentrantLock();

    /**
     * set進去的時候,就加鎖,調用set方法的時候,能否訪問另外一個加鎖的set方法
     */
    public void getLock() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t get Lock");
            setLock();
        } finally {
            lock.unlock();
        }
    }

    public void setLock() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t set Lock");
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void run() {
        getLock();
    }
}
public class ReenterLockDemo {


    public static void main(String[] args) {
        Phone phone = new Phone();

        /**
         * 因爲Phone實現了Runnable接口
         */
        Thread t3 = new Thread(phone, "t3");
        Thread t4 = new Thread(phone, "t4");
        t3.start();
        t4.start();
    }
}

現在我們使用ReentrantLock進行驗證,首先資源類實現了Runnable接口,重寫Run方法,裏面調用get方法,get方法在進入的時候,就加了鎖

    public void getLock() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t get Lock");
            setLock();
        } finally {
            lock.unlock();
        }
    }

然後在方法裏面,又調用另外一個加了鎖的setLock方法

    public void setLock() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t set Lock");
        } finally {
            lock.unlock();
        }
    }

最後輸出結果我們能發現,結果和加synchronized方法是一致的,都是在外層的方法獲取鎖之後,線程能夠直接進入裏層.

t3	 get Lock
t3	 set Lock
t4	 get Lock
t4	 set Lock

當我們在getLock方法加兩把鎖會是什麼情況呢? (阿里面試)

    public void getLock() {
        lock.lock();
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t get Lock");
            setLock();
        } finally {
            lock.unlock();
            lock.unlock();
        }
    }

得到結果

t3	 get Lock
t3	 set Lock

也就是說程序直接卡死,線程不能出來,也就說明我們申請幾把鎖,最後需要解除幾把鎖

當我們只加一把鎖,但是用兩把鎖來解鎖的時候,又會出現什麼情況呢?

    public void getLock() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t get Lock");
            setLock();
        } finally {
            lock.unlock();
            lock.unlock();
        }
    }

這個時候,運行程序會直接報錯

t3	 get Lock
t3	 set Lock
t4	 get Lock
t4	 set Lock
Exception in thread "t3" Exception in thread "t4" java.lang.IllegalMonitorStateException
	at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
	at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
	at com.moxi.interview.study.thread.Phone.getLock(ReenterLockDemo.java:52)
	at com.moxi.interview.study.thread.Phone.run(ReenterLockDemo.java:67)
	at java.lang.Thread.run(Thread.java:745)
java.lang.IllegalMonitorStateException
	at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
	at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
	at com.moxi.interview.study.thread.Phone.getLock(ReenterLockDemo.java:52)
	at com.moxi.interview.study.thread.Phone.run(ReenterLockDemo.java:67)
	at java.lang.Thread.run(Thread.java:745)

自旋鎖

概念

自旋鎖:spinlock,是指嘗試獲取鎖的線程不會立即阻塞,而是採用循環的方式去嘗試獲取鎖,這樣的好處是減少線程上下文切換的消耗,缺點是循環會消耗CPU

原來提到的比較並交換,底層使用的就是自旋,自旋就是多次嘗試,多次訪問,不會阻塞的狀態就是自旋。

優缺點

優點:循環比較獲取直到成功爲止,沒有類似於wait的阻塞

缺點:當不斷自旋的線程越來越多的時候,會因爲執行while循環不斷的消耗CPU資源

手寫自旋鎖

通過CAS操作完成自旋鎖,A線程先進來調用myLock方法自己持有鎖5秒,B隨後進來發現當前有線程持有鎖,不是null,所以只能通過自旋等待,直到A釋放鎖後B隨後搶到

/**
 * 手寫一個自旋鎖
 *
 * 循環比較獲取直到成功爲止,沒有類似於wait的阻塞
 *
 * 通過CAS操作完成自旋鎖,A線程先進來調用myLock方法自己持有鎖5秒,B隨後進來發現當前有線程持有鎖,不是null,所以只能通過自旋等待,直到A釋放鎖後B隨後搶到
 * @author: 輕狂書生FS
 * @create: 2020-04-15-15:46
 */
public class SpinLockDemo {

    // 現在的泛型裝的是Thread,原子引用線程
    AtomicReference<Thread>  atomicReference = new AtomicReference<>();

    public void myLock() {
        // 獲取當前進來的線程
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "\t come in ");

        // 開始自旋,期望值是null,更新值是當前線程,如果是null,則更新爲當前線程,否者自旋
        while(!atomicReference.compareAndSet(null, thread)) {

        }
    }

    /**
     * 解鎖
     */
    public void myUnLock() {

        // 獲取當前進來的線程
        Thread thread = Thread.currentThread();

        // 自己用完了後,把atomicReference變成null
        atomicReference.compareAndSet(thread, null);

        System.out.println(Thread.currentThread().getName() + "\t invoked myUnlock()");
    }

    public static void main(String[] args) {

        SpinLockDemo spinLockDemo = new SpinLockDemo();

        // 啓動t1線程,開始操作
        new Thread(() -> {

            // 開始佔有鎖
            spinLockDemo.myLock();


            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 開始釋放鎖
            spinLockDemo.myUnLock();

        }, "t1").start();


        // 讓main線程暫停1秒,使得t1線程,先執行
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 1秒後,啓動t2線程,開始佔用這個鎖
        new Thread(() -> {

            // 開始佔有鎖
            spinLockDemo.myLock();
            // 開始釋放鎖
            spinLockDemo.myUnLock();

        }, "t2").start();

    }
}

最後輸出結果

t1	 come in 
.....五秒後.....
t1	 invoked myUnlock()
t2	 come in 
t2	 invoked myUnlock()

首先輸出的是 t1 come in

然後1秒後,t2線程啓動,發現鎖被t1佔有,所有不斷的執行 compareAndSet方法,來進行比較,直到t1釋放鎖後,也就是5秒後,t2成功獲取到鎖,然後釋放。

獨佔鎖(寫鎖) / 共享鎖(讀鎖) / 互斥鎖

概念

獨佔鎖:指該鎖一次只能被一個線程所持有。對ReentrantLock和Synchronized而言都是獨佔鎖

共享鎖:指該鎖可以被多個線程鎖持有

對ReentrantReadWriteLock其讀鎖是共享,其寫鎖是獨佔

寫的時候只能一個人寫,但是讀的時候,可以多個人同時讀

爲什麼會有寫鎖和讀鎖

原來我們使用ReentrantLock創建鎖的時候,是獨佔鎖,也就是說一次只能一個線程訪問,但是有一個讀寫分離場景,讀的時候想同時進行,因此原來獨佔鎖的併發性就沒這麼好了,因爲讀鎖並不會造成數據不一致的問題,因此可以多個人共享讀

多個線程 同時讀一個資源類沒有任何問題,所以爲了滿足併發量,讀取共享資源應該可以同時進行,
但是如果一個線程想去寫共享資源,就不應該再有其它線程可以對該資源進行讀或寫
  • 讀-讀:能共存
  • 讀-寫:不能共存
  • 寫-寫:不能共存

代碼實現

實現一個讀寫緩存的操作,假設開始沒有加鎖的時候,會出現什麼情況

/**
 * 讀寫鎖
 * 多個線程 同時讀一個資源類沒有任何問題,所以爲了滿足併發量,讀取共享資源應該可以同時進行
 * 但是,如果一個線程想去寫共享資源,就不應該再有其它線程可以對該資源進行讀或寫
 *
 * @author: 輕狂書生FS
 * @create: 2020-04-15-16:59
 */

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;

/**
 * 資源類
 */
class MyCache {

    private volatile Map<String, Object> map = new HashMap<>();
    // private Lock lock = null;

    /**
     * 定義寫操作
     * 滿足:原子 + 獨佔
     * @param key
     * @param value
     */
    public void put(String key, Object value) {
        System.out.println(Thread.currentThread().getName() + "\t 正在寫入:" + key);
        try {
            // 模擬網絡擁堵,延遲0.3秒
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + "\t 寫入完成");
    }

    public void get(String key) {
        System.out.println(Thread.currentThread().getName() + "\t 正在讀取:");
        try {
            // 模擬網絡擁堵,延遲0.3秒
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Object value = map.get(key);
        System.out.println(Thread.currentThread().getName() + "\t 讀取完成:" + value);
    }


}
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(() -> {
                myCache.put(tempInt + "", tempInt +  "");
            }, String.valueOf(i)).start();
        }
        // 線程操作資源類, 5個線程讀
        for (int i = 0; i < 5; i++) {
            // lambda表達式內部必須是final
            final int tempInt = i;
            new Thread(() -> {
                myCache.get(tempInt + "");
            }, String.valueOf(i)).start();
        }
    }
}

我們分別創建5個線程寫入緩存

        // 線程操作資源類,5個線程寫
        for (int i = 0; i < 5; i++) {
            // lambda表達式內部必須是final
            final int tempInt = i;
            new Thread(() -> {
                myCache.put(tempInt + "", tempInt +  "");
            }, String.valueOf(i)).start();
        }

5個線程讀取緩存,

        // 線程操作資源類, 5個線程讀
        for (int i = 0; i < 5; i++) {
            // lambda表達式內部必須是final
            final int tempInt = i;
            new Thread(() -> {
                myCache.get(tempInt + "");
            }, String.valueOf(i)).start();
        }

最後運行結果:

0	 正在寫入:0
4	 正在寫入:4
3	 正在寫入:3
1	 正在寫入:1
2	 正在寫入:2
0	 正在讀取:
1	 正在讀取:
2	 正在讀取:
3	 正在讀取:
4	 正在讀取:
2	 寫入完成
4	 寫入完成
4	 讀取完成:null
0	 寫入完成
3	 讀取完成:null
0	 讀取完成:null
1	 寫入完成
3	 寫入完成
1	 讀取完成:null
2	 讀取完成:null

我們可以看到,在寫入的時候,寫操作都沒其它線程打斷了,這就造成了,還沒寫完,其它線程又開始寫,這樣就造成數據不一致

解決方法

上面的代碼是沒有加鎖的,這樣就會造成線程在進行寫入操作的時候,被其它線程頻繁打斷,從而不具備原子性,這個時候,我們就需要用到讀寫鎖來解決了

/**
* 創建一個讀寫鎖
* 它是一個讀寫融爲一體的鎖,在使用的時候,需要轉換
*/
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

當我們在進行寫操作的時候,就需要轉換成寫鎖

// 創建一個寫鎖
rwLock.writeLock().lock();

// 寫鎖 釋放
rwLock.writeLock().unlock();

當們在進行讀操作的時候,在轉換成讀鎖

// 創建一個讀鎖
rwLock.readLock().lock();

// 讀鎖 釋放
rwLock.readLock().unlock();

這裏的讀鎖和寫鎖的區別在於,寫鎖一次只能一個線程進入,執行寫操作,而讀鎖是多個線程能夠同時進入,進行讀取的操作

完整代碼:

/**
 * 讀寫鎖
 * 多個線程 同時讀一個資源類沒有任何問題,所以爲了滿足併發量,讀取共享資源應該可以同時進行
 * 但是,如果一個線程想去寫共享資源,就不應該再有其它線程可以對該資源進行讀或寫
 *
 * @author: 輕狂書生FS
 * @create: 2020-03-15-16:59
 */

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 資源類
 */
class MyCache {

    /**
     * 緩存中的東西,必須保持可見性,因此使用volatile修飾
     */
    private volatile Map<String, Object> map = new HashMap<>();

    /**
     * 創建一個讀寫鎖
     * 它是一個讀寫融爲一體的鎖,在使用的時候,需要轉換
     */
    private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

    /**
     * 定義寫操作
     * 滿足:原子 + 獨佔
     * @param key
     * @param value
     */
    public void put(String key, Object value) {

        // 創建一個寫鎖
        rwLock.writeLock().lock();

        try {

            System.out.println(Thread.currentThread().getName() + "\t 正在寫入:" + key);

            try {
                // 模擬網絡擁堵,延遲0.3秒
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            map.put(key, value);

            System.out.println(Thread.currentThread().getName() + "\t 寫入完成");

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 寫鎖 釋放
            rwLock.writeLock().unlock();
        }
    }

    /**
     * 獲取
     * @param key
     */
    public void get(String key) {

        // 讀鎖
        rwLock.readLock().lock();
        try {

            System.out.println(Thread.currentThread().getName() + "\t 正在讀取:");

            try {
                // 模擬網絡擁堵,延遲0.3秒
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            Object value = map.get(key);

            System.out.println(Thread.currentThread().getName() + "\t 讀取完成:" + value);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 讀鎖釋放
            rwLock.readLock().unlock();
        }
    }

    /**
     * 清空緩存
     */
    public void clean() {
        map.clear();
    }


}
public class ReadWriteLockDemo {

    public static void main(String[] args) {

        MyCache myCache = new MyCache();

        // 線程操作資源類,5個線程寫
        for (int i = 1; i <= 5; i++) {
            // lambda表達式內部必須是final
            final int tempInt = i;
            new Thread(() -> {
                myCache.put(tempInt + "", tempInt +  "");
            }, String.valueOf(i)).start();
        }

        // 線程操作資源類, 5個線程讀
        for (int i = 1; i <= 5; i++) {
            // lambda表達式內部必須是final
            final int tempInt = i;
            new Thread(() -> {
                myCache.get(tempInt + "");
            }, String.valueOf(i)).start();
        }
    }
}

運行結果:

1	 正在寫入:1
1	 寫入完成
2	 正在寫入:2
2	 寫入完成
3	 正在寫入:3
3	 寫入完成
4	 正在寫入:4
4	 寫入完成
5	 正在寫入:5
5	 寫入完成
2	 正在讀取:
3	 正在讀取:
1	 正在讀取:
4	 正在讀取:
5	 正在讀取:
2	 讀取完成:2
1	 讀取完成:1
4	 讀取完成:4
3	 讀取完成:3
5	 讀取完成:5

從運行結果我們可以看出,寫入操作是一個一個線程進行執行的,並且中間不會被打斷,而讀操作的時候,是同時5個線程進入,然後併發讀取操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章