Java并發(fā)編程 鎖

1.Lock接口

1.1 簡介、地位、作用
  • 鎖是一種工具,用于控制對共享資源的訪問。
  • Lock和synchronized,這兩個是最常見的鎖,它們都可以達到線程安全的目的,但是在使用上和功能上又有較大的不同。
  • Lock并不是用來代替synchronized的,而是當使用synchronized不合適或不足以滿足要求的時候,來提供高級功能的。
  • Lock接口最常見的實現(xiàn)類時ReentrantLock
  • 通常情況下,Lock只允許一個線程來訪問這個共享資源。不過有的時候,一些特殊的實現(xiàn)也可以允許并發(fā)訪問,比如ReadWriteLock里面的ReadLock。
1.2 為什么synchronized不夠用?

1.2.1 效率低:鎖的釋放情況少、試圖獲得鎖時不能設定超時、不能中斷一個正在試圖獲得鎖的線程。
1.2.2 不夠靈活(讀寫鎖更靈活):加鎖和釋放的時機單一,每個鎖僅有單一的條件(某個對象),可能是不夠的。
1.2.3 無法知道是否成功獲取到鎖。

1.3 Lock主要方法介紹
在Lock中聲明了四個方法來獲取鎖

lock()

就是最普通的獲取鎖。如果鎖已被其他線程獲取,則進行等待
Lock不會像synchronized一樣在異常時自動釋放鎖
因此最佳實踐是,在finally中釋放鎖,以保證發(fā)生異常時鎖一定被釋放
lock()方法不能被中斷,這樣會帶來很大的隱患:一旦陷入死鎖,lock()就會陷入永久等待

tryLock()
  • tryLock()用來嘗試獲取鎖,如果當前鎖沒有被其他線程占用。則獲取成功,則返回true,否則返回false,代表獲取鎖失敗
  • 相比于lock,這樣的方式顯然功能更強大了,我們可以根據(jù)是否能獲取到鎖決定后續(xù)程序的行為
tryLock(long time,TimeUnit unit)

超時就放棄

  • 該方法會立即返回,即便在拿不到鎖時不會一直在那等
public class TryLockDeadLock implements Runnable {
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();
    int flag = 1;

    public static void main(String[] args) {
        TryLockDeadLock tryLockDeadLock1 = new TryLockDeadLock();
        TryLockDeadLock tryLockDeadLock2 = new TryLockDeadLock();
        tryLockDeadLock1.flag = 1;
        tryLockDeadLock2.flag = 2;
        new Thread(tryLockDeadLock1).start();
        new Thread(tryLockDeadLock2).start();
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if (flag == 1) {
                try {
                    if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
                        try {
                            System.out.println("線程1獲取到了鎖1");
                            Thread.sleep(new Random().nextInt(1000));
                            if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
                                try {
                                    System.out.println("線程1獲取到了鎖2");
                                    System.out.println("線程1成功獲取到了2把鎖");
                                    break;
                                } finally {
                                    lock2.unlock();
                                }
                            } else {
                                System.out.println("線程1獲取鎖2失敗,已重試");
                            }
                        } finally {
                            lock1.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
                        System.out.println("線程1獲取鎖1失敗,已重試");
                    }

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            if (flag == 2) {
                try {
                    if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
                        try {
                            System.out.println("線程2獲取到了鎖2");
                            Thread.sleep(new Random().nextInt(1000));
                            if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
                                try {
                                    System.out.println("線程2獲取到了鎖1");
                                    System.out.println("線程2成功獲取到了2把鎖");
                                    break;
                                } finally {
                                    lock1.unlock();
                                }
                            } else {
                                System.out.println("線程2獲取鎖2失敗,已重試");
                            }
                        } finally {
                            lock2.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
                        System.out.println("線程2獲取鎖2失敗,已重試");
                    }

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
線程1獲取到了鎖1
線程2獲取到了鎖2
線程1獲取鎖2失敗,已重試
線程2獲取到了鎖1
線程2成功獲取到了2把鎖
線程1獲取到了鎖1
線程1獲取到了鎖2
線程1成功獲取到了2把鎖
lockInterruptibly()

相當于tryLock(long time,TimeUnit unit)把超時時間設置為無限。在等待鎖的過程中,線程可以被中斷

public class LockInterruptibly implements Runnable {
    private Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        LockInterruptibly lockInterruptibly = new LockInterruptibly();
        Thread thread0 = new Thread(lockInterruptibly);
        Thread thread1 = new Thread(lockInterruptibly);
        thread0.start();
        thread1.start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread1.interrupt();
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "嘗試獲取鎖");
        try {
            lock.lockInterruptibly();
            try {
                System.out.println(Thread.currentThread().getName() + "獲取到了鎖");
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + "睡眠期間被中斷了");
            } finally {
                lock.unlock();
                System.out.println(Thread.currentThread().getName() + "釋放了鎖");
            }
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + "獲得鎖期間被中斷了");
        }
    }
}

