1:初始鎖這個概念

1:什麼是鎖?

在JAVA中是一個非常重要的概念,尤其是在當今的互聯網時代,高併發的場景下,更是離不開鎖。那麼鎖到底是什麼呢?在計算機科學中,鎖(lock)或互斥(mutex)是一種同步機制,用於在有許多執行線程的環境中強制對資源的訪問進行限制。鎖旨在強制實施互斥排他、併發控制策略。咱們舉一個生活的例子來更好的理解鎖:大家都去過超市買東西,如果你隨身帶了包呢,要放到儲物櫃裏。咱們把這個例子再極端一下,假如櫃子只有一個,現在同時來了3個人A,B,C,都要往這個櫃子裏放東西。這個場景就構造了一個多線程,多線程自然離不開鎖。如下圖所示:

2:不使用鎖,併發情況下會導致的問題

A,B,C都要往櫃子裏放東西,可是櫃子只能放一件東西,那怎麼辦呢?這個時候呢就引出了鎖的概念,3個人中誰搶到了櫃子的鎖,就可以使用這個櫃子,其他的人就只能等待。比如:C搶到了鎖,C可以使用這個櫃子。A和B只能等待,等C使用完了,釋放鎖以後,A和B再爭搶鎖,誰搶到了,在繼續使用櫃子

將上面的場景用代碼的方式進行實現

package com.bfxy.esjob.lock;

/**
 * @Author: qiuj
 * @Description:    櫃子
 * @Date: 2020-06-26 15:20
 */
public class Cabinet {
    //  櫃子裏存放的數字
    private Integer number;

    public Integer getNumber() {
        return number;
    }

    public void setNumber(Integer number) {
        this.number = number;
    }
}
package com.bfxy.esjob.lock;

/**
 * @Author: qiuj
 * @Description:    用戶
 * @Date: 2020-06-26 15:20
 */
public class User {
    //  櫃子
    private Cabinet cabinet;
    //  存儲的數字
    private Integer myNumber;

    public User(Cabinet cabinet, Integer myNumber) {
        this.cabinet = cabinet;
        this.myNumber = myNumber;
    }

    public void useCabinet () {
        cabinet.setNumber(myNumber);
    }
}
package com.bfxy.esjob;

import com.bfxy.esjob.lock.Cabinet;
import com.bfxy.esjob.lock.User;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @Author: qiuj
 * @Description:
 * @Date: 2020-06-26 15:23
 */
public class Tests {
    public static void main(String[] args) {
        //  只有一個櫃子  有3個用戶
        Cabinet cabinet = new Cabinet();
        //  創建線程池
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 3; i++) {
            final Integer number = i;
            executorService.execute(() -> {
                User user = new User(cabinet,number);
                user.useCabinet();
                System.out.println("客戶的編號:" + number + "櫃子裏的數字:" + cabinet.getNumber());
            });
        }
        executorService.shutdown();
    }
}

我們仔細看一下main 函數執行的過程

  • 首先創建一個櫃子的實例,由於場景只有一個櫃子,所以我們只創建一個櫃子實例
  • 然後新建了一個線程池,線程池中有3個線程,每個線程執行一個用戶的操作
  • 再來看看每個線程具體的執行過程,新建用戶實例,傳入的是用戶使用的櫃子,我們這裏只有一個櫃子,所以傳入這個櫃子的實例,然後傳入這個用戶要存儲的數字,分別是 0、1、2 ,也分別對應着用戶A、用戶B、和用戶C
  • 再調用使用櫃子的操作,也就是向櫃子中放入要存儲的數字,然後立刻從櫃子中取出數字,並打印出來

執行main 方法的結果

客戶的編號:1櫃子裏的數字:2
客戶的編號:0櫃子裏的數字:2
客戶的編號:2櫃子裏的數字:2

再次執行一次,

客戶的編號:0櫃子裏的數字:1
客戶的編號:2櫃子裏的數字:1
客戶的編號:1櫃子裏的數字:1

從結果中我們發現 3個用戶櫃子中存儲的數字都是一樣的,這是爲什麼?

問題就出在user.useCabinet() 方法上,三個線程併發的往櫃子存放上自己的數字。
但是 number 值只佔有一塊內存,換句話說就是隻能存放一個值。那麼最早往櫃子賦值的用戶的就會被新的覆蓋
最終顯示的就是最後一個線程賦的值

3:使用鎖,解決併發情況下造成變量值前後不一致的問題

