多線程的這些鎖知道嗎?手寫一個自旋鎖?


多線程中的各種鎖

1. 公平鎖、非公平鎖

1.1 概念:

公平鎖就是先來后到、非公平鎖就是允許加塞 Lock lock = new ReentrantLock(Boolean fair); 默認非公平

  • 公平鎖是指多個線程按照申請鎖的順序來獲取鎖,類似排隊打飯。
  • 非公平鎖是指多個線程獲取鎖的順序並不是按照申請鎖的順序,有可能后申請的線程優先獲取鎖,在高並發的情況下,有可能會造成優先級反轉或者節現象。

1.2 兩者區別?

  • 公平鎖:

    Threads acquire a fair lock in the order in which they requested it

    公平鎖,就是很公平,在並發環境中,每個線程在獲取鎖時,會先查看此鎖維護的等待隊列,如果為空,或者當前線程就是等待隊列的第一個,就占有鎖,否則就會加入到等待隊列中,以后會按照FIFO的規則從隊列中取到自己

  • 非公平鎖:

    a nonfair lock permits barging: threads requesting a lock can jump ahead of the queue of waiting threads if the lock happens to be available when it is requested

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

1.3 如何體現公平非公平?

  • 對Java ReentrantLock而言,通過構造函數指定該鎖是否公平,默認是非公平鎖,非公平鎖的優點在於吞吐量比公平鎖大
  • 對Synchronized而言,是一種非公平鎖

image-20210707143515840

其中ReentrantLock和Synchronized默認都是非公平鎖,默認都是可重入鎖

2. 可重入鎖(遞歸鎖)

前文提到ReentrantLock和Synchronized默認都是非公平鎖,默認都是可重入鎖,那么什么是可重入鎖?

2.1 概念

指的時同一線程外層函數獲得鎖之后,內層遞歸函數仍然能獲取該鎖的代碼,在同一個線程在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖,也就是說,線程可以進入任何一個它已經擁有的鎖所同步着的代碼塊

2.2 為什么要用到可重入鎖?

  1. 可重入鎖最大的作用是避免死鎖
  2. ReentrantLock/Synchronized 就是一個典型的可重入鎖

2.3 代碼驗證可重入鎖?

首先我們先驗證ReentrantLock

package com.yuxue.juc.lockDemo;


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

/**
 * 嘗試驗證ReentrantLock鎖的可重入性的Demo
 * */
public class ReentrantLockDemo {
    public static void main(String[] args) {

        mobile mobile = new mobile();

        new Thread(mobile,"t1").start();
        new Thread(mobile,"t2").start();
    }
}

/**
 * 輔助類mobile,首先繼承了Runnable接口,可以重寫run方法
 * 內部主要有兩個方法
 * run方法首先調用第一個方法
 * */
class mobile implements Runnable {

    Lock lock = new ReentrantLock();