輸出結果1

Thread-1嘗試獲取鎖
Thread-0嘗試獲取鎖
Thread-1獲取到了鎖
Thread-1睡眠期間被中斷了
Thread-1釋放了鎖
Thread-0獲取到了鎖
Thread-0釋放了鎖

輸出結果2

Thread-0嘗試獲取鎖
Thread-1嘗試獲取鎖
Thread-0獲取到了鎖
Thread-1獲得鎖期間被中斷了
Thread-0釋放了鎖
unlock 解鎖
1.4 可見性保證

可見性
happens-before
因為jvm會對代碼進行編譯優(yōu)化,指令會出現(xiàn)重排序的情況,為了避免編譯優(yōu)化對并發(fā)編程安全性的影響,需要happens-before規(guī)則定義一些禁止編譯優(yōu)化的場景,保證并發(fā)編程的正確性。

public class VolatileExample {
    int x = 0 ;
    volatile boolean v = false;
    public void writer(){
        x = 42;
        v = true;
    }

    public void reader(){
        if (v == true){
            // 這里x會是多少呢
        }
    }
}

拋出問題:假設有兩個線程A和B,A執(zhí)行了writer方法,B執(zhí)行reader方法,那么B線程中獨到的變量x的值會是多少呢?

jdk1.5之前,線程B讀到的變量x的值可能是0,也可能是42,jdk1.5之后,變量x的值就是42了。原因是jdk1.5中,對volatile的語義進行了增強。來看一下happens-before規(guī)則在這段代碼中的體現(xiàn)。
jdk1.5之前,線程B讀到的變量x的值可能是0,也可能是42,jdk1.5之后,變量x的值就是42了。原因是jdk1.5中,對volatile的語義進行了增強。來看一下happens-before規(guī)則在這段代碼中的體現(xiàn)。

1. 規(guī)則一:程序的順序性規(guī)則

一個線程中,按照程序的順序,前面的操作happens-before后續(xù)的任何操作。

對于這一點,可能會有疑問。順序性是指,我們可以按照順序推演程序的執(zhí)行結果,但是編譯器未必一定會按照這個順序編譯,但是編譯器保證結果一定==順序推演的結果。

2. 規(guī)則二:volatile規(guī)則

對一個volatile變量的寫操作,happens-before后續(xù)對這個變量的讀操作。

3. 規(guī)則三:傳遞性規(guī)則

如果A happens-before B,B happens-before C,那么A happens-before C。

jdk1.5的增強就體現(xiàn)在這里。回到上面例子中,線程A中,根據(jù)規(guī)則一,對變量x的寫操作是happens-before對變量v的寫操作的,根據(jù)規(guī)則二,對變量v的寫操作是happens-before對變量v的讀操作的,最后根據(jù)規(guī)則三,也就是說,線程A對變量x的寫操作,一定happens-before線程B對v的讀操作,那么線程B在注釋處讀到的變量x的值,一定是42.

