Java中锁的解决方案
前言
在上一篇文章中,介紹了什么是鎖,以及鎖的使用場景,本文繼續給大家繼續做深入的介紹,介紹JAVA為我們提供的不同種類的鎖。
JAVA為我們提供了種類豐富的鎖,每種鎖都有不同的特性,鎖的使用場景也各不相同。由于篇幅有限,在這里只給大家介紹比較常用的幾種鎖。我會通過鎖的定義,核心代碼剖析,以及使用場景來給大家介紹JAVA中主流的幾種鎖。
樂觀鎖 與 悲觀鎖
樂觀鎖與悲觀鎖應該是每個開發人員最先接觸的兩種鎖。小編最早接觸的就是這兩種鎖,但是不是在JAVA中接觸的,而是在數據庫當中。當時的應用場景主要是在更新數據的時候,更新數據這個場景也是使用鎖的非常主要的場景之一。更新數據的主要流程如下:
- 檢索出要更新的數據,供操作人員查看;
- 操作人員更改需要修改的數值;
- 點擊保存,更新數據;
這個流程看似簡單,但是我們用多線程的思維去考慮,這也應該算是一種互聯網思維吧,就會發現其中隱藏著問題。我們具體看一下
- A檢索出數據;
- B檢索出數據;
- B修改了數據;
- A修改數據,系統會修改成功嗎?
當然啦,A修改成功與否,要看程序怎么寫。咱們拋開程序,從常理考慮,A保存數據的時候,系統要給提示,說“您修改的數據已被其他人修改過,請重新查詢確認”。那么我們程序中怎么實現呢?
- 在檢索數據,將數據的版本號(version)或者最后更新時間一并檢索出來;
- 操作員更改數據以后,點擊保存,在數據庫執行update操作
- 執行update操作時,用步驟1檢索出的版本號或者最后更新時間與數據庫中的記錄作比較;
- 如果版本號或最后更新時間一致,則可以更新;
- 如果不一致,就要給出上面的提示;
上述的流程就是樂觀鎖的實現方式。在JAVA中樂觀鎖并沒有確定的方法,或者關鍵字,它只是一個處理的流程、策略。咱們看懂上面的例子之后,再來看看JAVA中樂觀鎖。
樂觀鎖呢,它是假設一個線程在取數據的時候不會被其他線程更改數據,就像上面的例子那樣,但是在更新數據的時候會校驗數據有沒有被修改過。它是一種比較交換的機制,簡稱CAS (Compare And Swap)機制。一旦檢測到有沖突產生,也就是上面說到的版本號或者最后更新時間不一致,它就會進行重試,直到沒有沖突為止。樂觀鎖的機制如圖所示:
咱們看一下JAVA中最常用的i++,咱們思考一個問題,i++它的執行順序是什么樣子的?它是線程安全的嗎?當多個線程并發執行i++的時候,會不會有問題?接下來咱們通過程序看一下:
package cn.pottercoding.lock;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
 * @author 程序員波特
 * @since 2024年01月12日
 *
 * i++ 線程安全問題測試
 */