    //run方法首先調用第一個方法
    @Override
    public void run() {
        testMethod01();
    }
    //第一個方法,目的是首先讓線程進入方法1
    public void testMethod01() {
        //加鎖
        lock.lock();
        try {
            //驗證線程進入方法1
            System.out.println(Thread.currentThread().getName() + "\t" + "get in the method1");
            //休眠
            Thread.sleep(2000);
            //進入方法2
            testMethod02();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    //第二個方法。目的是驗證ReentrantLock是否是可重入鎖
    public void testMethod02() {
        lock.lock();
        try {
            System.out.println("==========" + Thread.currentThread().getName() + "\t" + "leave the method1 get in the method2");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

因為同一個線程在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖,也就是說,線程可以進入任何一個它已經擁有的鎖所同步着的代碼塊

之后觀察輸出結果為:

t1	get in the method1
==========t1	leave the method1 get in the method2
t2	get in the method1
==========t2	leave the method1 get in the method2

意味着線程t1進入方法1之后,再進入方法2,也就是說進入內層方法自動獲取鎖,之后釋放方法2的那把鎖,再釋放方法1的那把鎖,這之后線程t2才能獲取到方法1的鎖,才可以進入方法1

同樣地,如果我們在方法1中再加一把鎖,不給其解鎖,也就是

image-20210707153757001

那么結果會是怎么呢?我們運行代碼可以得到

image-20210707153858881

我們發現線程是停不下來的,線程t1進入方法1加了兩把鎖,之后進入t2,但是退出t1的方法過程中沒有解鎖,這就導致了t2線程無法拿到鎖,也就驗證了鎖重入的問題

那么為了驗證是同一把鎖,我們在方法1對其加鎖兩次,方法2對其解鎖兩次可以嗎?這鎖是相同的嗎?也就意味着:

image-20210707154054167

我們再次運行,發現結果為:

t1	get in the method1
==========t1	leave the method1 get in the method2
t2	get in the method1
==========t2	leave the method1 get in the method2

也就側面驗證了加鎖的是同一把鎖,更驗證了我們的鎖重入問題

那么對於synchronized鎖呢?代碼以及結果如下所示:

package com.yuxue.juc.lockDemo;

public class SynchronizedDemo {
    public static void main(String[] args) throws InterruptedException {
        phone phone = new phone();
        new Thread(()->{
            phone.phoneTest01();
        },"t1").start();
        Thread.sleep(1000);
        new Thread(()->{
            phone.phoneTest01();
        },"t2").start();
    }
}
class phone{
    public synchronized void phoneTest01(){
        System.out.println(Thread.currentThread().getName()+"\t invoked phoneTest01()");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        phoneTest02();
    }
    public synchronized void phoneTest02() {
        System.out.println(Thread.currentThread().getName()+"\t -----invoked phoneTest02()");
    }
}

結果為:

t1	 invoked phoneTest01()
t1	 -----invoked phoneTest02()
t2	 invoked phoneTest01()
t2	 -----invoked phoneTest02()

上述兩個實驗可以驗證我們的ReentrantLock以及synchronized都是可重入鎖!

3. 自旋鎖

3.1 自旋鎖概念

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

就是我們CAS一文當中提到的這段代碼:

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

其中while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4))這行代碼更體現出了自旋鎖的核心,也就是當我嘗試去拿鎖的時候,一直循環,直到拿到鎖為止

3.2 手寫一個自旋鎖試試?

下面的AtomicReference可以去看CAS那篇有詳細講解

package com.yuxue.juc.lockDemo;

import java.util.concurrent.atomic.AtomicReference;

/**
 * 實現自旋鎖
 * 自旋鎖好處,循環比較獲取直到成功為止,沒有類似wait的阻塞
 *
 * 通過CAS操作完成自旋鎖,t1線程先進來調用mylock方法自己持有鎖2秒鍾,
 * t2隨后進來發現當前有線程持有鎖,不是null,所以只能通過自旋等待,直到t1釋放鎖后t2隨后搶到
 */

public class SpinLockDemo {
    public static void main(String[] args) {
        //資源類
        SpinLock spinLock = new SpinLock();
        //t1線程
        new Thread(()->{
            //加鎖
            spinLock.myLock();
            try {
                //休眠
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //解鎖
            spinLock.myUnlock();
        },"t1").start();

        //這里主線程休眠,為了讓t1首先得到並加鎖
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //t2線程
        new Thread(()->{
            spinLock.myLock();
            try {
                //這里休眠時間較長是為了讓輸出結果更加可視化
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLock.myUnlock();
        },"t2").start();
    }
}

class SpinLock {
    //構造原子引用類
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    //自己的加鎖方法
    public void myLock() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + "\t" + "come in myLock");
        //自旋核心代碼!!當期望值是null並且主內存值也為null,就將其設置為自己的thread,否則就死循環,也就是一直自旋
        while (!atomicReference.compareAndSet(null, thread)) {

        }
    }

    public void myUnlock() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + "\t" + "===== come in myUnlock");
        //解鎖,用完之后,當期望值是自己的thread主物理內存的值也是自己的,也就是被自己的線程占用
        //用完之后解鎖,將主物理內存中Thread的地方設置為空,供其他線程使用
        atomicReference.compareAndSet(thread, null);
    }
}

結果為:

//t1以及t2同時進入myLock方法,爭奪鎖使用權
t1	come in myLock
t2	come in myLock
//t1使用完首先釋放鎖
t1	===== come in myUnlock
//t2使用完釋放鎖,但是在5秒之后,因為在程序中我們讓其休眠了5s
t2	===== come in myUnlock

4. 讀寫鎖

分為:獨占鎖(寫鎖)/共享鎖(讀鎖)/互斥鎖

4.1 概念

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

  • 共享鎖:只該鎖可被多個線程所持有

    ReentrantReadWriteLock其讀鎖是共享鎖,寫鎖是獨占鎖

