多線程的各種鎖

    在學習多線程的時候,我們經常會聽到可重入鎖/不可重入鎖、公平鎖/非公平鎖、讀寫鎖現在我們就逐一它們的神祕面紗。

Lock包下的層級結構
Lock包下的層級結構

      1.可重入鎖/非重入鎖:大部分jdk提供的都是可重入鎖,如syncronized,reentrantLock 都是可重入,代表單個(也可以說同一個)線程可以多次獲得該鎖,如果單個線程拿到鎖沒有釋放,你再去拿,拿到則是重入鎖 ,拿不到則是非重入鎖。

 public static void main(String[] args) {
        //KodyLock lock = new KodyLock ();
        Lock lock = new ReentrantLock ();
        lock.lock ();
        System.out.println ("獲得鎖");
        lock.lock ();
        System.out.println ("再次獲得鎖");
    }

        如上,可以發現ReentranLock是可重入的,那非重入鎖呢?下面是一個簡單的非重入鎖的實現

public class NCrLock {

    boolean isLock = false;

    public synchronized void  lock() throws InterruptedException{
        while (isLock){
            wait ();
        }
        isLock = true;
    }

    public synchronized void unlock(){
        isLock = false;
        notify ();
    }

    public static void main(String[] args) throws InterruptedException{
        NCrLock nCrLock = new NCrLock ();
        nCrLock.lock ();
        System.out.println ("獲得鎖");
        nCrLock.lock ();
        System.out.println ("再次獲得鎖");
    }

}

       2.讀寫鎖:我們知道ReentrantLock是一種排它鎖,同一時間內只允許一個線程訪問,而ReentrantReadWriteLock允許多個讀線程同時訪問,但不允許讀線程和寫線程、寫線程和寫線程同時訪問,在實際應用中,大部分共享數據(緩存)的訪問都是讀操作遠多於寫操作這時候ReentrantReadWrite就比排它鎖提供了更好的併發性和吞吐量。

 public static void main(String[] args) {
        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock ();
        new Thread (() -> {
            reentrantReadWriteLock.readLock ().lock ();
            long startTime = System.currentTimeMillis ();
            while (System.currentTimeMillis () - startTime < 5){
                //記錄5ms內 讀線程做的事情
                System.out.println (Thread.currentThread ().getName () + ":正在進行讀操作" );
            }
            reentrantReadWriteLock.readLock ().unlock ();
        },"讀鎖線程").start ();

        new Thread (() -> {
            reentrantReadWriteLock.readLock ().lock ();
            long startTime = System.currentTimeMillis ();
            while (System.currentTimeMillis () - startTime < 5){
                //記錄5ms內 讀線程做的事情
                System.out.println (Thread.currentThread ().getName () + ":正在進行讀操作" );
            }
            reentrantReadWriteLock.readLock ().unlock ();
        },"讀鎖線程2").start ();


    }

兩個讀線程控制檯打印信息如下:多試幾次因爲可能cpu調度切換這其中時間差導致先執行某個線程

讀鎖線程2:正在進行讀操作
讀鎖線程:正在進行讀操作
讀鎖線程2:正在進行讀操作
讀鎖線程2:正在進行讀操作
........

改成讀線程與寫線程控制檯打印信息如下:

讀鎖線程:正在進行讀操作
讀鎖線程:正在進行讀操作
讀鎖線程:正在進行讀操作
寫鎖線程2:正在進行讀操作
寫鎖線程2:正在進行讀操作
寫鎖線程2:正在進行讀操作
寫鎖線程2:正在進行讀操作
.......

    3.公平鎖與非公平鎖:java中自帶的關鍵字syncronized和ReentrantLock都能事先對方法或者代碼塊進行加鎖,前者只能是非公平鎖後者默認是非公平鎖,公平鎖和非公平鎖都是基於鎖內部維護的一個雙向鏈表,表節點Node的值就是每一個請求當前鎖的線程。公平鎖則是每次依次從隊首取值,所以能保證獲取鎖的順序性,而非公平鎖則是有機會去搶鎖,可能會導致線程搶鎖求而不得。

      所以總結下里就是:

  1. 公平鎖獲取鎖的順序是按照加鎖的順序來分配的,即FIFO先進先出,非公平鎖與公平鎖不一樣的就是它是隨機獲取鎖,先來的不一定得到鎖,這個方式可能造成線程拿不到鎖。

      非公平鎖:基於CAS嘗試將state(鎖數量)從0改爲1,如果設置成功則設置當前線程爲獨佔鎖線程,如果設置失敗還會在獲取一次鎖的數量:

  1. 如果鎖的數量爲0在基於CAS嘗試將state從0設置爲1,如果設置成功則設置當前線程爲獨佔鎖線程
  2. 如果鎖的數量不爲0或者上面嘗試失敗了,則查看當前鎖是不是獨佔鎖線程,如果是則將當前線程鎖的數量+1,如果不是則將線程封裝在一個Node內部,並加入到等待隊列,等待被其前一個線程節點喚醒

   公平鎖:獲取一次鎖的數量

  1. 如果鎖的數量爲0,如果當前線程是等待隊列的頭節點,基於CAS嘗試將state從哪個0設置爲1一次,設置成功則將當前線程設置爲獨佔鎖線程
  2. 如果鎖的數量不爲0或者當前線程不是等待隊列中的頭節點或者上面的嘗試失敗了,查看當前線程是不是已經是獨佔鎖線程,如果是則將當前鎖的數量+1,如果不是則將該線程封裝在一個Node內,並加入到等待隊列,等待被其前面一個線程喚醒。
  public static void main(String[] args) throws InterruptedException{
        Lock lock = new ReentrantLock (false);
        //分別依次啓動5個線程,觀察它的執行情況
        IntStream.range (0,5).forEach (value -> {
            new Thread (() -> {
                System.out.println (Thread.currentThread ().getName () + "線程開始運行");
                lock.lock ();
                System.out.println (Thread.currentThread ().getName () + "拿到鎖");
                lock.unlock ();
            },String.valueOf (value)).start ();
        });
    }

可以看出線程拿到鎖的順序爲0,1,4,3,2線程執行的順序爲0,1,3,4,2明顯是無序的

0線程開始運行
1線程開始運行
0拿到鎖
3線程開始運行
1拿到鎖
4線程開始運行
4拿到鎖
2線程開始運行
3拿到鎖
2拿到鎖

改爲公平鎖的執行情況,線程拿到鎖的順序爲0,1,2,3,4程執行的順序爲0,1,2,3,4線程拿到鎖順序與線程執行的順序一致,當然這裏拿到鎖的順序也並非全是0到4順序來的,這個得看cpu調度切換到誰。

public static void main(String[] args) throws InterruptedException{
        Lock lock = new ReentrantLock (true);
        //分別依次啓動5個線程,觀察它的執行情況
        IntStream.range (0,5).forEach (value -> {
            new Thread (() -> {
                System.out.println (Thread.currentThread ().getName () + "線程開始運行");
                lock.lock ();
                System.out.println (Thread.currentThread ().getName () + "拿到鎖");
                lock.unlock ();
            },String.valueOf (value)).start ();
        });
    }
0線程開始運行
0拿到鎖
1線程開始運行
1拿到鎖
2線程開始運行
2拿到鎖
3線程開始運行
3拿到鎖
4線程開始運行
4拿到鎖

 

 

 

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