4.規(guī)則四:管程中的鎖規(guī)則

對一個鎖的解鎖操作,happens-before后續(xù)對這個鎖的加鎖操作。

這一點不難理解。

5.規(guī)則五:線程start()規(guī)則

主線程A啟動線程B,線程B中可以看到主線程啟動B之前的操作。也就是start() happens before 線程B中的操作。

6.規(guī)則六:線程join()規(guī)則

主線程A等待子線程B完成,當子線程B執(zhí)行完畢后,主線程A可以看到線程B的所有操作。也就是說,子線程B中的任意操作,happens-before join()的返回。

2.鎖的分類

image.png

3.樂觀鎖和悲觀鎖

3.1 為什么會誕生非互斥同步鎖(樂觀鎖)
互斥同步鎖(悲觀鎖)的劣勢
  • 阻塞和喚醒帶來的性能劣勢
  • 永久阻塞:如果持有鎖的線程被永久阻塞,比如遇到了無限循環(huán)、死鎖等活躍性問題,那么等待該線程釋放鎖的線程,將永遠也得不到執(zhí)行。
  • 優(yōu)先級反轉 被阻塞的優(yōu)先級高的線程等待優(yōu)先級低的線程
3.2 什么是樂觀鎖和悲觀鎖

從是否鎖住資源的角度分類

  • 悲觀鎖

如果不鎖住資源,被人就回來爭搶,就會造成數(shù)據(jù)結果錯誤,所以每次悲觀鎖為了確保結果的正確性,會在每次獲取并修改數(shù)據(jù)時,把數(shù)據(jù)鎖住,讓別人無法訪問數(shù)據(jù),這樣就就可以保證數(shù)據(jù)內容萬無一失。
Java中悲觀鎖的實現(xiàn)就是synchronized和Lock相關類

  • 樂觀鎖

認為自己在處理操作的時候不會有其它線程的來干擾,所以并不會鎖住被操作對象
在更新的時候,去對比在我修改的期間數(shù)據(jù)有沒有被其他人改變過:
如果沒有改變過,就去正常修改數(shù)據(jù)。
如果數(shù)據(jù)和我一開始拿到的不一樣,就不去修改數(shù)據(jù),會選擇放棄、報錯、重試等策略。
樂觀鎖的實現(xiàn)一般都是利用CAS算法來實現(xiàn)的。
樂觀鎖Java的典型例子就是原子類、并發(fā)容器等。
樂觀鎖的其它例子:
1.Git
2.update set num = 2,version=version+1where version=1 and id = 5;

3.3 開銷對比
  • 悲觀鎖的原始開銷要高于樂觀鎖,但是特點是一勞永逸,臨界區(qū)持鎖時間就算越來越差,也不會對互斥鎖的開銷造成影響
  • 樂觀鎖一開始的開銷比悲觀鎖小,但是如果自旋時間很長或者不停重試,那么消耗的資源也會越來越多。
臨界區(qū)表示一種公共資源或共享數(shù)據(jù),可以被多個線程使用。但是每一次只能有一個線程使用它。一旦臨界區(qū)資源被占用,想使用該資源的其他線程必須等待。在java中通常使用下面的方式來實現(xiàn)
synchronized(syncObject) { 
    //critical section
}
3.4 使用場景

悲觀鎖:適合并發(fā)寫入多的情況,適用于臨界區(qū)持鎖時間比較長的情況,悲觀鎖可以避免大量的無用自旋等消耗,典型情況:

  1. 臨界區(qū)有IO操作
  2. 臨界區(qū)代碼復雜或者循環(huán)量大
  3. 臨界區(qū)競爭非常激烈
    樂觀鎖:適合并發(fā)寫入少,大部分是讀取的場景,不加鎖能讓讀取性能大幅提高。

4.可重入鎖和非可重入鎖

非可重入鎖

