從本篇開(kāi)始鸭蛙,我們來(lái)好好梳理一下Java開(kāi)發(fā)中的鎖,通過(guò)一些具體簡(jiǎn)單的例子來(lái)描述清楚從Java單體鎖到分布式鎖的演化流程筋岛。本篇我們先來(lái)看看什么是鎖娶视,以下老貓會(huì)通過(guò)一些日常生活中的例子也說(shuō)清楚鎖的概念。
描述
鎖在Java中是一個(gè)非常重要的概念泉蝌,在當(dāng)今的互聯(lián)網(wǎng)時(shí)代歇万,尤其在各種高并發(fā)的情況下揩晴,我們更加離不開(kāi)鎖勋陪。那么到底什么是鎖呢?在計(jì)算機(jī)中硫兰,鎖(lock)或者互斥(mutex)是一種同步機(jī)制诅愚,用于在有許多執(zhí)行線程的環(huán)境中強(qiáng)制對(duì)資源的訪問(wèn)限制。鎖可以強(qiáng)制實(shí)施排他互斥劫映、并發(fā)控制策略违孝。舉一個(gè)生活中的例子,大家都去超市買東西泳赋,如果我們帶了包的話雌桑,要放到儲(chǔ)物柜。我們?cè)侔堰@個(gè)例子極端一下祖今,假如柜子只有一個(gè)校坑,那么此時(shí)同時(shí)來(lái)了三個(gè)人A、B千诬、C都要往這個(gè)柜子里放東西耍目。那么這個(gè)場(chǎng)景就是一個(gè)多線程,多線程自然也就離不開(kāi)鎖徐绑。簡(jiǎn)單示意圖如下
A邪驮、B、C都要往柜子里面放東西傲茄,可是柜子只能存放一個(gè)東西毅访,那么怎么處理?這個(gè)時(shí)候我們就引出了鎖的概念盘榨,三個(gè)人中誰(shuí)先搶到了柜子的鎖俺抽,誰(shuí)就可以使用這個(gè)柜子,其他的人只能等待较曼。比如C搶到了鎖磷斧,C就可以使用這個(gè)柜子,A和B只能等待,等到C使用完畢之后弛饭,釋放了鎖冕末,AB再進(jìn)行搶鎖,誰(shuí)先搶到了侣颂,誰(shuí)就有使用柜子的權(quán)利档桃。
抽象成代碼
我們其實(shí)可以將以上場(chǎng)景抽象程相關(guān)的代碼模型,我們來(lái)看一下以下代碼的例子憔晒。
/**
* @author kdaddy@163.com
* @date 2020/11/2 23:13
*/
public class Cabinet {
//表示柜子中存放的數(shù)字
private int storeNumber;
public int getStoreNumber() {
return storeNumber;
}
public void setStoreNumber(int storeNumber) {
this.storeNumber = storeNumber;
}
}
柜子中存儲(chǔ)的是數(shù)字藻肄。
然后我們把3個(gè)用戶抽象成一個(gè)類,如下代碼
/**
* @author kdaddy@163.com
* @date 2020/11/7 22:03
*/
public class User {
// 柜子
private Cabinet cabinet;
// 存儲(chǔ)的數(shù)字
private int storeNumber;
public User(Cabinet cabinet, int storeNumber) {
this.cabinet = cabinet;
this.storeNumber = storeNumber;
}
// 表示使用柜子
public void useCabinet(){
cabinet.setStoreNumber(storeNumber);
}
}
在用戶的構(gòu)造方法中拒担,需要傳入兩個(gè)參數(shù)嘹屯,一個(gè)是要使用的柜子,另一個(gè)是要存儲(chǔ)的數(shù)字从撼。以上我們把柜子和用戶都已經(jīng)抽象完畢州弟,接下來(lái)我們?cè)賮?lái)寫一個(gè)啟動(dòng)類,模擬一下3個(gè)用戶使用柜子的場(chǎng)景低零。
/**
* @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+",我存儲(chǔ)的數(shù)字是:"+cabinet.getStoreNumber());
});
}
es.shutdown();
}
}
我們仔細(xì)的看一下這個(gè)main函數(shù)的過(guò)程
- 首先創(chuàng)建一個(gè)柜子的實(shí)例婆翔,由于場(chǎng)景中只有一個(gè)柜子,所以我們只創(chuàng)建了一個(gè)柜子實(shí)例掏婶。
- 然后我們新建了一個(gè)線程池啃奴,線程池中一共有三個(gè)線程,每個(gè)線程執(zhí)行一個(gè)用戶的操作雄妥。
- 再來(lái)看看每個(gè)線程具體的執(zhí)行過(guò)程最蕾,新建用戶實(shí)例,傳入的是用戶使用的柜子茎芭,我們這里只有一個(gè)柜子揖膜,所以傳入這個(gè)柜子的實(shí)例,然后傳入這個(gè)用戶所需要存儲(chǔ)的數(shù)字梅桩,分別是1,2,3壹粟,也分別對(duì)應(yīng)了用戶1,2,3。
- 再調(diào)用使用柜子的操作宿百,也就是想柜子中放入要存儲(chǔ)的數(shù)字趁仙,然后立刻從柜子中取出數(shù)字,并打印出來(lái)垦页。
我們運(yùn)行一下main函數(shù)雀费,看看得到的打印結(jié)果是什么?
我是用戶1,我存儲(chǔ)的數(shù)字是:3
我是用戶3,我存儲(chǔ)的數(shù)字是:3
我是用戶2,我存儲(chǔ)的數(shù)字是:2
從結(jié)果中痊焊,我們可以看出三個(gè)用戶在存儲(chǔ)數(shù)字的時(shí)候兩個(gè)都是3盏袄,一個(gè)是2忿峻。這是為什么呢?我們期待的應(yīng)該是每個(gè)人都能獲取不同的數(shù)字才對(duì)辕羽。其實(shí)問(wèn)題就是出在"user.useCabinet();"這個(gè)方法上逛尚,這是因?yàn)楣褡舆@個(gè)實(shí)例沒(méi)有加鎖的原因,三個(gè)用戶并行執(zhí)行刁愿,向柜子中存儲(chǔ)他們的數(shù)字绰寞,雖然3個(gè)用戶并行同時(shí)操作铣口,但是在具體賦值的時(shí)候,也是有順序的件缸,因?yàn)樽兞縮toreNumber只有一塊內(nèi)存旭蠕,storeNumber只存儲(chǔ)一個(gè)值旷坦,存儲(chǔ)最后的線程所設(shè)置的值秒梅。至于哪個(gè)線程排在最后,則完全不確定疮丛,賦值語(yǔ)句執(zhí)行完成之后辆它,進(jìn)入打印語(yǔ)句疯溺,打印語(yǔ)句取storeNumber的值并打印飒筑,這時(shí)storeNumber存儲(chǔ)的是最后一個(gè)線程鎖所設(shè)置的值协屡,3個(gè)線程取到的值有兩個(gè)是相同的,就像上面打印的結(jié)果一樣肤晓。
那么如何才能解決這個(gè)問(wèn)題认然?這就需要我們用到鎖漫萄。我們?cè)儋x值語(yǔ)句上加鎖,這樣當(dāng)多個(gè)線程(此處表示用戶)同時(shí)賦值的時(shí)候卷胯,誰(shuí)能優(yōu)先搶到這把鎖窑睁,誰(shuí)才能夠賦值挺峡,這樣保證同一個(gè)時(shí)刻只能有一個(gè)線程進(jìn)行賦值操作担钮,避免了之前的混亂的情況箫津。
那么在程序中,我們?nèi)绾渭渔i呢饼拍?
下面我們介紹一下Java中的一個(gè)關(guān)鍵字synchronized田炭。關(guān)于這個(gè)關(guān)鍵字教硫,其實(shí)有兩種用法。
-
synchronized方法茶鉴,顧名思義就是把synchronize的關(guān)鍵字寫在方法上景用,它表示這個(gè)方法是加了鎖的丛肢,當(dāng)多個(gè)線程同時(shí)調(diào)用這個(gè)方法的時(shí)候,只有獲得鎖的線程才能夠執(zhí)行蜂怎,具體如下:
public synchronized String getTicket(){ return "xxx"; }
以上我們可以看到getTicket()方法加了鎖杠步,當(dāng)多個(gè)線程并發(fā)執(zhí)行的時(shí)候氢伟,只有獲得鎖的線程才可以執(zhí)行榜轿,其他的線程只能夠等待。
-
synchronized代碼塊朵锣。如下:
synchronized (對(duì)象鎖){ …… }
我們將需要加鎖的語(yǔ)句都寫在代碼塊中,而在對(duì)象鎖的位置诚些,需要填寫加鎖的對(duì)象,它的含義是诬烹,當(dāng)多個(gè)線程并發(fā)執(zhí)行的時(shí)候砸烦,只有獲得你寫的這個(gè)對(duì)象的鎖绞吁,才能夠執(zhí)行后面的語(yǔ)句幢痘,其他的線程只能等待。synchronized塊通常的寫法是synchronized(this)家破,這個(gè)this是當(dāng)前類的實(shí)例颜说,也就是說(shuō)獲得當(dāng)前這個(gè)類的對(duì)象的鎖,才能夠執(zhí)行這個(gè)方法汰聋,此寫法等同于synchronized方法。
回到剛才的例子中马僻,我們又是如何解決storeNumber混亂的問(wèn)題呢韭邓?咱們?cè)囍诜椒ㄉ霞由湘i,這樣保證同時(shí)只有一個(gè)線程能調(diào)用這個(gè)方法女淑,具體如下。
/**
* @author kdaddy@163.com
* @date 2020/12/2 23:13
*/
public class Cabinet {
//表示柜子中存放的數(shù)字
private int storeNumber;
public int getStoreNumber() {
return storeNumber;
}
public synchronized void setStoreNumber(int storeNumber) {
this.storeNumber = storeNumber;
}
}
我們運(yùn)行一下代碼擒权,結(jié)果如下
我是用戶2,我存儲(chǔ)的數(shù)字是:2
我是用戶3,我存儲(chǔ)的數(shù)字是:2
我是用戶1,我存儲(chǔ)的數(shù)字是:1
我們發(fā)現(xiàn)結(jié)果還是混亂的袱巨,并沒(méi)有解決問(wèn)題愉老。我們檢查一下代碼
es.execute(()->{
User user = new User(cabinet,storeNumber);
user.useCabinet();
System.out.println("我是用戶"+storeNumber+",我存儲(chǔ)的數(shù)是:"+cabinet.getStoreNumber());
});
我們可以看到在useCabinet和打印的方法是兩個(gè)語(yǔ)句剖效,并沒(méi)有保持原子性嫉入,雖然在set方法上加了鎖焰盗,但是在打印的時(shí)候又存在了并發(fā),打印語(yǔ)句是有鎖的咒林,但是不能確定哪個(gè)線程去執(zhí)行熬拒。所以這里,我們要保證useCabinet和打印的方法的原子性垫竞,我們使用synchronized塊澎粟,但是synchronized塊里的對(duì)象我們使用誰(shuí)的?這又是一個(gè)問(wèn)題欢瞪,user還是cabinet?回答當(dāng)然是cabinet捌议,因?yàn)槊總€(gè)線程都初始化了user,總共有3個(gè)User對(duì)象引有,而cabinet對(duì)象只有一個(gè)瓣颅,所以synchronized要用cabine對(duì)象,具體代碼如下
/**
* @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+",我存儲(chǔ)的數(shù)字是:"+cabinet.getStoreNumber());
}
});
}
es.shutdown();
}
}
此時(shí)我們?cè)偃ミ\(yùn)行一下:
我是用戶3,我存儲(chǔ)的數(shù)字是:3
我是用戶2,我存儲(chǔ)的數(shù)字是:2
我是用戶1,我存儲(chǔ)的數(shù)字是:1
由于我們加了synchronized塊譬正,保證了存儲(chǔ)和取出的原子性宫补,這樣用戶存儲(chǔ)的數(shù)字和取出的數(shù)字就對(duì)應(yīng)上了,不會(huì)造成混亂曾我,最后我們用圖來(lái)表示一下上面例子的整體情況粉怕。
如上圖所示,線程A抒巢,線程B贫贝,線程C同時(shí)調(diào)用Cabinet類的setStoreNumber方法,線程B獲得了鎖蛉谜,所以線程B可以執(zhí)行setStore的方法稚晚,線程A和線程C只能等待。
總結(jié)
通過(guò)上面的場(chǎng)景以及例子型诚,我們可以了解多線程情況下客燕,造成的變量值前后不一致的問(wèn)題,以及鎖的作用狰贯,在使用了鎖以后也搓,可以避免這種混亂的現(xiàn)象,后續(xù)涵紊,老貓會(huì)和大家介紹一個(gè)Java中都有哪些關(guān)于鎖的解決方案傍妒,以及項(xiàng)目中所用到的實(shí)戰(zhàn)。