java线程安全例子_Java总结篇系列:Java多线程(三)
本文主要接著前面多線程的兩篇文章總結(jié)Java多線程中的線程安全問(wèn)題。
一.一個(gè)典型的Java線程安全例子
1 public classThreadTest {2
3 public static voidmain(String[] args) {4 Account account = new Account("123456", 1000);5 DrawMoneyRunnable drawMoneyRunnable = new DrawMoneyRunnable(account, 700);6 Thread myThread1 = newThread(drawMoneyRunnable);7 Thread myThread2 = newThread(drawMoneyRunnable);8 myThread1.start();9 myThread2.start();10 }11
12 }13
14 class DrawMoneyRunnable implementsRunnable {15
16 privateAccount account;17 private doubledrawAmount;18
19 public DrawMoneyRunnable(Account account, doubledrawAmount) {20 super();21 this.account =account;22 this.drawAmount =drawAmount;23 }24
25 public voidrun() {26 if (account.getBalance() >= drawAmount) { //1
27 System.out.println("取錢(qián)成功, 取出錢(qián)數(shù)為:" +drawAmount);28 double balance = account.getBalance() -drawAmount;29 account.setBalance(balance);30 System.out.println("余額為:" +balance);31 }32 }33 }34
35 classAccount {36
37 privateString accountNo;38 private doublebalance;39
40 publicAccount() {41
42 }43
44 public Account(String accountNo, doublebalance) {45 this.accountNo =accountNo;46 this.balance =balance;47 }48
49 publicString getAccountNo() {50 returnaccountNo;51 }52
53 public voidsetAccountNo(String accountNo) {54 this.accountNo =accountNo;55 }56
57 public doublegetBalance() {58 returnbalance;59 }60
61 public void setBalance(doublebalance) {62 this.balance =balance;63 }64
65 }
上面例子很容易理解,有一張銀行卡,里面有1000的余額,程序模擬你和你老婆同時(shí)在取款機(jī)進(jìn)行取錢(qián)操作的場(chǎng)景。多次運(yùn)行此程序,可能具有多個(gè)不同組合的輸出結(jié)果。其中一種可能的輸出為:
1 取錢(qián)成功, 取出錢(qián)數(shù)為:700.0
2 余額為:300.0
3 取錢(qián)成功, 取出錢(qián)數(shù)為:700.0
4 余額為:-400.0
也就是說(shuō),對(duì)于一張只有1000余額的銀行卡,你們一共可以取出1400,這顯然是有問(wèn)題的。
經(jīng)過(guò)分析,問(wèn)題在于Java多線程環(huán)境下的執(zhí)行的不確定性。CPU可能隨機(jī)的在多個(gè)處于就緒狀態(tài)中的線程中進(jìn)行切換,因此,很有可能出現(xiàn)如下情況:當(dāng)thread1執(zhí)行到//1處代碼時(shí),判斷條件為true,此時(shí)CPU切換到thread2,執(zhí)行//1處代碼,發(fā)現(xiàn)依然為真,然后執(zhí)行完thread2,接著切換到thread1,接著執(zhí)行完畢。此時(shí),就會(huì)出現(xiàn)上述結(jié)果。
因此,講到線程安全問(wèn)題,其實(shí)是指多線程環(huán)境下對(duì)共享資源的訪問(wèn)可能會(huì)引起此共享資源的不一致性。因此,為避免線程安全問(wèn)題,應(yīng)該避免多線程環(huán)境下對(duì)此共享資源的并發(fā)訪問(wèn)。
二.同步方法
對(duì)共享資源進(jìn)行訪問(wèn)的方法定義中加上synchronized關(guān)鍵字修飾,使得此方法稱(chēng)為同步方法。可以簡(jiǎn)單理解成對(duì)此方法進(jìn)行了加鎖,其鎖對(duì)象為當(dāng)前方法所在的對(duì)象自身。多線程環(huán)境下,當(dāng)執(zhí)行此方法時(shí),首先都要獲得此同步鎖(且同時(shí)最多只有一個(gè)線程能夠獲得),只有當(dāng)線程執(zhí)行完此同步方法后,才會(huì)釋放鎖對(duì)象,其他的線程才有可能獲取此同步鎖,以此類(lèi)推...
在上例中,共享資源為account對(duì)象,當(dāng)使用同步方法時(shí),可以解決線程安全問(wèn)題。只需在run()方法前加上synshronized關(guān)鍵字即可。
1 public synchronized voidrun() {2
3 //....
4
5 }
三.同步代碼塊
正如上面所分析的那樣,解決線程安全問(wèn)題其實(shí)只需限制對(duì)共享資源訪問(wèn)的不確定性即可。使用同步方法時(shí),使得整個(gè)方法體都成為了同步執(zhí)行狀態(tài),會(huì)使得可能出現(xiàn)同步范圍過(guò)大的情況,于是,針對(duì)需要同步的代碼可以直接另一種同步方式——同步代碼塊來(lái)解決。
同步代碼塊的格式為:
1 synchronized(obj) {2
3 //...
4
5 }
其中,obj為鎖對(duì)象,因此,選擇哪一個(gè)對(duì)象作為鎖是至關(guān)重要的。一般情況下,都是選擇此共享資源對(duì)象作為鎖對(duì)象。
如上例中,最好選用account對(duì)象作為鎖對(duì)象。(當(dāng)然,選用this也是可以的,那是因?yàn)閯?chuàng)建線程使用了runnable方式,如果是直接繼承Thread方式創(chuàng)建的線程,使用this對(duì)象作為同步鎖會(huì)其實(shí)沒(méi)有起到任何作用,因?yàn)槭遣煌膶?duì)象了。因此,選擇同步鎖時(shí)需要格外小心...)
四.Lock對(duì)象同步鎖
上面我們可以看出,正因?yàn)閷?duì)同步鎖對(duì)象的選擇需要如此小心,有沒(méi)有什么簡(jiǎn)單點(diǎn)的解決方案呢?以方便同步鎖對(duì)象與共享資源解耦,同時(shí)又能很好的解決線程安全問(wèn)題。
使用Lock對(duì)象同步鎖可以方便的解決此問(wèn)題,唯一需要注意的一點(diǎn)是Lock對(duì)象需要與資源對(duì)象同樣具有一對(duì)一的關(guān)系。Lock對(duì)象同步鎖一般格式為:
1 classX {2
3 //顯示定義Lock同步鎖對(duì)象,此對(duì)象與共享資源具有一對(duì)一關(guān)系
4 private final Lock lock = newReentrantLock();5
6 public voidm(){7 //加鎖
8 lock.lock();9
10 //... 需要進(jìn)行線程安全同步的代碼11
12 //釋放Lock鎖
13 lock.unlock();14 }15
16 }
五.wait()/notify()/notifyAll()線程通信
在博文《Java總結(jié)篇系列:java.lang.Object》中有提及到這三個(gè)方法,雖然這三個(gè)方法主要都是用于多線程中,但實(shí)際上都是Object類(lèi)中的本地方法。因此,理論上,任何Object對(duì)象都可以作為這三個(gè)方法的主調(diào),在實(shí)際的多線程編程中,只有同步鎖對(duì)象調(diào)這三個(gè)方法,才能完成對(duì)多線程間的線程通信。
wait():導(dǎo)致當(dāng)前線程等待并使其進(jìn)入到等待阻塞狀態(tài)。直到其他線程調(diào)用該同步鎖對(duì)象的notify()或notifyAll()方法來(lái)喚醒此線程。
notify():喚醒在此同步鎖對(duì)象上等待的單個(gè)線程,如果有多個(gè)線程都在此同步鎖對(duì)象上等待,則會(huì)任意選擇其中某個(gè)線程進(jìn)行喚醒操作,只有當(dāng)前線程放棄對(duì)同步鎖對(duì)象的鎖定,才可能執(zhí)行被喚醒的線程。
notifyAll():喚醒在此同步鎖對(duì)象上等待的所有線程,只有當(dāng)前線程放棄對(duì)同步鎖對(duì)象的鎖定,才可能執(zhí)行被喚醒的線程。
1 packagecom.qqyumidi;2
3 public classThreadTest {4
5 public static voidmain(String[] args) {6 Account account = new Account("123456", 0);7
8 Thread drawMoneyThread = new DrawMoneyThread("取錢(qián)線程", account, 700);9 Thread depositeMoneyThread = new DepositeMoneyThread("存錢(qián)線程", account, 700);10
11 drawMoneyThread.start();12 depositeMoneyThread.start();13 }14
15 }16
17 class DrawMoneyThread extendsThread {18
19 privateAccount account;20 private doubleamount;21
22 public DrawMoneyThread(String threadName, Account account, doubleamount) {23 super(threadName);24 this.account =account;25 this.amount =amount;26 }27
28 public voidrun() {29 for (int i = 0; i < 100; i++) {30 account.draw(amount, i);31 }32 }33 }34
35 class DepositeMoneyThread extendsThread {36
37 privateAccount account;38 private doubleamount;39
40 public DepositeMoneyThread(String threadName, Account account, doubleamount) {41 super(threadName);42 this.account =account;43 this.amount =amount;44 }45
46 public voidrun() {47 for (int i = 0; i < 100; i++) {48 account.deposite(amount, i);49 }50 }51 }52
53 classAccount {54
55 privateString accountNo;56 private doublebalance;57 //標(biāo)識(shí)賬戶(hù)中是否已有存款
58 private boolean flag = false;59
60 publicAccount() {61
62 }63
64 public Account(String accountNo, doublebalance) {65 this.accountNo =accountNo;66 this.balance =balance;67 }68
69 publicString getAccountNo() {70 returnaccountNo;71 }72
73 public voidsetAccountNo(String accountNo) {74 this.accountNo =accountNo;75 }76
77 public doublegetBalance() {78 returnbalance;79 }80
81 public void setBalance(doublebalance) {82 this.balance =balance;83 }84
85 /**
86 * 存錢(qián)87 *88 *@paramdepositeAmount89 */
90 public synchronized void deposite(double depositeAmount, inti) {91
92 if(flag) {93 //賬戶(hù)中已有人存錢(qián)進(jìn)去,此時(shí)當(dāng)前線程需要等待阻塞
94 try{95 System.out.println(Thread.currentThread().getName() + " 開(kāi)始要執(zhí)行wait操作" + " -- i=" +i);96 wait();97 //1
98 System.out.println(Thread.currentThread().getName() + " 執(zhí)行了wait操作" + " -- i=" +i);99 } catch(InterruptedException e) {100 e.printStackTrace();101 }102 } else{103 //開(kāi)始存錢(qián)
104 System.out.println(Thread.currentThread().getName() + " 存款:" + depositeAmount + " -- i=" +i);105 setBalance(balance +depositeAmount);106 flag = true;107
108 //喚醒其他線程
109 notifyAll();110
111 //2
112 try{113 Thread.sleep(3000);114 } catch(InterruptedException e) {115 e.printStackTrace();116 }117 System.out.println(Thread.currentThread().getName() + "-- 存錢(qián) -- 執(zhí)行完畢" + " -- i=" +i);118 }119 }120
121 /**
122 * 取錢(qián)123 *124 *@paramdrawAmount125 */
126 public synchronized void draw(double drawAmount, inti) {127 if (!flag) {128 //賬戶(hù)中還沒(méi)人存錢(qián)進(jìn)去,此時(shí)當(dāng)前線程需要等待阻塞
129 try{130 System.out.println(Thread.currentThread().getName() + " 開(kāi)始要執(zhí)行wait操作" + " 執(zhí)行了wait操作" + " -- i=" +i);131 wait();132 System.out.println(Thread.currentThread().getName() + " 執(zhí)行了wait操作" + " 執(zhí)行了wait操作" + " -- i=" +i);133 } catch(InterruptedException e) {134 e.printStackTrace();135 }136 } else{137 //開(kāi)始取錢(qián)
138 System.out.println(Thread.currentThread().getName() + " 取錢(qián):" + drawAmount + " -- i=" +i);139 setBalance(getBalance() -drawAmount);140
141 flag = false;142
143 //喚醒其他線程
144 notifyAll();145
146 System.out.println(Thread.currentThread().getName() + "-- 取錢(qián) -- 執(zhí)行完畢" + " -- i=" + i); //3
147 }148 }149
150 }
上面的例子演示了wait()/notify()/notifyAll()的用法。部分輸出結(jié)果為:
1 取錢(qián)線程 開(kāi)始要執(zhí)行wait操作 執(zhí)行了wait操作 -- i=0
2 存錢(qián)線程 存款:700.0 -- i=0
3 存錢(qián)線程-- 存錢(qián) -- 執(zhí)行完畢 -- i=0
4 存錢(qián)線程 開(kāi)始要執(zhí)行wait操作 -- i=1
5 取錢(qián)線程 執(zhí)行了wait操作 執(zhí)行了wait操作 -- i=0
6 取錢(qián)線程 取錢(qián):700.0 -- i=1
7 取錢(qián)線程-- 取錢(qián) -- 執(zhí)行完畢 -- i=1
8 取錢(qián)線程 開(kāi)始要執(zhí)行wait操作 執(zhí)行了wait操作 -- i=2
9 存錢(qián)線程 執(zhí)行了wait操作 -- i=1
10 存錢(qián)線程 存款:700.0 -- i=2
11 存錢(qián)線程-- 存錢(qián) -- 執(zhí)行完畢 -- i=2
12 取錢(qián)線程 執(zhí)行了wait操作 執(zhí)行了wait操作 -- i=2
13 取錢(qián)線程 取錢(qián):700.0 -- i=3
14 取錢(qián)線程-- 取錢(qián) -- 執(zhí)行完畢 -- i=3
15 取錢(qián)線程 開(kāi)始要執(zhí)行wait操作 執(zhí)行了wait操作 -- i=4
16 存錢(qián)線程 存款:700.0 -- i=3
17 存錢(qián)線程-- 存錢(qián) -- 執(zhí)行完畢 -- i=3
18 存錢(qián)線程 開(kāi)始要執(zhí)行wait操作 -- i=4
19 取錢(qián)線程 執(zhí)行了wait操作 執(zhí)行了wait操作 -- i=4
20 取錢(qián)線程 取錢(qián):700.0 -- i=5
21 取錢(qián)線程-- 取錢(qián) -- 執(zhí)行完畢 -- i=5
22 取錢(qián)線程 開(kāi)始要執(zhí)行wait操作 執(zhí)行了wait操作 -- i=6
23 存錢(qián)線程 執(zhí)行了wait操作 -- i=4
24 存錢(qián)線程 存款:700.0 -- i=5
25 存錢(qián)線程-- 存錢(qián) -- 執(zhí)行完畢 -- i=5
26 存錢(qián)線程 開(kāi)始要執(zhí)行wait操作 -- i=6
27 取錢(qián)線程 執(zhí)行了wait操作 執(zhí)行了wait操作 -- i=6
28 取錢(qián)線程 取錢(qián):700.0 -- i=7
29 取錢(qián)線程-- 取錢(qián) -- 執(zhí)行完畢 -- i=7
30 取錢(qián)線程 開(kāi)始要執(zhí)行wait操作 執(zhí)行了wait操作 -- i=8
31 存錢(qián)線程 執(zhí)行了wait操作 -- i=6
32 存錢(qián)線程 存款:700.0 -- i=7
由此,我們需要注意如下幾點(diǎn):
1.wait()方法執(zhí)行后,當(dāng)前線程立即進(jìn)入到等待阻塞狀態(tài),其后面的代碼不會(huì)執(zhí)行;
2.notify()/notifyAll()方法執(zhí)行后,將喚醒此同步鎖對(duì)象上的(任意一個(gè)-notify()/所有-notifyAll())線程對(duì)象,但是,此時(shí)還并沒(méi)有釋放同步鎖對(duì)象,也就是說(shuō),如果notify()/notifyAll()后面還有代碼,還會(huì)繼續(xù)進(jìn)行,知道當(dāng)前線程執(zhí)行完畢才會(huì)釋放同步鎖對(duì)象;
3.notify()/notifyAll()執(zhí)行后,如果右面有sleep()方法,則會(huì)使當(dāng)前線程進(jìn)入到阻塞狀態(tài),但是同步對(duì)象鎖沒(méi)有釋放,依然自己保留,那么一定時(shí)候后還是會(huì)繼續(xù)執(zhí)行此線程,接下來(lái)同2;
4.wait()/notify()/nitifyAll()完成線程間的通信或協(xié)作都是基于不同對(duì)象鎖的,因此,如果是不同的同步對(duì)象鎖將失去意義,同時(shí),同步對(duì)象鎖最好是與共享資源對(duì)象保持一一對(duì)應(yīng)關(guān)系;
5.當(dāng)wait線程喚醒后并執(zhí)行時(shí),是接著上次執(zhí)行到的wait()方法代碼后面繼續(xù)往下執(zhí)行的。
當(dāng)然,上面的例子相對(duì)來(lái)說(shuō)比較簡(jiǎn)單,只是為了簡(jiǎn)單示例wait()/notify()/noitifyAll()方法的用法,但其本質(zhì)上說(shuō),已經(jīng)是一個(gè)簡(jiǎn)單的生產(chǎn)者-消費(fèi)者模式了。
總結(jié)
以上是生活随笔為你收集整理的java线程安全例子_Java总结篇系列:Java多线程(三)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 音乐疗法
- 下一篇: Java分布式篇4——Redis