所謂不可重入鎖,即若當前線程執(zhí)行某個方法已經(jīng)獲取了該鎖,那么在方法中嘗試再次獲取鎖時,就會獲取不到被阻塞。
我們嘗試設計一個不可重入鎖:

public class Lock{
    private boolean isLocked = false;
    public synchronized void lock() throws InterruptedException{
        while(isLocked){
            wait();
        }
        isLocked = true;
    }
    public synchronized void unlock(){
        isLocked = false;
        notify();
    }
}
public class LockTest {
    Lock lock = new Lock();
    public void methodA() {
        try {
            lock.lock();
            methodB();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

        System.out.println("methodA 執(zhí)行完了");
    }

    public void methodB() throws InterruptedException {

        try {
            lock.lock();
            System.out.println("methodB 正在執(zhí)行...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

    public static void main(String[] args)  {
        LockTest lockTest = new LockTest();
        lockTest.methodA();
    }

}

發(fā)生死鎖,結果無輸出

可重入鎖

可重入鎖,指的是以線程為單位,當一個線程獲取對象鎖之后,這個線程可以再次獲取本對象上的鎖,而其他的線程是不可以的。
synchronized 和 ReentrantLock 都是可重入鎖。
將前面

public class LockTest {
    ReentrantLock lock = new ReentrantLock ();
    public void methodA() {
        try {
            lock.lock();
            methodB();
        } finally {
            lock.unlock();
        }

        System.out.println("methodA 執(zhí)行完了");
    }

    public void methodB()  {

        try {
            lock.lock();
            System.out.println("methodB 正在執(zhí)行...");
        }  finally {
            lock.unlock();
        }

    }

    public static void main(String[] args)  {
        LockTest lockTest = new LockTest();
        lockTest.methodA();
    }

}
methodB 正在執(zhí)行...
methodA 執(zhí)行完了

查看線程得到鎖的個數(shù)

public class GetHoldCount {
    private  static ReentrantLock lock =  new ReentrantLock();

    public static void main(String[] args) {
        System.out.println(lock.getHoldCount());
        lock.lock();
        System.out.println(lock.getHoldCount());
        lock.lock();
        System.out.println(lock.getHoldCount());
        lock.lock();
        System.out.println(lock.getHoldCount());
        lock.unlock();
        System.out.println(lock.getHoldCount());
        lock.unlock();
        System.out.println(lock.getHoldCount());
        lock.unlock();
        System.out.println(lock.getHoldCount());
    }
}
0
1
2
3
2
1
0
public class RecursionDemo {

    private static ReentrantLock lock = new ReentrantLock();

    private static void accessResource() {
        lock.lock();
        try {
            System.out.println("已經(jīng)對資源進行了處理");
            if (lock.getHoldCount()<5) {
                System.out.println(lock.getHoldCount());
                accessResource();
                System.out.println(lock.getHoldCount());
            }
        } finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) {
        accessResource();
    }
}
可重入鎖優(yōu)點
  • 避免死鎖
  • 提升代碼封裝性
源碼實現(xiàn)

可重入鎖ReentrantLock


image.png

非可重入鎖ThreadPoolExecutor得Worker類


image.png
ReentrantLock的其他方法介紹
  • isheldbycurrentthread():查詢當前線程是否保持此鎖。
  • getQueueLength():返回正等待獲取此鎖定線程數(shù),如果一共開啟了5個線程,一個線程執(zhí)行了await()方法,那么在調用此方法是,返回4,說明此時正有4個線程在等待鎖的釋放。

5.公平鎖和非公平鎖

5.1什么是公平和非公平
  • 公平指的是按照線程請求的順序,來分配鎖;非公平指的是,不完全按照請求的順序,在一定的情況下,可以插隊。
  • 注意:非公平也同樣不提倡“插隊”行為,這里的非公平,指的是“在合適的時機”插隊,而不是盲目插隊。
公平鎖
public class Service {

    private ReentrantLock lock;

    public Service(boolean isFair) {
        super();
        lock = new ReentrantLock(isFair);
    }

    public void serviceMethod() {
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName()
                    + "獲取鎖定");
        } finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) {
        final Service service = new Service(true);
        Runnable runnable = new Runnable() {

            @Override
            public void run() {
                System.out.println("線程" + Thread.currentThread().getName()
                        + "運行了");
                service.serviceMethod();
            }
        };
        Thread[] threadArray = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threadArray[i] = new Thread(runnable);
        }
        for (int i = 0; i < 10; i++) {
            threadArray[i].start();
        }
    }
}
線程Thread-0運行了
線程Thread-1運行了
Thread-0獲取鎖定
線程Thread-4運行了
Thread-1獲取鎖定
Thread-4獲取鎖定
線程Thread-5運行了
Thread-5獲取鎖定
線程Thread-8運行了
Thread-8獲取鎖定
線程Thread-2運行了
Thread-2獲取鎖定
線程Thread-3運行了
Thread-3獲取鎖定
線程Thread-6運行了
Thread-6獲取鎖定
線程Thread-9運行了
Thread-9獲取鎖定
線程Thread-7運行了
Thread-7獲取鎖定
非公平

上述代碼true 修改為false

final Service service = new Service(false);
線程Thread-0運行了
線程Thread-4運行了
線程Thread-5運行了
線程Thread-1運行了
Thread-0獲取鎖定
Thread-4獲取鎖定
線程Thread-2運行了
線程Thread-3運行了
Thread-5獲取鎖定
線程Thread-8運行了
Thread-8獲取鎖定
Thread-1獲取鎖定
Thread-2獲取鎖定
線程Thread-7運行了
Thread-3獲取鎖定
Thread-7獲取鎖定
線程Thread-9運行了
線程Thread-6運行了
Thread-9獲取鎖定
Thread-6獲取鎖定
5.2 什么要有非公平鎖
  • 為了提高效率,避免喚醒帶來的空檔期
5.3公平和非公平的優(yōu)缺點
類型 優(yōu)勢 劣勢
公平鎖 各線程公平平等,每個線程在等待一段時間后,總有執(zhí)行的機會 更慢,吞吐量更小
不公平鎖 更快,吞吐量更大 有可能產生線程饑餓,也就是某些線程在長時間內,始終得不到執(zhí)行
image.png
image.png

6.共享鎖和排它鎖:以ReentrantReadWriteLock讀寫鎖

排它鎖,又稱為獨占鎖,獨享鎖
共享鎖,又稱為讀鎖,獲得共享鎖之后,可以查看但無法修改和刪除數(shù)據(jù),其他線程此時也可以獲取到共享鎖,也可以查看單無法修改和刪除數(shù)據(jù)

共享鎖和排它鎖的典型是讀寫鎖ReentrantReadWriteLock,其中讀鎖是共享鎖,寫鎖是獨享鎖。

6.1讀寫鎖的作用
  • 在沒有讀寫鎖之前,我們假設使用ReentrantLock,那么雖然我們保證了線程安全,但是浪費了一定資源,多個讀操作同時進行,并沒有線程安全問題
  • 在讀的地方使用讀鎖,在寫的地方使用寫鎖,靈活控制,如果沒有寫鎖的情況下,讀是阻塞的,提高了程序的執(zhí)行效率。
6.2讀寫鎖的規(guī)則
  • 多個線程只申請讀鎖,都可以申請到
  • 如果有一個線程已經(jīng)占有了讀鎖,則此時其它線程如果要申請寫鎖,則申請寫鎖的線程會一直等待釋放讀鎖
  • 如果有一個線程已經(jīng)占有了寫鎖,則此時其它線程如果要申請寫鎖或者讀鎖,則申請寫鎖的線程會一直等待釋放寫鎖
    總結:要么一個或多個線程同時有讀鎖,要么是一個線程有寫鎖,但是兩者不會同時出現(xiàn)。

讀寫鎖只是一把鎖,可以通過兩種方式鎖定:讀鎖定和寫鎖定。讀寫鎖可以同時被一個或多個線程讀鎖定,也可以被單一線程寫鎖定。但是永遠不能同時對這把鎖進行讀鎖定和寫鎖定。

ReentrantReadWriteLock用法
public class ReadWrite {

    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    private static void read() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到了讀鎖,正在讀取");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "釋放讀鎖");
            readLock.unlock();
        }
    }

    private static void write() {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到了寫鎖,正在寫入");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "釋放寫鎖");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) {
        new Thread(()->read(),"Thread1").start();
        new Thread(()->read(),"Thread2").start();
        new Thread(()->write(),"Thread3").start();
        new Thread(()->write(),"Thread4").start();
    }
}
Thread1得到了讀鎖,正在讀取
Thread2得到了讀鎖,正在讀取
Thread1釋放讀鎖
Thread2釋放讀鎖
Thread3得到了寫鎖,正在寫入
Thread3釋放寫鎖
Thread4得到了寫鎖,正在寫入
Thread4釋放寫鎖

