1邑退、多線程內(nèi)存情況簡(jiǎn)析
參考:aHR0cHM6Ly93d3cuYmlsaWJpbGkuY29tL3ZpZGVvL0JWMXVKNDExazd3eT9wPTMwOQ==
同樣聲明,對(duì)于 Java 程序運(yùn)行時(shí)的內(nèi)存、JVM 等問(wèn)題换淆,可能闡述的并不會(huì)非常準(zhǔn)確,僅僅是大致上闡述以有助于更好地理解一些復(fù)雜的事情几颜。
代碼一(單線程):
public class MyThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
public class MyThreadTest {
public static void main(String[] args) {
Thread myThread = new Thread(new MyThread());
myThread.run(); // 人工直接調(diào)用 run 方法倍试,相當(dāng)于調(diào)用類中的普通的方法
}
}
此時(shí),內(nèi)存中的情形如下圖:
單線程時(shí)蛋哭,按照代碼的順序县习,main
方法在棧中開(kāi)辟空間(入棧),隨后調(diào)用的run
方法也同樣在棧中開(kāi)辟空間(入棧)谆趾。CPU 等只需對(duì)這一個(gè)棧進(jìn)行“操作”(運(yùn)行一個(gè)棧即可)躁愿。
- 代碼二(多線程):
上圖中每個(gè)棧用不同的顏色來(lái)表示。
在多線程的模型中沪蓬,一個(gè)線程需要一個(gè)單獨(dú)的棧區(qū)來(lái)支持彤钟。
當(dāng)調(diào)用start
方法的時(shí)候,JVM 會(huì)為該線程開(kāi)辟一個(gè)屬于它的棧區(qū)跷叉,由該棧區(qū)負(fù)責(zé)該線程中涉及到的方法逸雹、變量等营搅。
對(duì)于 CPU 等而言,所要做的就是基于時(shí)間片輪換梆砸、自定義等各類的規(guī)則來(lái)對(duì)每一個(gè)棧區(qū)進(jìn)行交替“操作”(交替“運(yùn)行”每一個(gè)棧區(qū))转质。最終達(dá)到多線程的并發(fā)的目的。
public class MyThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
public class MyThreadTest {
public static void main(String[] args) {
Thread myThread = new Thread(new MyThread());
myThread.start();
}
}
此時(shí)帖世,內(nèi)存中的情形如下圖:
補(bǔ)充一點(diǎn):每一個(gè)線程擁有各自的棧區(qū)休蟹,但是堆區(qū)是被所有線程共享的,所以狮暑,從這個(gè)角度出發(fā)鸡挠,需要線程同步機(jī)制來(lái)確保堆區(qū)中的數(shù)據(jù)等正確、安全搬男。
2拣展、關(guān)于創(chuàng)建線程補(bǔ)充
請(qǐng)先閱讀38、【JavaSE】【Java 核心類庫(kù)(下)】多線程(1)中的標(biāo)題號(hào)為3.2部分的內(nèi)容缔逛。
先看下面的代碼:
/* 實(shí)現(xiàn) java.lang.Runnable 接口 */
public class MyOperation implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getId() + ": " + i);
}
}
}
public class Main {
public static void main(String[] args) {
Thread thread1 = new Thread(new MyOperation());
Thread thread2 = new Thread(new MyOperation());
thread1.start();
thread2.start();
}
}
public class Main {
public static void main(String[] args) {
Runnable runnable = new MyOperation();
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
}
}
好备埃,通過(guò)比較兩個(gè)Main
類中的代碼,可以知道要探討的問(wèn)題是:在使用Thread(Runnable runnable)
構(gòu)造方法的時(shí)候褐奴,傳入同一個(gè)java.lang.Runnable
引用以及傳入不同的java.lang.Runnable
的引用按脚,有什么區(qū)別。
首先明確一點(diǎn)敦冬,在上面的兩個(gè)Main
類代碼中辅搬,不管傳入的是同一個(gè)java.lang.Runnable
引用還是不同的java.lang.Runnable
的引用,所創(chuàng)建的線程是兩個(gè)脖旱,不是說(shuō)因?yàn)閭魅氲氖峭粋€(gè)java.lang.Runnable
引用而就是一個(gè)線程堪遂,線程的個(gè)數(shù)看的是new Thread(···)
的次數(shù)。上面的代碼會(huì)印證這一點(diǎn)萌庆,因?yàn)檩敵龅臅r(shí)候加了Thread.currentThread().getId()
溶褪。
下面的問(wèn)題,就是說(shuō)践险,“傳入的引用相同與不同”是否有區(qū)別猿妈?正常情況下是沒(méi)有區(qū)別!因?yàn)閷?shí)現(xiàn)java.lang.Runnable
接口的類所定義出的可以理解為是一個(gè)“操作說(shuō)明書(shū)”巍虫,把“操作說(shuō)明書(shū)”交給java.lang.Thread
類讓其按照“說(shuō)明書(shū)”的步驟來(lái)執(zhí)行即可彭则。這樣的話,“給兩份一樣的操作說(shuō)明書(shū)”或者“只給同一份的操作說(shuō)明書(shū)”占遥,并沒(méi)有任何區(qū)別俯抖,反倒是“兩份一樣的操作說(shuō)明書(shū)”比較“浪費(fèi)資源”(多new
出了一個(gè)java.lang.Runnable
類)。
但是筷频,為什么還要在這里說(shuō)討論這個(gè)問(wèn)題蚌成,因?yàn)樵凇熬€程同步”中前痘,如果出現(xiàn)上面的情況,又會(huì)有什么出現(xiàn)問(wèn)題担忧?芹缔!
3、線程同步
3.1瓶盛、概述
之所以要有“線程同步”這樣的一個(gè)機(jī)制最欠,主要是因?yàn)榫€程之間可能會(huì)存在著資源共享的情況,即多個(gè)線程要去使用同一資源(可以表述為“共享資源”惩猫、“臨界資源”等)芝硬,那么就會(huì)出現(xiàn)數(shù)據(jù)不一致、臟數(shù)據(jù)等問(wèn)題轧房。
舉個(gè)例子:
假設(shè):銀行卡里存有500元拌阴,張三與李四都可以取出這銀行卡里面的錢,張三通過(guò) ATM 取出全部的錢奶镶,而李四想將全部的錢存入手機(jī)的支付軟件的余額中迟赃。巧合的是,兩人差不多在同一時(shí)間進(jìn)行著各自的操作厂镇。李四成功完成了操作纤壁,看到支付軟件上的余額顯示為500元,而此時(shí)捺信,張三這邊也進(jìn)行到 ATM 機(jī)正在“吐錢”酌媒,最終拿到了現(xiàn)金500元。
當(dāng)然迄靠,現(xiàn)實(shí)情況中是不會(huì)發(fā)生這樣的“好事”的秒咨,否則銀行要關(guān)門了。
但是梨水,分析一下上面所敘述的過(guò)程拭荤,兩種方式看作兩個(gè)線程茵臭,目的都是“取出同一張銀行卡中的錢”疫诽,但是兩個(gè)線程通過(guò)不同途徑的查詢方式,均得出了“余額有500元”這樣的結(jié)論旦委,然后都成功的取出奇徒。這是一個(gè)非常重大的錯(cuò)誤!
多個(gè)線程并發(fā)讀寫(xiě)同一個(gè)臨界資源時(shí)會(huì)發(fā)生線程并發(fā)安全問(wèn)題缨硝!
線程同步機(jī)制的目的就是為了在多線程并發(fā)條件下摩钙,對(duì)線程之間進(jìn)行通信與協(xié)調(diào),最終能夠正確地處理數(shù)據(jù)等查辩。
下面用代碼模擬一下上面所講的這樣的一個(gè)過(guò)程:
/* 銀行賬戶類胖笛,balance 表示賬戶中的余額 */
public class Balance {
private int balance;
public Balance() {
}
public Balance(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
}
/* 定義一個(gè)“取款”的操作 */
public class GetBalanceOperation implements Runnable {
private Balance balance; // 取款的目標(biāo)賬戶
private int account; // 取款金額
public GetBalanceOperation(Balance balance, int account) {
this.balance = balance;
this.account = account;
}
@Override
public void run() {
if (balance != null) {
int temp = balance.getBalance();
if (temp >= account && account > 0) {
temp -= account;
System.out.println("交易處理中···");
try {
Thread.sleep(300); // 一方面為了模擬真實(shí)的取款過(guò)程网持,另一方面為了使多線程所會(huì)引發(fā)的問(wèn)題更明顯
balance.setBalance(temp);
System.out.println("交易完成!");
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("余額不足长踊!");
}
}
}
}
public class Main {
public static void main(String[] args) {
Balance balance = new Balance(500);
Runnable operation = new GetBalanceOperation(balance, 600);
Thread thread1 = new Thread(operation);
Thread thread2 = new Thread(operation);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
System.out.println("余額:" + balance.getBalance());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
最后功舀,輸出的結(jié)果是“300”,而現(xiàn)實(shí)生活中身弊,兩次取款后辟汰,最后的余額應(yīng)該是“500-200-200=100”。
為了使“多個(gè)線程并發(fā)對(duì)同一個(gè)臨界資源所會(huì)產(chǎn)生的問(wèn)題”體現(xiàn)的更明顯阱佛,上面的代碼中帖汞,特別是
run
方法的寫(xiě)法是稍微有些講究的,比如說(shuō)不直接在成員變量balance
上進(jìn)行減法凑术,setBalance
方法調(diào)用位置在sleep
方法后等翩蘸。
線程1與線程2并發(fā)執(zhí)行,由于線程1是先啟動(dòng)的淮逊,當(dāng)線程1執(zhí)行到sleep
的時(shí)候鹿鳖,此時(shí)“取款后更新余額”的方法setBalance
方法并沒(méi)有調(diào)用,而這時(shí)壮莹,線程2雖然比線程1略晚一點(diǎn)翅帜,但run
方法已經(jīng)是在執(zhí)行中了,問(wèn)題就出現(xiàn)了命满,線程1由于還沒(méi)及時(shí)“更新余額”所以線程2讀取到的“余額”仍然是“最初的余額”(實(shí)例代碼自中給的是500)涝滴,所以線程2是基于“最初的余額”進(jìn)行“取款”的。最后的局面是胶台,兩個(gè)線程中的temp
值都是“500-200=300”歼疮,所以,就出現(xiàn)了輸出的結(jié)果是“300”這樣的局面诈唬。(總結(jié):線程一執(zhí)行取款時(shí)還沒(méi)來(lái)得及將取款后的余額更新韩脏,線程二就已經(jīng)開(kāi)始取款)
3.2、線程同步機(jī)制(如何實(shí)現(xiàn)線程同步)
- 繼續(xù)用上面的“余額”代碼來(lái)探討铸磅,首先赡矢,來(lái)看一下用下面的代碼來(lái)解決“余額不正確”的問(wèn)題:
public class Main {
public static void main(String[] args) {
Balance balance = new Balance(500);
Runnable operation = new GetBalanceOperation(balance, 200);
Thread thread1 = new Thread(operation);
Thread thread2 = new Thread(operation);
thread1.start();
try {
thread1.join();
thread2.start(); // thread2 的 start 的調(diào)用位置
thread2.join();
System.out.println("余額:" + balance.getBalance());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
從代碼中也能明確地看出解決方案,就是先讓子線程1啟動(dòng)但不讓子線程2啟動(dòng)阅仔,同時(shí)讓主線程等待子線程1完成后再繼續(xù)吹散,因?yàn)樽泳€程2的啟動(dòng)仍然要在主線程中,所以代碼這樣一寫(xiě)八酒,就能夠解決問(wèn)題空民。
但是,這并不是線程同步機(jī)制羞迷,這樣的做法界轩,不是不可以画饥,但是這樣的做法,是一種“用著多線程的語(yǔ)法浊猾,寫(xiě)著單線程思路的代碼”荒澡,意思就是線程的調(diào)度順序還是由代碼的編寫(xiě)順序控制的,我們多線程的目標(biāo)是与殃,在指定我們?cè)撝付ǖ恼{(diào)度規(guī)則后单山,線程的調(diào)度由系統(tǒng)自行完成,而不是主觀通過(guò)控制代碼編寫(xiě)順序?qū)崿F(xiàn)所謂的“多線程”幅疼,如果是這樣米奸,上面的例子中,兩個(gè)線程的代碼還好編寫(xiě)爽篷,那如果是高訪問(wèn)量悴晰、高讀寫(xiě)等特點(diǎn)的大型系統(tǒng),那又如何去編寫(xiě)逐工!
3.2.1铡溪、使用 synchronized 關(guān)鍵字實(shí)現(xiàn)線程同步
使用
synchronized
關(guān)鍵字來(lái)實(shí)現(xiàn)“同步鎖”(有的地方稱“對(duì)象鎖”、“同步監(jiān)視器”等)機(jī)制從而保證線程在某一執(zhí)行階段的原子性泪喊。原子性棕硫,所謂原子性是指不可分割的一系列操作指令,在執(zhí)行完畢前不會(huì)被任何其他操作中斷袒啼,要么全部執(zhí)行哈扮,要么全部不執(zhí)行。
“鎖”蚓再,這個(gè)字在多線程體系中會(huì)經(jīng)常見(jiàn)到滑肉。多線程場(chǎng)景下,會(huì)出現(xiàn)資源競(jìng)爭(zhēng)等摘仅,需要對(duì)部分代碼等進(jìn)行“加鎖”靶庙,進(jìn)而達(dá)到某個(gè)線程可以短暫“獨(dú)自占有、使用娃属、操作”的相應(yīng)的資源這一目的六荒。
3.2.1.1、synchronized 代碼塊
使用
synchronized
修飾代碼塊(也稱“同步代碼塊”)膳犹,即表示線程可以對(duì)這部分代碼“加鎖”恬吕∏┰颍“加鎖”意味著線程在執(zhí)行這部分代碼的時(shí)候具有原子性须床,也就是說(shuō),要執(zhí)行就必須執(zhí)行完渐裂,中間不能被打斷(某一線程執(zhí)行“加鎖”的代碼時(shí)豺旬,其他的線程是不可能再同時(shí)執(zhí)行這段被“加鎖”的代碼钠惩,其他線程將會(huì)被變?yōu)椤?strong>阻塞”狀態(tài))。語(yǔ)法格式如下:
synchronized(類類型的引用) {
編寫(xiě)所有需要鎖定的代碼族阅;
}
- 下面篓跛,使用
synchronized
代碼塊來(lái)解決“多線程取款”問(wèn)題:
/* 銀行賬戶類,表示一個(gè)銀行賬戶坦刀,balance 為賬戶中的金額 */
public class Balance {
private int balance;
public Balance() {
}
public Balance(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
}
/* 定義一個(gè)類愧沟,由該類的實(shí)例將來(lái)作為“鎖” */
public class GetBalanceLock {
}
public class GetBalanceOperation implements Runnable {
private Balance balance;
private int account;
private GetBalanceLock lock = new GetBalanceLock(); // “鎖”
public GetBalanceOperation(Balance balance, int account) {
this.balance = balance;
this.account = account;
}
@Override
public void run() {
synchronized (lock) {
if (balance != null) {
int temp = balance.getBalance();
if (temp >= account && account > 0) {
temp -= account;
System.out.println("交易處理中···");
try {
Thread.sleep(300);
balance.setBalance(temp);
System.out.println("交易完成!");
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("余額不足鲤遥!");
}
}
}
}
}
public class Main {
public static void main(String[] args) {
Balance balance = new Balance(500);
Runnable operation = new GetBalanceOperation(balance, 200);
Thread thread1 = new Thread(operation);
Thread thread2 = new Thread(operation);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
System.out.println("余額:" + balance.getBalance());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
交易處理中···
交易完成沐寺!
交易處理中···
交易完成!
余額:100
下面討論一下
synchronized
括號(hào)中的“鎖”盖奈,“鎖”的要求是混坞,如果希望多個(gè)線程在執(zhí)行到synchronized
代碼塊的時(shí)候能夠?qū)崿F(xiàn)“臨時(shí)串行”,那么就要求這多個(gè)線程都用的是同一個(gè)“鎖”對(duì)象钢坦,也就是這個(gè)過(guò)程中必須保證“鎖”對(duì)象的唯一性究孕。可以將
synchronized
代碼塊想象成“衣服店里的試衣間”,而“鎖”對(duì)應(yīng)的就是“試衣間門上的鎖”爹凹。
“衣服店里的試衣間”要是沒(méi)有人的話是一直開(kāi)放的厨诸。顧客想試穿衣服,如果試衣間沒(méi)有人禾酱,可以直接進(jìn)去使用泳猬,然后從門的內(nèi)側(cè)把門鎖上,這樣試衣間就暫時(shí)歸這位顧客使用宇植、其他想試穿的顧客就無(wú)法進(jìn)入了得封;等這位顧客試穿完后,將試衣間的門鎖重新從里面打開(kāi)指郁,其他顧客才能進(jìn)去試穿忙上。
某個(gè)線程獲取到 CPU 等資源(想要執(zhí)行synchronized
代碼塊的大前提)并要開(kāi)始執(zhí)行synchronized
代碼塊中的內(nèi)容時(shí):首先需要“判斷鎖”(試衣間是否有人);可行的話再去“擁有(占有)鎖”(如果試衣間沒(méi)有人闲坎,可以進(jìn)入試衣間疫粥,從門內(nèi)鎖門);synchronized
代碼塊執(zhí)行完畢后腰懂,“釋放鎖”(從試衣間開(kāi)門出來(lái))梗逮。
判斷鎖(“判斷試衣間是否有人”可以表述為“判斷鎖”)
-->
獲取鎖(“從門內(nèi)將試衣間鎖上”可以表述為“獲取、占有绣溜、擁有鎖”)
-->
釋放鎖(“試穿完后從試衣間開(kāi)鎖出來(lái)”可以表述為“釋放鎖”)
- 如果不能在過(guò)程中保證上面所提到的“唯一性”的話慷彤,導(dǎo)致多個(gè)線程在執(zhí)行
synchronized
語(yǔ)句塊的時(shí)候不會(huì)再像“多個(gè)顧客用一個(gè)試衣間試穿衣服”那樣“串行”燕刻,synchronized
語(yǔ)句塊便是無(wú)意義的存在错负。下面看看具體的案例:
public class GetBalanceOperation implements Runnable {
private Balance balance;
private int account;
public GetBalanceOperation(Balance balance, int account) {
this.balance = balance;
this.account = account;
}
@Override
public void run() {
// synchronized 中使用 new
synchronized (new GetBalanceLock()) {
if (balance != null) {
int temp = balance.getBalance();
if (temp >= account && account > 0) {
temp -= account;
System.out.println("交易處理中···");
try {
Thread.sleep(300);
balance.setBalance(temp);
System.out.println("交易完成失乾!");
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("余額不足隧枫!");
}
}
}
}
}
public class Main {
public static void main(String[] args) {
Balance balance = new Balance(500);
Runnable operation = new GetBalanceOperation(balance, 200); // 兩個(gè)線程“共用一份操作說(shuō)明書(shū)”
Thread thread1 = new Thread(operation);
Thread thread2 = new Thread(operation);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
System.out.println("余額:" + balance.getBalance());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
交易處理中···
交易處理中···
交易完成!
交易完成跋选!
余額:300
輸出的結(jié)果很明顯涕癣,并沒(méi)有實(shí)現(xiàn)“線程同步”。其原因就是違反了之前提到的“唯一性”前标。用synchronized (new GetBalanceLock())
意味著任何一個(gè)線程執(zhí)行到這里的時(shí)候都會(huì)創(chuàng)建新的“鎖”對(duì)象坠韩,進(jìn)而讓線程“認(rèn)為試衣間里沒(méi)人”,然后“進(jìn)入試衣間”炼列,這樣的話“試衣間”沒(méi)有任何存在的意義了同眯。
public class GetBalanceOperation implements Runnable {
private Balance balance;
private int account;
private GetBalanceLock lock = new GetBalanceLock();
public GetBalanceOperation(Balance balance, int account) {
this.balance = balance;
this.account = account;
}
@Override
public void run() {
synchronized (lock) {
if (balance != null) {
int temp = balance.getBalance();
if (temp >= account && account > 0) {
temp -= account;
System.out.println("交易處理中···");
try {
Thread.sleep(300);
balance.setBalance(temp);
System.out.println("交易完成!");
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("余額不足唯鸭!");
}
}
}
}
}
public class Main {
public static void main(String[] args) {
Balance balance = new Balance(500);
// new 出了兩個(gè)相同的操作
Runnable operation1 = new GetBalanceOperation(balance, 200);
Runnable operation2 = new GetBalanceOperation(balance, 200);
Thread thread1 = new Thread(operation1);
Thread thread2 = new Thread(operation2);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
System.out.println("余額:" + balance.getBalance());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
交易處理中···
交易處理中···
交易完成须蜗!
交易完成!
余額:300
輸出的結(jié)果很明顯目溉,也沒(méi)有實(shí)現(xiàn)“線程同步”明肮。其原因也是違反了之前提到的“唯一性”。兩個(gè)線程一人一份“操作說(shuō)明書(shū)”缭付。new
出來(lái)兩個(gè)“操作說(shuō)明書(shū)”對(duì)象柿估,但是對(duì)應(yīng)的GetBalanceLock
“鎖”就有兩個(gè),在執(zhí)行synchronized
代碼塊時(shí)也一定不會(huì)是“串行”陷猫。
這種情況下秫舌,可以將“鎖”設(shè)為靜態(tài)即用static
修飾,不管new
多少份一樣的“操作說(shuō)明書(shū)”绣檬,“鎖”就只有一個(gè)足陨,滿足“唯一性”。
public class GetBalanceOperation implements Runnable {
private Balance balance;
private int account;
private static GetBalanceLock lock = new GetBalanceLock(); // 用 static 修飾
public GetBalanceOperation(Balance balance, int account) {
this.balance = balance;
this.account = account;
}
@Override
public void run() {
synchronized (lock) {
if (balance != null) {
int temp = balance.getBalance();
if (temp >= account && account > 0) {
temp -= account;
System.out.println("交易處理中···");
try {
Thread.sleep(300);
balance.setBalance(temp);
System.out.println("交易完成娇未!");
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("余額不足墨缘!");
}
}
}
}
}
- 上述,我們將
synchronized
代碼塊比作“試衣間”零抬,但也不希望大家誤解一個(gè)事情镊讼,就是synchronized
所處理的代碼必須是一模一樣的。因?yàn)樯厦嫠o出的代碼例子所有線程執(zhí)行的代碼是一樣的平夜,“取款”蝶棋,就好像“試衣間”只能“試穿衣服”。
我們使用各種手段實(shí)現(xiàn)線程同步忽妒,所希望的是資源的安全性玩裙,像使用synchronized
代碼塊“包裹”代碼兼贸,根本原因是“這段代碼會(huì)涉及到共享資源(共享的變量等)使用,需要對(duì)線程加以控制献酗,防止共享資源出現(xiàn)安全問(wèn)題(變量讀寫(xiě)‘錯(cuò)亂’等)”寝受。
public class MyLock {
}
/* 通過(guò)這里的代碼坷牛,想傳達(dá)出 synchronized 代碼塊中的代碼可以代表的是不同的任務(wù) */
/* 不要受前面的代碼的影響罕偎,認(rèn)為 synchronized 代碼塊中的代碼只能做同樣的任務(wù) */
/* 未來(lái)可能會(huì)遇到,執(zhí)行的代碼不同京闰,但是代碼所要涉及的資源是同樣的颜及,這個(gè)時(shí)候仍需要合適的線程同步機(jī)制解決 */
public class OperationOne implements Runnable {
private final MyLock lock;
public OperationOne(MyLock lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 5; i++) {
System.out.println("CHN");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class OperationTwo implements Runnable {
private final MyLock lock;
public OperationTwo(MyLock lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 5; i++) {
System.out.println("PRC");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class Main {
public static void main(String[] args) {
MyLock lock = new MyLock();
Thread thread1 = new Thread(new OperationOne(lock));
Thread thread2 = new Thread(new OperationTwo(lock));
thread1.start();
thread2.start();
}
}
輸出結(jié)果:先打印5個(gè)“CHN”,再打印5個(gè)“PRC”蹂楣。
因?yàn)閮蓚€(gè)線程用的“鎖”對(duì)象是同一個(gè)俏站,所以執(zhí)行synchronized
代碼塊時(shí),線程串行執(zhí)行
- “鎖”的引用建議用
final
修飾痊土,防止中途被修改肄扎。非final
的對(duì)象可以被重新賦值,“鎖”對(duì)象就不受管控了赁酝。當(dāng)一個(gè)“鎖”被其他線程占有時(shí)犯祠,當(dāng)前線程可以對(duì)“鎖”對(duì)象重新賦值(相當(dāng)于從新創(chuàng)建了一個(gè)“鎖”對(duì)象),從而也拿到了運(yùn)行的權(quán)利酌呆。
3.2.1.2衡载、synchronized 方法
可以使用
synchronized
關(guān)鍵字來(lái)修飾方法(也稱“同步方法”),所達(dá)到的效果與synchronized
代碼塊的效果相同隙袁,即意味著整個(gè)方法被“加鎖”痰娱,涉及的代碼范圍更大。用
synchronized
關(guān)鍵字來(lái)修飾方法的時(shí)候菩收,對(duì)于靜態(tài)方法梨睁、非靜態(tài)方法會(huì)略有區(qū)別。用
synchronized
關(guān)鍵字修飾非靜態(tài)方法娜饵,先看下面的代碼:
/* 銀行賬戶類而姐,balance 表示余額 */
public class Balance {
private int balance;
public Balance(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
}
/* “取款”操作 */
public class GetBalanceOperation implements Runnable {
private Balance balance;
private int account;
public GetBalanceOperation(Balance balance, int account) {
this.balance = balance;
this.account = account;
}
// synchronized 修飾 run 方法是可以的!;馈拴念!
// synchronized 修飾 run 方法即表示 run 方法是一個(gè)“同步方法”
// 即當(dāng)多個(gè)線程執(zhí)行 run 方法中所有的代碼,將會(huì)“串行”執(zhí)行褐缠。某個(gè)線程要將 run 方法全部執(zhí)行完后政鼠,其他線程才能執(zhí)行 run 方法
@Override
public synchronized void run() {
if (balance != null) {
int temp = balance.getBalance();
if (account > 0 && account <= temp) {
temp -= account;
System.out.println("交易處理中···");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance.setBalance(temp);
System.out.println("交易完成!");
}
}
}
}
public class Main {
public static void main(String[] args) {
Balance balance = new Balance(800);
GetBalanceOperation getBalanceOperation = new GetBalanceOperation(balance, 200);
Thread thread1 = new Thread(getBalanceOperation);
Thread thread2 = new Thread(getBalanceOperation);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("余額:" + balance.getBalance());
}
}
看完上面的代碼队魏,大家可能有疑問(wèn)公般,在synchronized
代碼塊中“費(fèi)了九牛二虎之力”所講的“鎖”對(duì)象到哪里去了万搔?在用synchronized
修飾非靜態(tài)方法時(shí),這個(gè)“鎖”對(duì)象仍是存在的官帘,只不過(guò)這個(gè)“鎖”對(duì)象就是“調(diào)用該非靜態(tài)方法的對(duì)象本身”瞬雹。通過(guò)下面的兩個(gè)等價(jià)代碼,就能理解為什么這么說(shuō):
@Override
public synchronized void run() {
if (balance != null) {
int temp = balance.getBalance();
if (account > 0 && account <= temp) {
temp -= account;
System.out.println("交易處理中···");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance.setBalance(temp);
System.out.println("交易完成刽虹!");
}
}
}
@Override
public void run() {
synchronized (this) {
if (balance != null) {
int temp = balance.getBalance();
if (account > 0 && account <= temp) {
temp -= account;
System.out.println("交易處理中···");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance.setBalance(temp);
System.out.println("交易完成酗捌!");
}
}
}
}
用synchronized
修飾非靜態(tài)方法,等價(jià)于使用synchronized (this)
代碼塊然后代碼塊中是整個(gè)方法體涌哲∨昼停“鎖”對(duì)象的引用就是synchronized (this)
中的this
就上面的“取款”代碼而言,“鎖”對(duì)象是main
方法中定義的GetBalanceOperation
類的對(duì)象:getBalanceOperation
阀圾。
因?yàn)?code>synchronized修飾的是
run
方法哪廓,而前面提到這種情況下“鎖”對(duì)象就是“調(diào)用該非靜態(tài)方法的對(duì)象本身”,那么是誰(shuí)調(diào)用的run
方法初烘?如果大家對(duì)java.lang.Thread
類的源碼有有印象的話涡真,對(duì)于使用Thread(Runnable target)
構(gòu)造方法創(chuàng)建的線程來(lái)說(shuō),java.lang.Thread
對(duì)象使用start
啟動(dòng)線程后肾筐,由 JVM 去調(diào)用java.lang.Thread
對(duì)象中的run
方法哆料,此時(shí)的run
方法,本質(zhì)上是由所傳參數(shù)target
中的run
方法局齿。
@Override
public void run() {
if (target != null) {
target.run();
}
}
理清頭緒之后剧劝,再來(lái)看看,這里的“鎖”對(duì)象是否是“唯一”的抓歼,很顯然是“唯一”的讥此。所以線程能夠?qū)崿F(xiàn)同步。
- 補(bǔ)充代碼:
public class GetBalanceOperation implements Runnable {
private Balance balance;
private int account;
public GetBalanceOperation(Balance balance, int account) {
this.balance = balance;
this.account = account;
}
// synchronized “間接加修飾 run 方法”
// 這樣寫(xiě)的話谣妻,相對(duì)來(lái)說(shuō)萄喳,代碼結(jié)構(gòu)會(huì)比較清晰
private synchronized void get() {
if (balance != null) {
int temp = balance.getBalance();
if (account > 0 && account <= temp) {
temp -= account;
System.out.println("交易處理中···");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance.setBalance(temp);
System.out.println("交易完成!");
}
}
}
@Override
public void run() {
get();
}
}
- 用
synchronized
關(guān)鍵字修飾靜態(tài)方法蹋半,先看下面的代碼:
public class Product {
private static int count;
public static void setCount(int count) {
Product.count = count;
}
public static int getCount() {
return count;
}
}
public class BuyThread extends Thread {
private synchronized static void buy() {
int temp = Product.getCount();
if (temp >= 10) {
System.out.println("正在出貨···");
temp -= 10;
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Product.setCount(temp);
System.out.println("出貨成功他巨!");
}
}
@Override
public void run() {
buy();
}
}
public class Main {
public static void main(String[] args) {
Product.setCount(30);
Thread thread1 = new BuyThread();
Thread thread2 = new BuyThread();
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("剩余:" + Product.getCount());
}
}
正在出貨···
出貨成功!
正在出貨···
出貨成功减江!
剩余:10
首先染突,上面的代碼可能與之前的代碼有所不同,主要是因?yàn)橐獎(jiǎng)?chuàng)造能夠展現(xiàn)“使用synchronized
修飾靜態(tài)方法”的條件辈灼。
public synchronized static xxx xxx() {···}
對(duì)于通過(guò)“繼承java.lang.Thread
類創(chuàng)建線程類份企,然后再去使用new
創(chuàng)建多個(gè)線程”的方式,如果需要進(jìn)行線程同步的話巡莹,那必須確彼局荆“鎖”對(duì)象是唯一的甜紫。之前可能大家看到,面對(duì)這樣的情景骂远,可以使用synchronized
代碼塊然后使用static
修飾的一個(gè)對(duì)象作為唯一的“鎖”對(duì)象囚霸,實(shí)現(xiàn)線程同步;而在這里所展示的是“使用synchronized
修飾靜態(tài)方法”也是能夠面對(duì)這樣相似的情景激才。不過(guò)相對(duì)來(lái)說(shuō)拓型,這種“使用synchronized
修飾靜態(tài)方法”相對(duì)來(lái)說(shuō)還是有些局限性的,畢竟在靜態(tài)方法中只能使用靜態(tài)的贸营。
下面的討論一下吨述,為什么“使用synchronized
修飾靜態(tài)方法”也能實(shí)現(xiàn)線程同步岩睁?還是同樣的道理钞脂,這個(gè)唯一的“鎖”對(duì)象是什么?這里的“鎖”對(duì)象是類對(duì)象捕儒,每個(gè)類都有唯一的一個(gè)類對(duì)象冰啃,獲取類對(duì)象:類名.class
。
類對(duì)象:在“面向?qū)ο缶幊獭敝辛跤ǎ叭f(wàn)物皆對(duì)象”理念是一直貫穿始終的阎毅,所有的類,包括 Java 本身提供的類点弯、由我們自定義的類等最終都會(huì)歸為一個(gè)“類”扇调,所有的類都是這個(gè)“類”的對(duì)象。
可以這樣理解抢肛,為什么 JVM 能夠識(shí)別帶class
關(guān)鍵字的就能判定它是一個(gè)類狼钮?說(shuō)明在底層中,所有的類有一個(gè)“模板”捡絮,通過(guò)這個(gè)“模板”會(huì)很好的判定哪些是類熬芜,哪些是其他的。而 Java 中“模版”不就可以說(shuō)成類嗎8N取(粗淺理解涎拉,詳細(xì)內(nèi)容,見(jiàn)TODO 反射機(jī)制)
這樣的話的圆,上面的代碼可以等價(jià)于:
public class BuyThread extends Thread {
private static void buy() {
synchronized (BuyThread.class) {
int temp = Product.getCount();
if (temp >= 10) {
System.out.println("正在出貨···");
temp -= 10;
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Product.setCount(temp);
System.out.println("出貨成功鼓拧!");
}
}
}
@Override
public void run() {
buy();
}
}
正在出貨···
出貨成功!
正在出貨···
出貨成功越妈!
剩余:10
注意:靜態(tài)方法與非靜態(tài)方法同時(shí)使用了synchronized
后它們之間是非互斥關(guān)系的季俩,原因在于靜態(tài)方法的“鎖”對(duì)象是類對(duì)象而非靜態(tài)方法的“鎖”對(duì)象的是當(dāng)前方法所屬對(duì)象。
3.2.1.3叮称、synchronized 注意事項(xiàng)
多個(gè)需要同步的線程在訪問(wèn)同步塊時(shí)(當(dāng)多個(gè)線程因?yàn)橘Y源等問(wèn)題希望“串行”執(zhí)行某些代碼時(shí))种玛,使用的應(yīng)該是同一個(gè)鎖對(duì)象引用藐鹤。
在使用同步塊時(shí)應(yīng)當(dāng)盡量減少同步范圍以提高并發(fā)的執(zhí)行效率,即
synchronized
關(guān)鍵字影響的范圍盡可能小赂韵,比如“一般情況下娱节,能小范圍地使用synchronized
代碼塊的就沒(méi)必要用synchronized
修飾整個(gè)方法”。