public class ThreadTest {
    private int i = 0;
    public static void main(String[] args) {
        ThreadTest test = new ThreadTest();
        // 線程池,50個固定線程
        ExecutorService executorService = Executors.newFixedThreadPool(50);
        CountDownLatch countDownLatch = new CountDownLatch(5000);
        for (int i = 0; i < 5000; i++) {
            executorService.execute(() -> {
                test.i++;
                countDownLatch.countDown();
            });
        }
        executorService.shutdown();
        try {
            countDownLatch.await();
            System.out.println("執行完成后,i = " + test.i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
上面的程序中,我們模擬了50個線程同時執行i++,總共執行5000次,按照常規的理解,得到的結果應該是5000,我們運行一下程序,看看執行的結果如何?
執行完成后,i=4975
執行完成后,i=4986
執行完成后,i=4971
這是運行3次以后得到的結果,可以看到每次執行的結果都不一樣,而且不是5000,這是為什么呢?這就說明i++并不是一個原子性的操作,在多線程的情況下并不安全。我們把i++的詳細執行步驟拆解一下:
- 從內存中取出i的當前值;
- 將i的值加1;
- 將計算好的值放入到內存當中;
這個流程和我們上面講解的數據庫的操作流程是一樣的。在多線程的場景下,我們可以想象一下,線程A和線程B同時從內存取出的值,假如i的值是1000,然后線程A和線程B再同時執行+1的操作,然后把值再放入內存當中,這時,內存中的值是1001,而我們期望的是1002,正是這個原因導致了上面的錯誤。那么我們如何解決呢?在JAVA1.5以后,JDK官方提供了大量的原子類,這些類的內部都是基于CAS機制的,也就是使用了樂觀鎖。我們將上面的程序稍微改造一下,如下:
package cn.pottercoding.lock;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
/**
 * @author 程序員波特
 * @since 2024年01月12日
 *
 * 原子類測試
 */
public class AtomicTest {
    private AtomicInteger i = new AtomicInteger(0);
    public static void main(String[] args) {
        AtomicTest test = new AtomicTest();
        // 線程池,50個固定線程
        ExecutorService executorService = Executors.newFixedThreadPool(50);
        CountDownLatch countDownLatch = new CountDownLatch(5000);
        for (int i = 0; i < 5000; i++) {
            executorService.execute(() -> {
                test.i.incrementAndGet();
                countDownLatch.countDown();
            });
        }
        executorService.shutdown();
        try {
            countDownLatch.await();
            System.out.println("執行完成后,i = " + test.i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
我們將變量的類型改為AtomicInteger ,AtomicInteger 是一個原子類。我們在之前調用i++的地方改成了i.incrementAndGet(),incrementAndGet()方法采用了CAS機制,也就是說使用了樂觀鎖。我們再運行一下程序,看看結果如何。
執行完成后,i=5000
執行完成后,i=5000
執行完成后,i=5000
我們同樣執行了3次,3次的結果都是5000,符合了我們預期。這個就是樂觀鎖。我們對樂觀鎖稍加總結,樂觀鎖在讀取數據的時候不做任何限制,而是在更新數據的時候,進行數據的比較,保證數據的版本一致時再更新數據。根據它的這個特點,可以看出樂觀鎖適用于讀操作多,而寫操作少的場景。
悲觀鎖與樂觀鎖恰恰相反,悲觀鎖從讀取數據的時候就顯示的加鎖,直到數據更新完成,釋放鎖為止。在這期間只能有一個線程去操作,其他的線程只能等待。在JAVA中,悲觀鎖可以使用synchronized關鍵字或者ReentrantLock類來實現。還是,上面的例子,我們分別使用這兩種方式來實現一下。首先是使用synchronized關鍵字來實現:
package cn.pottercoding.lock;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
 * @author 程序員波特
 * @since 2024年01月12日
 *
 * 使用 synchronized 關鍵字來實現自增
 */
public class SynchronizedTest {
    private int i = 0;
    public static void main(String[] args) {
        SynchronizedTest test = new SynchronizedTest();
        // 線程池,50個固定線程
        ExecutorService executorService = Executors.newFixedThreadPool(50);
        CountDownLatch countDownLatch = new CountDownLatch(5000);
        for (int i = 0; i < 5000; i++) {
            executorService.execute(() -> {
                // 修改部分,開始
                synchronized (test) {
                    test.i++;
                }
                // 修改部分結束
                countDownLatch.countDown();
            });
        }
        executorService.shutdown();
        try {
            countDownLatch.await();
            System.out.println("執行完成后,i = " + test.i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
我們唯一的改動就是增加了synchronized塊,它鎖住的對象是test,在所有線程中,誰獲得了test對象的鎖,誰才能執行i++操作。我們使用了synchronized悲觀鎖的方式,使得i++線程安全我們運行一下,看看結果如何。
執行完成后,i=5000
執行完成后,i=5000
執行完成后,i=5000
我們運行3次,結果都是5000,符合預期。接下來,我們再使用Reent rantLock類來實現悲觀鎖。代碼如下:
package cn.pottercoding.lock;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
 * @author 程序員波特
 * @since 2024年01月12日
 */
public class LockTest {
    private int i = 0;
    Lock lock = new ReentrantLock();
    public static void main(String[] args) {
        LockTest test = new LockTest();
        // 線程池,50個固定線程
        ExecutorService executorService = Executors.newFixedThreadPool(50);
        CountDownLatch countDownLatch = new CountDownLatch(5000);
        for (int i = 0; i < 5000; i++) {
            executorService.execute(() -> {
                // 修改部分,開始
                test.lock.lock();
                test.i++;
                test.lock.unlock();
                // 修改部分結束
                countDownLatch.countDown();
            });
        }
        executorService.shutdown();
        try {
            countDownLatch.await();
            System.out.println("執行完成后,i = " + test.i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
我們在類中顯示的增加了 Lock lock= new ReentrantLock();,而且在i++之前增加了 lock.lock(),加鎖操作,在i++之后增加了lock.unlock()釋放鎖的操作。我們同樣運行3次,看看結果。
執行完成后,i=5000
執行完成后,i=5000
執行完成后,i=5000
3次運行結果都是5000,完全符合預期。我們再來總結一下悲觀鎖,悲觀鎖從讀取數據的時候就加了鎖,而且在更新數據的時候,保證只有一個線程在執行更新操作,沒有像樂觀鎖那樣進行數據版本的比較。所以悲觀鎖適用于讀相對少,寫相對多的操作。
公平鎖與非公平鎖
前面我們介紹了樂觀鎖與悲觀鎖,這一小節我們將從另外一個維度去講解鎖一公平鎖與非公平鎖。從名字不難看出,公平鎖在多線程情況下,對待每一個線程都是公平的;而非公平鎖恰好與之相反。從字面上理解還是有些晦澀難懂,我們還是舉例說明,場景還是去超市買東西,在儲物柜存儲東西的例子。儲物柜只有一個,同時來了3個人使用儲物柜,這時A先搶到了柜子,A去使用,B和C自覺進行排隊。A使用完以后,后面排隊中的第一個人將繼續使用柜子,這就是公平鎖。在公平鎖當中,所有的線程都自覺排隊,一個線程執行完以后,排在后面的線程繼續使用。
非公平鎖則不然,A在使用柜子的時候,B和C并不會排隊,A使用完以后,將柜子的鑰匙往后一拋,B和C誰搶到了誰用,甚至可能突然跑來一個D,這個D搶到了鑰匙,那么D將使用柜子,這個就是非公平鎖。
公平鎖如圖所示:
多個線程同時執行方法,線程A搶到了鎖,A可以執行方法。其他線程則在隊列里進行排隊,A執行完方法后,會從隊列里取出下一個線程B,再去執行方法。以此類推,對于每一個線程來說都是公平的,不會存在后加入的線程先執行的情況。
非公平鎖入下圖所示:
多個線程同時執行方法,線程A搶到了鎖,A可以執行方法。其他的線程并沒有排隊,A執行完方法,釋放鎖后,其他的線程誰搶到了鎖,誰去執行方法。會存在后加入的線程,反而先搶到鎖的情況。
公平鎖與非公平鎖都在ReentrantLock類里給出了實現,我們看一下 ReentrantLock的源碼。
ReentrantLock有兩個構造方法,默認的構造方法中,sync=new NonfairSync();我們可以從字面意思看出它是一個非公平鎖。再看看第二個構造方法,它需要傳入一個參數,參數是一個布爾型true 是公平鎖,false 是非公平鎖。從上面的源碼我們可以看出sync 有兩個實現類,分別是FairSync 和NonfairSync,我們再看看獲取鎖的核心方法,首先是公平鎖FairSync 的,
然后是非公平鎖NonfairSync的,
通過對比兩個方法,我們可以看出唯一的不同之處在于!hasQueuedPredecessors()這個方法,很明顯這個方法是一個隊列,由此可以推斷,公平鎖是將所有的線程放在一個隊列中,一個線程執行完成后,從隊列中取出下一個線程,而非公平鎖則沒有這個隊列。這些都是公平鎖與非公平鎖底層的實現原理,我們在使用的時候不用追到這么深層次的代碼,只需要了解公平鎖與非公平鎖的含義,并且在調用構造方法時,傳入 true和 false即可。
總結
JAVA中鎖的種類非常多,在這一節中,我們找了非常典型的幾個鎖的類型給大家做了介紹。樂觀鎖與悲觀鎖是最基礎的,也是大家必須掌握的。大家在工作中不可避免的都要使用到樂觀鎖和悲觀鎖。從公平鎖與非公平鎖這個維度上看,大家平時使用的都是非公平鎖,這也是默認的鎖的類型。如果要使用公平鎖,大家可以在秒殺的場景下使用,在秒殺的場景下,是遵循先到先得的原則,是需要排隊的,所以這種場景下是最適合使用公平鎖的。
本文已收錄至的我的公眾號【程序員波特】,關注我,第一時間獲取我的最新動態。
總結
以上是生活随笔為你收集整理的Java中锁的解决方案的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 简易机器学习笔记(十一)opencv 简
- 下一篇: 通过 KernelUtil.dll 劫持