可以同時讀,不能同時寫

6.3讀鎖和寫鎖的交互方式

讀鎖插隊策略
公平鎖:不允許插隊
非公平鎖下:假設線程2和線程4正在同時讀取,線程3想要寫入,拿不到鎖,于是進入等待隊列,線程5不在隊列里,現(xiàn)在過來想要讀取。此時有2中策略:
策略1:讀可以插隊

  • 效率高
  • 容易造成饑餓

策略2:避免饑餓
1.寫鎖可以隨時插隊
2.讀鎖僅在等待隊列頭結不是想獲取寫鎖的線程的時候可以插隊。

ReentrantReadWriteLock 選擇了策略2
6.4ReentrantReadWriteLock源碼
公平鎖的情況
image.png
非公平鎖的情況
image.png
public class ReadWriteQueue {

    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false);
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    private static void read() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到了讀鎖,正在讀取");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "釋放讀鎖");
            readLock.unlock();
        }
    }

    private static void write() {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到了寫鎖,正在寫入");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "釋放寫鎖");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) {
        new Thread(()->write(),"Thread1").start();
        new Thread(()->read(),"Thread2").start();
        new Thread(()->read(),"Thread3").start();
        new Thread(()->write(),"Thread4").start();
        new Thread(()->read(),"Thread5").start();
    }
}
Thread1得到了寫鎖,正在寫入
Thread1釋放寫鎖
Thread2得到了讀鎖,正在讀取
Thread3得到了讀鎖,正在讀取
Thread2釋放讀鎖
Thread3釋放讀鎖
Thread4得到了寫鎖,正在寫入
Thread4釋放寫鎖
Thread5得到了讀鎖,正在讀取
Thread5釋放讀鎖

