從本篇開始,咱們來好好梳理一下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函數的過程線程
咱們運行一下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中都有哪些關於鎖的解決方案,以及項目中所用到的實戰。