【分佈式鎖的演化】什麼是鎖?

從本篇開始,咱們來好好梳理一下Java開發中的鎖,經過一些具體簡單的例子來描述清楚從Java單體鎖到分佈式鎖的演化流程。本篇咱們先來看看什麼是鎖,如下老貓會經過一些平常生活中的例子也說清楚鎖的概念。java

描述

鎖在Java中是一個很是重要的概念,在當今的互聯網時代,尤爲在各類高併發的狀況下,咱們更加離不開鎖。那麼到底什麼是鎖呢?在計算機中,鎖(lock)或者互斥(mutex)是一種同步機制,用於在有許多執行線程的環境中強制對資源的訪問限制。鎖能夠強制實施排他互斥、併發控制策略。舉一個生活中的例子,你們都去超市買東西,若是咱們帶了包的話,要放到儲物櫃。咱們再把這個例子極端一下,假如櫃子只有一個,那麼此時同時來了三我的A、B、C都要往這個櫃子裏放東西。那麼這個場景就是一個多線程,多線程天然也就離不開鎖。簡單示意圖以下多線程

存儲櫃子模型

A、B、C都要往櫃子裏面放東西,但是櫃子只能存放一個東西,那麼怎麼處理?這個時候咱們就引出了鎖的概念,三我的中誰先搶到了櫃子的鎖,誰就可使用這個櫃子,其餘的人只能等待。好比C搶到了鎖,C就可使用這個櫃子,A和B只能等待,等到C使用完畢以後,釋放了鎖,AB再進行搶鎖,誰先搶到了,誰就有使用櫃子的權利。併發

抽象成代碼

咱們其實能夠將以上場景抽象程相關的代碼模型,咱們來看一下如下代碼的例子。分佈式

/**
 * @author kdaddy@163.com
 * @date 2020/11/2 23:13
 */
public class Cabinet {
    //表示櫃子中存放的數字
    private int storeNumber;

    public int getStoreNumber() {
        return storeNumber;
    }
    public void setStoreNumber(int storeNumber) {
        this.storeNumber = storeNumber;
    }
}

櫃子中存儲的是數字。函數

而後咱們把3個用戶抽象成一個類,以下代碼高併發

/**
 * @author kdaddy@163.com
 * @date 2020/11/7 22:03
 */
public class User {
    // 櫃子
    private Cabinet cabinet;
    // 存儲的數字
    private int storeNumber;

    public User(Cabinet cabinet, int storeNumber) {
        this.cabinet = cabinet;
        this.storeNumber = storeNumber;
    }
    // 表示使用櫃子
    public void useCabinet(){
        cabinet.setStoreNumber(storeNumber);
    }
}

在用戶的構造方法中,須要傳入兩個參數,一個是要使用的櫃子,另外一個是要存儲的數字。以上咱們把櫃子和用戶都已經抽象完畢,接下來咱們再來寫一個啓動類,模擬一下3個用戶使用櫃子的場景。this

/**
 * @author kdaddy@163.com
 * @date 2020/11/7 22:05
 */
public class Starter {
    public static void main(String[] args) {
        final Cabinet cabinet = new Cabinet();
        ExecutorService es = Executors.newFixedThreadPool(3);

        for(int i= 1; i < 4; i++){
            final int storeNumber = i;
            es.execute(()->{
                User user = new User(cabinet,storeNumber);
                user.useCabinet();
                System.out.println("我是用戶"+storeNumber+",我存儲的數字是:"+cabinet.getStoreNumber());
            });
        }
        es.shutdown();
    }
}

咱們仔細的看一下這個main函數的過程線程

  • 首先建立一個櫃子的實例,因爲場景中只有一個櫃子,因此咱們只建立了一個櫃子實例。
  • 而後咱們新建了一個線程池,線程池中一共有三個線程,每一個線程執行一個用戶的操做。
  • 再來看看每一個線程具體的執行過程,新建用戶實例,傳入的是用戶使用的櫃子,咱們這裏只有一個櫃子,因此傳入這個櫃子的實例,而後傳入這個用戶所須要存儲的數字,分別是1,2,3,也分別對應了用戶1,2,3。
  • 再調用使用櫃子的操做,也就是想櫃子中放入要存儲的數字,而後馬上從櫃子中取出數字,並打印出來。

咱們運行一下main函數,看看獲得的打印結果是什麼?code

我是用戶1,我存儲的數字是:3
我是用戶3,我存儲的數字是:3
我是用戶2,我存儲的數字是:2

從結果中,咱們能夠看出三個用戶在存儲數字的時候兩個都是3,一個是2。這是爲何呢?咱們期待的應該是每一個人都能獲取不一樣的數字纔對。其實問題就是出在"user.useCabinet();"這個方法上,這是由於櫃子這個實例沒有加鎖的緣由,三個用戶並行執行,向櫃子中存儲他們的數字,雖然3個用戶並行同時操做,可是在具體賦值的時候,也是有順序的,由於變量storeNumber只有一塊內存,storeNumber只存儲一個值,存儲最後的線程所設置的值。至於哪一個線程排在最後,則徹底不肯定,賦值語句執行完成以後,進入打印語句,打印語句取storeNumber的值並打印,這時storeNumber存儲的是最後一個線程鎖所設置的值,3個線程取到的值有兩個是相同的,就像上面打印的結果同樣。對象