非公平鎖:讀鎖僅在等待隊列頭結不是想獲取寫鎖的線程的時候可以插隊
代碼驗證如下

public class NonfairBargeDemo {

    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(
            false);

    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    private static void read() {
        System.out.println(Thread.currentThread().getName() + "開始嘗試獲取讀鎖");
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到讀鎖,正在讀取");
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } finally {
            System.out.println(Thread.currentThread().getName() + "釋放讀鎖");
            readLock.unlock();
        }
    }

    private static void write() {
        System.out.println(Thread.currentThread().getName() + "開始嘗試獲取寫鎖");
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到寫鎖,正在寫入");
            try {
                Thread.sleep(40);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } finally {
            System.out.println(Thread.currentThread().getName() + "釋放寫鎖");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) {
        new Thread(()->write(),"Thread1").start();
        new Thread(()->read(),"Thread2").start();
        new Thread(()->read(),"Thread3").start();
        new Thread(()->write(),"Thread4").start();
        new Thread(()->read(),"Thread5").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                Thread thread[] = new Thread[1000];
                for (int i = 0; i < 1000; i++) {
                    thread[i] = new Thread(() -> read(), "子線程創(chuàng)建的Thread" + i);
                }
                for (int i = 0; i < 1000; i++) {
                    thread[i].start();
                }
            }
        }).start();
    }
}
image.png
6.4鎖的升降級