那麼在程序中如何加鎖呢?這就要使用JAVA中的一個關鍵字 --synchronized 。
synchronized 分爲 synchronized方法  和 synchronized同步代碼塊

下面我們看一下兩者的具體用法:

  • synchronized 方法,顧名思義,是把 synchronized 關鍵字寫在方法上,它表示這個方法是加了鎖的,當多個線程同時調用這個方法時,只有獲得鎖的線程才允許執行這個方法 。
    public synchronized String getTicket(){
        return "xxx";
    }

我們可以看到 getTicket() 方法加了鎖,當多個線程併發執行的時候,只有獲得鎖的線程纔可以執行,其他線程只能等待

  • 我們再來看看 synchronized 塊,synchronized 塊的語法是:
synchronized (對象鎖) {
    ......
}

synchronized 代碼塊內的代碼將上鎖,而鎖是  synchronized(對象鎖)   也就是 括號內的對象(object)  持有括號內的對象的線程才允許進行方法體

再回到我們的示例當中,如何解決number 混亂的問題呢?咱們可以在設置 storeNumber 的方法上加上鎖,這樣保證同時只有一個線程能調用這個方法。如下所示:

package com.bfxy.esjob.lock;

/**
 * @Author: qiuj
 * @Description:    櫃子
 * @Date: 2020-06-26 15:20
 */
public class Cabinet {
    //  櫃子裏存放的數字
    private Integer number;

    public Integer getNumber() {
        return number;
    }

    public synchronized void setNumber(Integer number) {
        this.number = number;
    }
}

我們在set方法上加了 synchronized 關鍵字,這樣在賦值數字的時候就只會讓有鎖的線程進入,就不會並行的去執行了,而是哪個用戶搶到鎖,哪個用戶執行存儲數字的方法。我們在運行一下main 方法,看看運行的結果:

客戶的編號:1櫃子裏的數字:2
客戶的編號:0櫃子裏的數字:2
客戶的編號:2櫃子裏的數字:2

咦?! 結果還是混亂的,爲什麼?我再檢查一下代碼:

            executorService.execute(() -> {
                User user = new User(cabinet,number);
                user.useCabinet();
                System.out.println("客戶的編號:" + number + "櫃子裏的數字:" + cabinet.getNumber());
            });

我們可以看到 user.useCabinet() 和 打印方法是兩個語句,並沒有保持原子性。也就是 useCabinet() 方法內它是上鎖的,執行完方法走到 輸出語句 也是上鎖的 。但是不能確保哪個線程去執行。所以我們要保證 useCabinet() 和輸出語句是在一個原子性裏。

我們使用 synchronized() 代碼塊 ,但是 synchronized 塊裏的對象使用誰的 ? user 還是 cabinet 。user 我們在每個線程初始化的時候,都新建了一個。所以總共有3 個 ,而 cabinet 只有一個 。所以 synchronized 要使用 cabinet 

package com.bfxy.esjob;

import com.bfxy.esjob.lock.Cabinet;
import com.bfxy.esjob.lock.User;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @Author: qiuj
 * @Description:
 * @Date: 2020-06-26 15:23
 */
public class Tests {
    public static void main(String[] args) {
        //  只有一個櫃子  有3個用戶
        Cabinet cabinet = new Cabinet();
        //  創建線程池
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 3; i++) {
            final Integer number = i;
            executorService.execute(() -> {
                User user = new User(cabinet,number);
                synchronized (cabinet) {
                    user.useCabinet();
                    System.out.println("客戶的編號:" + number + "櫃子裏的數字:" + cabinet.getNumber());
                }

            });
        }
        executorService.shutdown();
    }
}

我們再去運行一下:

客戶的編號:0櫃子裏的數字:0
客戶的編號:2櫃子裏的數字:2
客戶的編號:1櫃子裏的數字:1

 由於我們加了  synchronized 塊,保證了存儲和取出的原子性 ,這樣用戶存儲的數字,和取出的數字就對應上了。不會造成混亂。最後我們通過一張圖實例整體情況

如上圖所示,線程A、線程B、 線程C 同時調用 Cabinet 類的 setNumber 方法,線程B 獲得了鎖,所以線程B 可以執行 setNumber 的方法,線程A 和 線程C 只能等待

4:總結

通過上面的場景與實例,我們可以瞭解多線程情況下,造成的變量值前後不一致的問題,以及鎖的作用。在使用鎖了之後,可以避免這種混亂的現象

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