java同步机制简单介绍
在java編程里經(jīng)常聽到類似的術(shù)語: 這個函數(shù)是不是同步的...
本文就簡單介紹下什么是同步, java中同步的一些處理方法。
1.同步問題產(chǎn)生的原因
Java中同步問題是伴隨這多線程而產(chǎn)生的, 也就是說如果一個程序是單線程的, 那么就沒有同步的概念。
舉1個最常見的例子:
假如1個售票程序支持多個線程同時售票。
它里面的核心代碼大概是這樣的:
void sellTickets{買票者身份驗證();一些前期步驟();if (剩余票數(shù)()>0){賣一張票();剩余票數(shù)減一;交易額加上票的單價;}}
這部分代碼假如只有一條線程來執(zhí)行是毫無問題的.
但是假如有A B C三條線程來同時執(zhí)行. 也就是三條線程同時來售票. 就可能會出現(xiàn)問題了.
例如庫存里共有50張票
cpu首先執(zhí)行A線層, 執(zhí)行完 "賣一張票()"時, cpu突然跳到另1個線層B去執(zhí)行. 線程B檢查剩余票數(shù), 還是50張.?
這就出問題了, 因為A線程雖然賣出了一張票, 但是剩余票數(shù)還未減一,? 那么第一張票可能被重復(fù)出售. 產(chǎn)生數(shù)據(jù)錯誤.
所以同步問題產(chǎn)生的根本原因是:
1. 多線程.
2. 多線程同時訪問并修改同一段數(shù)據(jù).
下面可以舉個例子:
package Thread_kng.Td_synchronized;class Sell_ticket implements Runnable{public int tickets = 50; //count of ticketspublic int unit_price = 30; //unit_price of per ticketpublic int sum_price = 0; //total turnoverprivate int pre_counts;public Sell_ticket(){pre_counts = this.tickets;}Thread cur_thrd;//verify account of buyerprivate void accountVerify(){System.out.printf("Thread %s: account is valid!\n", cur_thrd.getName());}//previous preparationprivate void pre_Jobs(){System.out.printf("Thread %s: it's prepared!\n", cur_thrd.getName());}private void sellTicket(){this.accountVerify();this.pre_Jobs();if (this.tickets > 0){System.out.printf("Thread %s: sold the no.%d ticket!\n", cur_thrd.getName(),pre_counts - tickets + 1);tickets--;sum_price+=unit_price;}else{System.out.printf("Thread %s: all the tickets are sold out!\n", cur_thrd.getName(),pre_counts - tickets + 1);}}public void run(){cur_thrd = Thread.currentThread();while(this.tickets > 0){this.sellTicket(); }} }public class Td_syn_1{public static void st(){Sell_ticket s = new Sell_ticket();Thread a = new Thread(s);Thread b = new Thread(s);Thread c = new Thread(s);a.setName("A");b.setName("B");c.setName("C");a.start();b.start();c.start();try{Thread.currentThread().sleep(5000);}catch(Exception e){e.printStackTrace();}System.out.printf("Turnover is %d\n", s.sum_price);} }上面定義了實現(xiàn)了Runnable 接口的售票類. 里面定義了票數(shù), 單價, 總售價等.
其中run方法里調(diào)用了sellTicket()方法.
啟動類里利用這個對象啟動了3個線程同時執(zhí)行
實際執(zhí)行時, 就產(chǎn)生同步問題了: 標(biāo)志就是最后的總交易額對不上, 例如上面售票50張總額應(yīng)該是1500.
但是有些票被重復(fù)售出, 結(jié)果就錯誤了: 而且多次執(zhí)行的結(jié)果并不一樣.
執(zhí)行結(jié)果(最后部分): 可以見到執(zhí)行結(jié)果很混亂.最后交易額錯誤
二.什么是同步, 同步方法
如上面那個例子, 如果一個方法單線程中執(zhí)行正常, 多線程由于同步問題出現(xiàn)數(shù)據(jù)錯誤.
那么我們就說這個方法不是同步的.
否則,我們就說這個是1個同步的方法, 也就是多線程執(zhí)行也能正常執(zhí)行.
三.同步方法的解決方法.
我們已經(jīng)清楚同步問題的原因是: 多條線程同時訪問同一段數(shù)據(jù).
所以解決方法很簡單: 就是令那1段數(shù)據(jù)在同1個時間點內(nèi)只能有1個線程訪問.
也就是A, B, C中, 如果B線程正在訪問數(shù)據(jù), 那么A 和 C線程就必須暫停. 直至B訪問數(shù)據(jù)完畢!
我們這三條線程互斥
但是這樣的話如果A在買票, 其他人都買不了嗎? 多線程貌似失去意義.
實際上, 我們只需要令關(guān)鍵的部分代碼互斥就可以了.
例如上面例子:? 賬戶驗證();前期準(zhǔn)備();?? 等代碼不是關(guān)鍵的, 不需要互斥
而關(guān)鍵代碼部分:? 獲得剩余票數(shù), 賣出一張票, 票數(shù)-1,交易額+=單價 這些部分是非常關(guān)鍵的, 需要互斥. 因為如果同時有多個線程執(zhí)行這些代碼就會數(shù)據(jù)錯誤.
所以我們又認(rèn)為互斥是解決同步方法的必要條件.
四.關(guān)鍵字synchronized
Java里 提供了1個關(guān)鍵字synchronized來達(dá)到代碼多線程互斥的目的.
synchronized 有兩種方法,
1.就是用來修飾方法.
2.用來為對象加上同步鎖.
4.1 synchronized 修飾方法
synchornized修飾一個方法, 意味著為同1個對象中(非靜態(tài)方法)或同1個類中(靜態(tài)方法)加上同步鎖.
意識就是如果有多條線程執(zhí)行該方法, 那么當(dāng)其中一條線程執(zhí)行該方法時, 就為該方法上鎖, 其他線程就不能執(zhí)行該方法. 而進(jìn)入阻塞狀態(tài)(暫停執(zhí)行), 直至該方法執(zhí)行完該方法.
也就是cpu絕不會執(zhí)行synchronized的途中跳到另一條線程執(zhí)行!
4.1.1 synchronized 修飾非static方法.
synchronized 修飾非靜態(tài)方法與靜態(tài)方法還是有點區(qū)別的.
我們知道非靜態(tài)方法屬于對象而不是類本身, 需要1個已實例化的對象才能執(zhí)行.
而synchronized修飾1個非靜態(tài)方法意味這個方法在同1個對象中, 多條線程執(zhí)行該方法是互斥的. 也就是其中一條線程一旦執(zhí)行該方法,其他線程就進(jìn)入阻塞狀態(tài).
但是前提是同1個對象中.
舉回上面的例子, 修改一下:
package Thread_kng.Td_synchronized;class Sell_ticket2 implements Runnable{public int tickets = 50; //count of ticketspublic int unit_price = 30; //unit_price of per ticketpublic int sum_price = 0; //total turnoverprivate int pre_counts;public Sell_ticket2(){pre_counts = this.tickets;}Thread cur_thrd;//verify account of buyerprivate void accountVerify(){System.out.printf("Thread %s: account is valid!\n", cur_thrd.getName());}//previous preparationprivate void pre_Jobs(){System.out.printf("Thread %s: it's prepared!\n", cur_thrd.getName());}private synchronized void sellTicket(){this.accountVerify();this.pre_Jobs();if (this.tickets > 0){System.out.printf("Thread %s: sold the no.%d ticket!\n", cur_thrd.getName(),pre_counts - tickets + 1);tickets--;sum_price+=unit_price;}else{System.out.printf("Thread %s: all the tickets are sold out!\n", cur_thrd.getName(),pre_counts - tickets + 1);}}public void run(){cur_thrd = Thread.currentThread();while(this.tickets > 0){this.sellTicket(); }} }public class Td_syn_2{public static void st(){Sell_ticket2 s = new Sell_ticket2();Thread a = new Thread(s);Thread b = new Thread(s);Thread c = new Thread(s);a.setName("A");b.setName("B");c.setName("C");a.start();b.start();c.start();try{Thread.currentThread().sleep(5000);}catch(Exception e){e.printStackTrace();}System.out.printf("Turnover is %d\n", s.sum_price);} }實際上的改動只有一處, 就是在售票類中
用synchronized 關(guān)鍵字去修飾方法 sellTicket()
所以執(zhí)行時一旦有1條線程開始執(zhí)行 sellTicket(); 關(guān)于該對象的所有其他線程就進(jìn)入阻塞狀態(tài)..
執(zhí)行結(jié)果:
可以見到售票次序很規(guī)律.
而且多次執(zhí)行, 最后的交易額都是正確的.
4.1.2 synchronized 修飾static方法.
上面之所以強調(diào)同一個對象,是因為synchronized方法修飾靜態(tài)方法只會影響同1個對象的線程.
而對同1個類而不同對象, 用synchornized 去修飾是無效的.
見下面的例子, 也是修改上面的程序:
package Thread_kng.Td_synchronized;class Sell_ticket3 extends Thread{public static int tickets = 50; //count of ticketspublic static int unit_price = 30; //unit_price of per ticketpublic static int sum_price = 0; //total turnoverprivate int pre_counts;public Sell_ticket3(String name){super(name);pre_counts = this.tickets;}//verify account of buyerprivate void accountVerify(){System.out.printf("Thread %s: account is valid!\n", this.getName());}//previous preparationprivate void pre_Jobs(){System.out.printf("Thread %s: it's prepared!\n", this.getName());}private synchronized void sellTicket(){this.accountVerify();this.pre_Jobs();if (this.tickets > 0){System.out.printf("Thread %s: sold the no.%d ticket!\n", this.getName(),pre_counts - tickets + 1);tickets--;sum_price+=unit_price;}else{System.out.printf("Thread %s: all the tickets are sold out!\n", this.getName(),pre_counts - tickets + 1);}}public void run(){while(this.tickets > 0){this.sellTicket(); }} }public class Td_syn_3{public static void st(){Sell_ticket3 a = new Sell_ticket3("A");Sell_ticket3 b = new Sell_ticket3("B");Sell_ticket3 c = new Sell_ticket3("C");a.start();b.start();c.start();try{Thread.currentThread().sleep(5000);}catch(Exception e){e.printStackTrace();}System.out.printf("Turnover is %d\n", Sell_ticket3.sum_price);} }注意上面的售票類不在是實驗Runnable 接口, 而是繼承了Thread類.
那么在啟動3個線程時, 就必須實例化3個對象,? 而之前的例子是實例化一個對象而創(chuàng)建3個線程.
由于這3個對象a b c是不同的對象, 所以即使在非靜態(tài)方法sellTicket()加上synchronized 關(guān)鍵字, 但是實際執(zhí)行時.
3個線程分別執(zhí)行自己對象的sellTicket(), 所以就不是互斥的.
執(zhí)行結(jié)果:? 可見最后交易額也是錯誤的. 而且有重復(fù)售票的現(xiàn)象:
解決方法也很簡單, 就是把售票方法sellTickt() 改為1個靜態(tài)方法.
靜態(tài)方法只屬于類本身.
如果用synchronized關(guān)鍵字修飾1個靜態(tài)方法.
那么這個類或者任何對象調(diào)用這個方法, 在多線程中也是互斥的.
解決方法就是把sellTicket()改為1個靜態(tài)方法, 注意的是靜態(tài)方法不能調(diào)用非靜態(tài)成員和方法.
修改后的例子:
package Thread_kng.Td_synchronized;class Sell_ticket4 extends Thread{public static int tickets = 50; //count of ticketspublic static int unit_price = 30; //unit_price of per ticketpublic static int sum_price = 0; //total turnoverprivate static int pre_counts;public Sell_ticket4(String name){super(name);pre_counts = this.tickets;}//verify account of buyerprivate static void accountVerify(){System.out.printf("Thread %s: account is valid!\n", Thread.currentThread().getName());}//previous preparationprivate static void pre_Jobs(){System.out.printf("Thread %s: it's prepared!\n", Thread.currentThread().getName());}private synchronized static void sellTicket(){accountVerify();pre_Jobs();if (tickets > 0){System.out.printf("Thread %s: sold the no.%d ticket!\n", Thread.currentThread().getName(),pre_counts - tickets + 1);tickets--;sum_price+=unit_price;}else{System.out.printf("Thread %s: all the tickets are sold out!\n", Thread.currentThread().getName(),pre_counts - tickets + 1);}}public void run(){while(this.tickets > 0){this.sellTicket(); }} }public class Td_syn_4{public static void st(){Sell_ticket4 a = new Sell_ticket4("A");Sell_ticket4 b = new Sell_ticket4("B");Sell_ticket4 c = new Sell_ticket4("C");a.start();b.start();c.start();try{Thread.currentThread().sleep(5000);}catch(Exception e){e.printStackTrace();}System.out.printf("Turnover is %d\n", Sell_ticket4.sum_price);} }修改后的結(jié)果就變成正確同步的了:
4.2 利用synchronized關(guān)鍵字 為對象加上同步鎖
我們回顧了上面的例子, 用synchronized 為售票方法設(shè)為同步方法, 貌似解決了問題.
但是還是有一些缺點.
因為售票方法sellTicket() 里面還調(diào)用了 賬戶驗證() 前期準(zhǔn)備()等方法.
現(xiàn)實中就意味著如果A在買票, 那么B連賬戶驗證()這一步也做不了, 是不合適的.
根本原因是賬戶驗證() 前期準(zhǔn)備()等方法沒有訪問修改公共數(shù)據(jù). 也就說這些方法不是需要同步的關(guān)鍵代碼.
所以我們應(yīng)該只為關(guān)鍵代碼設(shè)為互斥.
方法無非兩種:
1. 就是只把關(guān)鍵代碼放進(jìn)另1個方法, 為這個方法加上synchronized關(guān)鍵字.
2. 就是利用synchronized加上對象鎖.
4.2.1 什么是對象鎖
java編程中, 任何一個對象都具有1個對象鎖, 包括在方法體內(nèi)定義的局部變量對象.
某1個對象鎖只能被1個線程占用,? 如果一個線程占用了1個對象的鎖, 那么其他線程嘗試去占用這個對象鎖時就會失敗.
我們可以利用這個機制去達(dá)到某些代碼在多線程中互斥的目的.
4.2.2 synchronized 為對象加鎖的用法
用法如下
synchronized (object){
?? 代碼...;
}
注意, object 是1個對象
那么當(dāng)1個線程A執(zhí)行這段代碼時, 就嘗試去獲得這個對象object的鎖, 如果獲取成功, 則執(zhí)行里面的代碼.
如果這個對象的鎖已經(jīng)被其他某條線程占用, 則線程A進(jìn)入阻塞狀態(tài),暫停執(zhí)行. 直至這個對象的鎖被其他線程釋放并且線程A成功占用該對象的鎖.
下面是例子, 還是上面的例子做下修改:
package Thread_kng.Td_synchronized;class Sell_ticket5 extends Thread{public static int tickets = 50; //count of ticketspublic static int unit_price = 30; //unit_price of per ticketpublic static int sum_price = 0; //total turnoverprivate int pre_counts;public Sell_ticket5(String name){super(name);pre_counts = this.tickets;}//verify account of buyerprivate void accountVerify(){System.out.printf("Thread %s: account is valid!\n", this.getName());}//previous preparationprivate void pre_Jobs(){System.out.printf("Thread %s: it's prepared!\n", this.getName());}private static int i_flag;private static String s_flag = "flag_of_objectlock"; //assignmen is needed, otherwise will //throw exception when being synchronizedprivate void sellTicket(){this.accountVerify();this.pre_Jobs();//synchronized (i_flag) //error integer variable is not an objectsynchronized (s_flag){ //try to lock the object i_flagif (this.tickets > 0){System.out.printf("Thread %s: sold the no.%d ticket!\n", this.getName(),pre_counts - tickets + 1);tickets--;sum_price+=unit_price;}else{System.out.printf("Thread %s: all the tickets are sold out!\n", this.getName(),pre_counts - tickets + 1);}}}public void run(){while(this.tickets > 0){this.sellTicket(); }System.out.printf("Turnover is %d\n", Sell_ticket5.sum_price);} }public class Td_syn_5{public static void st(){Sell_ticket5 a = new Sell_ticket5("A");Sell_ticket5 b = new Sell_ticket5("B");Sell_ticket5 c = new Sell_ticket5("C");a.start();b.start();c.start();try{Thread.currentThread().sleep(5000);}catch(Exception e){e.printStackTrace();}System.out.printf("Turnover is %d\n", Sell_ticket5.sum_price);} }上面的例子中, sellTicket方法沒有synchronized 修飾.
但是我定義了1個公共(靜態(tài))對象String s_flag.
在sellTicket方法把關(guān)鍵代碼寫進(jìn) synchronzied(s_flag)中.
這樣的話sellTicket實際上也可以被稱為同步的, 因為關(guān)鍵代碼會在多線程中互斥.
執(zhí)行結(jié)果:
可以見到結(jié)果正確, 而且執(zhí)行狀態(tài)是幾種方法中最佳的.
4.2.3 synchronized 為對象加鎖的要注意的地方
1. 只能為對象加鎖,? synchronized 括號里面的只能是1個對象名, 而不能是類名.
2. int類型的變量不是對象. 所以不能為int類型的變量加鎖
3. String類型的變量是對象, 一般情況下為1個String對象加鎖, 比較方便.
4. 這個對象必須被實例化, 也就是有內(nèi)存指向, 否則拋出異常,? 對于String對象來講, 如果要為其加鎖, 則這個String變量必須被復(fù)制.
5. 如果多個線程由多個線程對象創(chuàng)建(如上面的例子), 則這個對象必須是1個類靜態(tài)(公共)變量, 如果是類的非靜態(tài)成員則達(dá)不到線程互斥的母的.
?? 因為多個線程里對象里的非靜態(tài)成員的內(nèi)存地址是不同的, 多個線程為各自的成員加鎖肯定是成功的.
?? 所以上面的例子中的s_flag 必須是1個靜態(tài)變量, 它只屬于類本身.
4.3 synchronized 一些要點:
實在上, 如果為1個非靜態(tài)的方法f() 加上synchronized 修飾
synchronized void f(){
?? ....
}
就相當(dāng)于
void f(){
?synchronized (this){
?? ....
?}
}
只有該對象的線程調(diào)用該方法才是互斥的.
也就是獲取對象本身的鎖.
而為1個靜態(tài)方法g()方法加上synchronize 修飾.
實際上就是為類本身上鎖, 該類所有對象的線程調(diào)用該方法都是互斥的.
五.小結(jié)
關(guān)于java線程同步其實并不復(fù)雜. 個人覺得比oracle的鎖還好理解一點. 關(guān)鍵就是熟悉synchronized 的用法.
總結(jié)
以上是生活随笔為你收集整理的java同步机制简单介绍的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java里的线程控制
- 下一篇: Java 的toString() 和 e