同一個線程中,在沒有釋放讀鎖的情況下,就去申請寫鎖,這屬于鎖升級,ReentrantReadWriteLock是不支持的。
同一個線程中,在沒有釋放寫鎖的情況下,就去申請讀鎖,這屬于鎖降級,ReentrantReadWriteLock是支持的

public class Upgrading {

    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(
            false);
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    private static void readUpgrading() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到了讀鎖,正在讀取");
            Thread.sleep(1000);
            System.out.println("升級會帶來阻塞");
            writeLock.lock();
            System.out.println(Thread.currentThread().getName() + "獲取到了寫鎖,升級成功");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "釋放讀鎖");
            readLock.unlock();
        }
    }

    private static void writeDowngrading() {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到了寫鎖,正在寫入");
            Thread.sleep(1000);
            readLock.lock();
            System.out.println("在不釋放寫鎖的情況下,直接獲取讀鎖,成功降級");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            readLock.unlock();
            System.out.println(Thread.currentThread().getName() + "釋放寫鎖");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("先演示降級是可以的");
        Thread thread1 = new Thread(() -> writeDowngrading(), "Thread1");
        thread1.start();
        thread1.join();
        System.out.println("------------------");
        System.out.println("演示升級是不行的");
        Thread thread2 = new Thread(() -> readUpgrading(), "Thread2");
        thread2.start();
    }
}
先演示降級是可以的
Thread1得到了寫鎖,正在寫入
在不釋放寫鎖的情況下,直接獲取讀鎖,成功降級
Thread1釋放寫鎖
------------------
演示升級是不行的
Thread2得到了讀鎖,正在讀取
升級會帶來阻塞
ReentrantReadWriteLock 能降級不能升級
鎖降級中讀鎖的獲取是否必要呢?

答案是必要的。主要是為了保證數(shù)據(jù)的可見性,如果當前線程不獲取讀鎖而是直接釋放寫鎖, 假設此刻另一個線程(記作線程T)獲取了寫鎖并修改了數(shù)據(jù),那么當前線程無法感知線程T的數(shù)據(jù)更新。如果當前線程獲取讀鎖,即遵循鎖降級的步驟,則線程T將會被阻塞,直到當前線程使用數(shù)據(jù)并釋放讀鎖之后,線程T才能獲取寫鎖進行數(shù)據(jù)更新。

public class DegradeLock {
    private int value;
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private Lock readLock = lock.readLock();
    private Lock writeLock = lock.writeLock();

    public void writeRead(){
        try{
            Thread.sleep(300);
        }  catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.writeLock.lock();//1
        this.value++; //2
        //this.readLock.lock(); //3
        this.writeLock.unlock();//4

        System.out.printf("%s 讀取到value的值為 %d \n",Thread.currentThread().getName(),this.value);
        // this.readLock.unlock();//5
    }

    public static void main(String[] args) {
        DegradeLock dl = new DegradeLock();
        new Thread(()->dl.writeRead()).start();
        new Thread(()->dl.writeRead()).start();
        new Thread(()->dl.writeRead()).start();
        new Thread(()->dl.writeRead()).start();
        new Thread(()->dl.writeRead()).start();
    }
}
image.png

可以看到,出現(xiàn)并發(fā)問題了。

把代碼中的注釋打開之后,問題解決。

原因是3處又用讀鎖鎖了一次,4處雖然釋放了鎖,由于又加了3處的鎖,讀寫鎖互斥,其他線程仍然無法進入2處修改值,只有到5處釋放了鎖,其它線程才能競爭1處的鎖。

6.5適合場景

ReentrantReadWriteLock適用于讀多寫少的情況,合理使用可以進一步提高并發(fā)效率。