那麼如何才能解決這個問題?這就須要咱們用到鎖。咱們再賦值語句上加鎖,這樣當多個線程(此處表示用戶)同時賦值的時候,誰能優先搶到這把鎖,誰纔可以賦值,這樣保證同一個時刻只能有一個線程進行賦值操做,避免了以前的混亂的狀況。

那麼在程序中,咱們如何加鎖呢?

下面咱們介紹一下Java中的一個關鍵字synchronized。關於這個關鍵字,其實有兩種用法。

  • synchronized方法,顧名思義就是把synchronize的關鍵字寫在方法上,它表示這個方法是加了鎖的,當多個線程同時調用這個方法的時候,只有得到鎖的線程纔可以執行,具體以下:

    public synchronized String getTicket(){
            return "xxx";
        }

    以上咱們能夠看到getTicket()方法加了鎖,當多個線程併發執行的時候,只有得到鎖的線程才能夠執行,其餘的線程只可以等待。

  • synchronized代碼塊。以下:

    synchronized (對象鎖){
        ……
    }

    咱們將須要加鎖的語句都寫在代碼塊中,而在對象鎖的位置,須要填寫加鎖的對象,它的含義是,當多個線程併發執行的時候,只有得到你寫的這個對象的鎖,纔可以執行後面的語句,其餘的線程只能等待。synchronized塊一般的寫法是synchronized(this),這個this是當前類的實例,也就是說得到當前這個類的對象的鎖,纔可以執行這個方法,此寫法等同於synchronized方法。

回到剛纔的例子中,咱們又是如何解決storeNumber混亂的問題呢?我們試着在方法上加上鎖,這樣保證同時只有一個線程能調用這個方法,具體以下。

/**
 * @author kdaddy@163.com
 * @date 2020/12/2 23:13
 */
public class Cabinet {
    //表示櫃子中存放的數字
    private int storeNumber;

    public int getStoreNumber() {
        return storeNumber;
    }

    public synchronized void setStoreNumber(int storeNumber) {
        this.storeNumber = storeNumber;
    }
}

咱們運行一下代碼,結果以下

我是用戶2,我存儲的數字是:2
我是用戶3,我存儲的數字是:2
我是用戶1,我存儲的數字是:1

咱們發現結果仍是混亂的,並無解決問題。咱們檢查一下代碼

es.execute(()->{
                User user = new User(cabinet,storeNumber);
                user.useCabinet();
                System.out.println("我是用戶"+storeNumber+",我存儲的數是:"+cabinet.getStoreNumber());
            });

咱們能夠看到在useCabinet和打印的方法是兩個語句,並無保持原子性,雖然在set方法上加了鎖,可是在打印的時候又存在了併發,打印語句是有鎖的,可是不能肯定哪一個線程去執行。因此這裏,咱們要保證useCabinet和打印的方法的原子性,咱們使用synchronized塊,可是synchronized塊裏的對象咱們使用誰的?這又是一個問題,user仍是cabinet?回答固然是cabinet,由於每一個線程都初始化了user,總共有3個User對象,而cabinet對象只有一個,因此synchronized要用cabine對象,具體代碼以下

/**
 * @author kdaddy@163.com
 * @date 2020/12/7 22:05
 */
public class Starter {
    public static void main(String[] args) {
        final Cabinet cabinet = new Cabinet();
        ExecutorService es = Executors.newFixedThreadPool(3);

        for(int i= 1; i < 4; i++){
            final int storeNumber = i;
            es.execute(()->{
                User user = new User(cabinet,storeNumber);
                synchronized (cabinet){
                    user.useCabinet();
                    System.out.println("我是用戶"+storeNumber+",我存儲的數字是:"+cabinet.getStoreNumber());
                }
            });
        }
        es.shutdown();
    }
}

此時咱們再去運行一下:

我是用戶3,我存儲的數字是:3
我是用戶2,我存儲的數字是:2
我是用戶1,我存儲的數字是:1

因爲咱們加了synchronized塊,保證了存儲和取出的原子性,這樣用戶存儲的數字和取出的數字就對應上了,不會形成混亂,最後咱們用圖來表示一下上面例子的總體狀況。
最終模型

如上圖所示,線程A,線程B,線程C同時調用Cabinet類的setStoreNumber方法,線程B得到了鎖,因此線程B能夠執行setStore的方法,線程A和線程C只能等待。

總結

經過上面的場景以及例子,咱們能夠了解多線程狀況下,形成的變量值先後不一致的問題,以及鎖的做用,在使用了鎖之後,能夠避免這種混亂的現象,後續,老貓會和你們介紹一個Java中都有哪些關於鎖的解決方案,以及項目中所用到的實戰。

相關文章
相關標籤/搜索