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