7.自旋鎖和阻塞鎖

阻塞或喚醒一個Java線程需要操作系統(tǒng)切換CPU狀態(tài)來完成,這種狀態(tài)轉換需要耗費處理器時間。

如果同步代碼塊中的內容過于簡單,狀態(tài)轉換消耗的時間有可能比用戶代碼執(zhí)行的時間還要長。

在許多場景中,同步資源的鎖定時間很短,為了這一小段時間去切換線程,線程掛起和恢復現(xiàn)場的花費可能會讓系統(tǒng)得不償失。
如果物理機器有多個處理器,能夠讓兩個或以上的線程同時并行執(zhí)行,我們就可以讓后面那個請求鎖的線程不放棄CPU的執(zhí)行時間,看看持有鎖的線程能否很快就釋放鎖。
而為了讓當前線程“稍等一下”,我們需讓當前線程進行自旋,如果在自旋完成后前面鎖定同步資源的線程已經(jīng)釋放了鎖,那么當前線程就可以不必阻塞而是直接獲取同步資源,從而避免切換線程的開銷。
這就是自旋。

自旋的缺點:

不能代替阻塞。雖然避免線程切換的開銷,但要占用處理器時間。如果鎖被占用的時間很短,自旋等待的效果就會非常好。反之,如果鎖被長時間占用,那么自旋的線程只會浪費處理器資源。
所以,自旋等待的時間必須有一定的限度,如果自旋超過了限度次數(shù)(默認是10次,可以使用-XX:PreBlockSpin來更改)沒有成功獲得鎖,就應當掛起線程。
自旋鎖的實現(xiàn)原理同樣也是CAS,AtomInteger中調用unsafe進行自增操作的源碼中do-while循環(huán)就是一個自旋操作,如果修改數(shù)值失敗則通過循環(huán)來執(zhí)行自旋,直至修改成功。

public class SpinLock {

    private AtomicReference<Thread> sign = new AtomicReference<>();

    public void lock() {
        Thread current = Thread.currentThread();
        while (!sign.compareAndSet(null, current)) {
            System.out.println("自旋獲取失敗,再次嘗試");
        }
    }

    public void unlock() {
        Thread current = Thread.currentThread();
        sign.compareAndSet(current, null);
    }

    public static void main(String[] args) {
        SpinLock spinLock = new SpinLock();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "開始嘗試獲取自旋鎖");
                spinLock.lock();
                System.out.println(Thread.currentThread().getName() + "獲取到了自旋鎖");
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    spinLock.unlock();
                    System.out.println(Thread.currentThread().getName() + "釋放了自旋鎖");
                }
            }
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}

自旋鎖的適用場景
自旋鎖一般用于多核的服務器,在并發(fā)度不是特別高的情況下,比阻塞鎖的效率高
自旋鎖適用于臨界區(qū)比較短小的情況,負責如果臨界區(qū)(線程一旦拿到鎖,很久以后才釋放),那也是不合適的。

8.可中斷鎖:顧名思義,就是可以響應中斷的鎖

如果某一線程A正在執(zhí)行鎖中的代碼,另一個線程B正在等待獲取該鎖,可能由于等待時間過長,線程B不想等待了,想先處理其他事情,我們可以中斷它,這就是可中斷鎖。

在Java中,synchronized就是不可中斷鎖,而Lock是可中斷鎖,因為tryLock(time)和lockInterruptibly都能響應中斷。

9.鎖優(yōu)化

9.1 Java虛擬機對鎖的優(yōu)化
  • 自旋鎖和自適應
  • 鎖消除
  • 鎖粗化
9.2 如何優(yōu)化鎖和提高并發(fā)性能
  1. 縮小同步代碼塊
  2. 盡量不要鎖住方法
  3. 減少請求鎖的次數(shù)
  4. 避免人為制造熱點
  5. 鎖中盡量不要再包含鎖
  6. 選擇合適的鎖類型或合適的工具類
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容