  • 互斥鎖:讀鎖的共享鎖可以保證並發讀是非常高效的,讀寫、寫讀、寫寫的過程是互斥的

4.2 代碼

首先是沒有讀寫鎖的代碼,定義了一個資源類,里面底層數據結構為HashMap,之后多個線程對其寫以及讀操作

package com.yuxue.juc.lockDemo;

import java.util.HashMap;
import java.util.Map;

class MyCache {
    //定義緩存當中的數據結構為Map型,鍵為String,值為Object類型
    private volatile Map<String, Object> map = new HashMap<>();
    //向Map中添加元素的方法
    public void setMap(String key, Object value) {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + "\t" + " put value:" + value);
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //底層的map直接put
        map.put(key, value);
        System.out.println(thread.getName() + "\t" + "put value successful");
    }
    //向Map中取出元素
    public void getMap(String key) {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + "\t" + "get the value");
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //底層的map直接get,並且返回值為Object類型
        Object retValue = map.get(key);
        System.out.println(thread.getName() + "\t" + "get the value successful, is " + retValue);
    }

}

/**
 * 多個線程同時讀一個資源類沒有任何問題,所以為了滿足並發量,讀取共享資源應該可以同時進行。 * 但是
 * 如果有一個線程想取寫共享資源來,就不應該允許其他線程可以對資源進行讀或寫
 * 總結
 * 讀讀能共存
 * 讀寫不能共存
 * 寫寫不能共存
 */
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(() -> {
                //5個線程分別填值
                myCache.setMap(tempInt + "", tempInt + "");
            }, "thread-" + i).start();
        }
        //創建5個線程
        for (int i = 0; i < 5; i++) {
            final int tempInt = i;
            new Thread(() -> {
                //5個線程取值
                myCache.getMap(tempInt + "");
            }, "thread-" + i).start();
        }
    }
}

結果為:

thread-0	 put value:0
thread-1	 put value:1
thread-2	 put value:2
thread-3	 put value:3
thread-4	 put value:4
thread-0	get the value
thread-1	get the value
thread-2	get the value
thread-3	get the value
thread-4	get the value
thread-0	put value successful
thread-1	put value successful
thread-2	put value successful
thread-3	put value successful
thread-4	put value successful
...

上述執行結果看似沒有問題,但是違背了寫鎖最核心的本質,也就是如果有一個線程想取寫共享資源來,就不應該允許其他線程可以對資源進行讀或寫

所以出現問題,此時就需要用到我們的讀寫鎖,我們對我們自己的myLock()以及myUnlock()方法進行修改為使用讀寫鎖的版本:

class MyCache {
    //定義緩存當中的數據結構為Map型,鍵為String,值為Object類型
    private volatile Map<String, Object> map = new HashMap<>();

    //讀寫鎖
    ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    /**
     *  寫操作:原子+獨占
     *  整個過程必須是一個完整的統一體,中間不許被分割,不許被打斷 *
     * @param key
     * @param value
     * */
    //向Map中添加元素的方法
    public void setMap(String key, Object value) {
        try {
            readWriteLock.writeLock().lock();
            Thread thread = Thread.currentThread();
            System.out.println(thread.getName() + "\t" + " put value:" + value);
            Thread.sleep(300);
            //底層的map直接put
            map.put(key, value);
            System.out.println(thread.getName() + "\t" + "put value successful");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }

    //向Map中取出元素
    public void getMap(String key) {
        try {
            readWriteLock.readLock().lock();
            Thread thread = Thread.currentThread();
            System.out.println(thread.getName() + "\t" + "get the value");
            Thread.sleep(300);
            //底層的map直接get,並且返回值為Object類型
            Object retValue = map.get(key);
            System.out.println(thread.getName() + "\t" + "get the value successful, is " + retValue);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
    }
}

之后運行結果變成:

thread-1	 put value:1
thread-1	put value successful
thread-0	 put value:0
thread-0	put value successful
thread-2	 put value:2
thread-2	put value successful
thread-3	 put value:3
thread-3	put value successful
thread-4	 put value:4
thread-4	put value successful
thread-0	get the value
thread-1	get the value
thread-2	get the value
thread-4	get the value
thread-3	get the value
thread-2	get the value successful, is 2
thread-3	get the value successful, is 3
thread-0	get the value successful, is 0
thread-4	get the value successful, is 4
thread-1	get the value successful, is 1

也就對應上了寫鎖獨占,必須當每一個寫鎖對應寫操作完成之后,才可以進行下一次寫操作,但是對於讀操作,就可以多個線程共享,一起去緩存中讀取數據


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM