程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> Java學習筆記---多線程同步的五種方法,java學習筆記

Java學習筆記---多線程同步的五種方法,java學習筆記

編輯:JAVA綜合教程

Java學習筆記---多線程同步的五種方法,java學習筆記


一、引言

前幾天面試,被大師虐殘了,好多基礎知識必須得重新拿起來啊。閒話不多說,進入正題。

二、為什麼要線程同步

因為當我們有多個線程要同時訪問一個變量或對象時,如果這些線程中既有讀又有寫操作時,就會導致變量值或對象的狀態出現混亂,從而導致程序異常。舉個例子,如果一個銀行賬戶同時被兩個線程操作,一個取100塊,一個存錢100塊。假設賬戶原本有0塊,如果取錢線程和存錢線程同時發生,會出現什麼結果呢?取錢不成功,賬戶余額是100.取錢成功了,賬戶余額是0.那到底是哪個呢?很難說清楚。因此多線程同步就是要解決這個問題。

三、不同步時的代碼

Bank.java

  1. packagethreadTest;
  2.  
  3. /**
  4. *@authorww
  5. *
  6. */
  7. publicclassBank{
  8.  
  9. privateintcount=0;//賬戶余額
  10.  
  11. //存錢
  12. publicvoidaddMoney(intmoney){
  13. count+=money;
  14. System.out.println(System.currentTimeMillis()+"存進:"+money);
  15. }
  16.  
  17. //取錢
  18. publicvoidsubMoney(intmoney){
  19. if(count-money<0){
  20. System.out.println("余額不足");
  21. return;
  22. }
  23. count-=money;
  24. System.out.println(+System.currentTimeMillis()+"取出:"+money);
  25. }
  26.  
  27. //查詢
  28. publicvoidlookMoney(){
  29. System.out.println("賬戶余額:"+count);
  30. }
  31. }

SyncThreadTest.java

  1. packagethreadTest;
  2.  
  3.  
  4. publicclassSyncThreadTest{
  5.  
  6. publicstaticvoidmain(Stringargs[]){
  7. finalBankbank=newBank();
  8.  
  9. Threadtadd=newThread(newRunnable(){
  10.  
  11. @Override
  12. publicvoidrun(){
  13. //TODOAuto-generatedmethodstub
  14. while(true){
  15. try{
  16. Thread.sleep(1000);
  17. }catch(InterruptedExceptione){
  18. //TODOAuto-generatedcatchblock
  19. e.printStackTrace();
  20. }
  21. bank.addMoney(100);
  22. bank.lookMoney();
  23. System.out.println("n");
  24.  
  25. }
  26. }
  27. });
  28.  
  29. Threadtsub=newThread(newRunnable(){
  30.  
  31. @Override
  32. publicvoidrun(){
  33. //TODOAuto-generatedmethodstub
  34. while(true){
  35. bank.subMoney(100);
  36. bank.lookMoney();
  37. System.out.println("n");
  38. try{
  39. Thread.sleep(1000);
  40. }catch(InterruptedExceptione){
  41. //TODOAuto-generatedcatchblock
  42. e.printStackTrace();
  43. }
  44. }
  45. }
  46. });
  47. tsub.start();
  48.  
  49. tadd.start();
  50. }
  51.  
  52.  
  53.  
  54. }

代碼很簡單,我就不解釋了,看看運行結果怎樣呢?截取了其中的一部分,是不是很亂,有寫看不懂。

  1. 余額不足
  2. 賬戶余額:0
  3.  
  4.  
  5. 余額不足
  6. 賬戶余額:100
  7.  
  8.  
  9. 1441790503354存進:100
  10. 賬戶余額:100
  11.  
  12.  
  13. 1441790504354存進:100
  14. 賬戶余額:100
  15.  
  16.  
  17. 1441790504354取出:100
  18. 賬戶余額:100
  19.  
  20.  
  21. 1441790505355存進:100
  22. 賬戶余額:100
  23.  
  24.  
  25. 1441790505355取出:100
  26. 賬戶余額:100

四、使用同步時的代碼

(1)同步方法:

即有synchronized關鍵字修飾的方法。由於java的每個對象都有一個內置鎖,當用此關鍵字修飾方法時,內置鎖會保護整個方法。在調用該方法前,需要獲得內置鎖,否則就處於阻塞狀態。

修改後的Bank.java

  1. packagethreadTest;
  2.  
  3. /**
  4. *@authorww
  5. *
  6. */
  7. publicclassBank{
  8.  
  9. privateintcount=0;//賬戶余額
  10.  
  11. //存錢
  12. publicsynchronizedvoidaddMoney(intmoney){
  13. count+=money;
  14. System.out.println(System.currentTimeMillis()+"存進:"+money);
  15. }
  16.  
  17. //取錢
  18. publicsynchronizedvoidsubMoney(intmoney){
  19. if(count-money<0){
  20. System.out.println("余額不足");
  21. return;
  22. }
  23. count-=money;
  24. System.out.println(+System.currentTimeMillis()+"取出:"+money);
  25. }
  26.  
  27. //查詢
  28. publicvoidlookMoney(){
  29. System.out.println("賬戶余額:"+count);
  30. }
  31. }

再看看運行結果:

  1. 余額不足
  2. 賬戶余額:0
  3.  
  4.  
  5. 余額不足
  6. 賬戶余額:0
  7.  
  8.  
  9. 1441790837380存進:100
  10. 賬戶余額:100
  11.  
  12.  
  13. 1441790838380取出:100
  14. 賬戶余額:0
  15. 1441790838380存進:100
  16. 賬戶余額:100
  17.  
  18.  
  19.  
  20.  
  21. 1441790839381取出:100
  22. 賬戶余額:0

瞬間感覺可以理解了吧。

注: synchronized關鍵字也可以修飾靜態方法,此時如果調用該靜態方法,將會鎖住整個類

(2)同步代碼塊

即有synchronized關鍵字修飾的語句塊。被該關鍵字修飾的語句塊會自動被加上內置鎖,從而實現同步

Bank.java代碼如下:

  1. packagethreadTest;
  2.  
  3. /**
  4. *@authorww
  5. *
  6. */
  7. publicclassBank{
  8.  
  9. privateintcount=0;//賬戶余額
  10.  
  11. //存錢
  12. publicvoidaddMoney(intmoney){
  13.  
  14. synchronized(this){
  15. count+=money;
  16. }
  17. System.out.println(System.currentTimeMillis()+"存進:"+money);
  18. }
  19.  
  20. //取錢
  21. publicvoidsubMoney(intmoney){
  22.  
  23. synchronized(this){
  24. if(count-money<0){
  25. System.out.println("余額不足");
  26. return;
  27. }
  28. count-=money;
  29. }
  30. System.out.println(+System.currentTimeMillis()+"取出:"+money);
  31. }
  32.  
  33. //查詢
  34. publicvoidlookMoney(){
  35. System.out.println("賬戶余額:"+count);
  36. }
  37. }

運行結果如下:

  1. 余額不足
  2. 賬戶余額:0
  3.  
  4.  
  5. 1441791806699存進:100
  6. 賬戶余額:100
  7.  
  8.  
  9. 1441791806700取出:100
  10. 賬戶余額:0
  11.  
  12.  
  13. 1441791807699存進:100
  14. 賬戶余額:100

效果和方法一差不多。

注:同步是一種高開銷的操作,因此應該盡量減少同步的內容。通常沒有必要同步整個方法,使用synchronized代碼塊同步關鍵代碼即可。

(3)使用特殊域變量(volatile)實現線程同步

a.volatile關鍵字為域變量的訪問提供了一種免鎖機制
b.使用volatile修飾域相當於告訴虛擬機該域可能會被其他線程更新
c.因此每次使用該域就要重新計算,而不是使用寄存器中的值
d.volatile不會提供任何原子操作,它也不能用來修飾final類型的變量

Bank.java代碼如下:

  1. packagethreadTest;
  2.  
  3. /**
  4. *@authorww
  5. *
  6. */
  7. publicclassBank{
  8.  
  9. privatevolatileintcount=0;//賬戶余額
  10.  
  11. //存錢
  12. publicvoidaddMoney(intmoney){
  13.  
  14. count+=money;
  15. System.out.println(System.currentTimeMillis()+"存進:"+money);
  16. }
  17.  
  18. //取錢
  19. publicvoidsubMoney(intmoney){
  20.  
  21. if(count-money<0){
  22. System.out.println("余額不足");
  23. return;
  24. }
  25. count-=money;
  26. System.out.println(+System.currentTimeMillis()+"取出:"+money);
  27. }
  28.  
  29. //查詢
  30. publicvoidlookMoney(){
  31. System.out.println("賬戶余額:"+count);
  32. }
  33. }

運行效果怎樣呢?

  1. 余額不足
  2. 賬戶余額:0
  3.  
  4.  
  5. 余額不足
  6. 賬戶余額:100
  7.  
  8.  
  9. 1441792010959存進:100
  10. 賬戶余額:100
  11.  
  12.  
  13. 1441792011960取出:100
  14. 賬戶余額:0
  15.  
  16.  
  17. 1441792011961存進:100
  18. 賬戶余額:100

是不是又看不懂了,又亂了。這是為什麼呢?就是因為volatile不能保證原子操作導致的,因此volatile不能代替synchronized。此外volatile會組織編譯器對代碼優化,因此能不使用它就不適用它吧。它的原理是每次要線程要訪問volatile修飾的變量時都是從內存中讀取,而不是存緩存當中讀取,因此每個線程訪問到的變量值都是一樣的。這樣就保證了同步。

(4)使用重入鎖實現線程同步

在JavaSE5.0中新增了一個java.util.concurrent包來支持同步。ReentrantLock類是可重入、互斥、實現了Lock接口的鎖,它與使用synchronized方法和快具有相同的基本行為和語義,並且擴展了其能力。
ReenreantLock類的常用方法有:
ReentrantLock() : 創建一個ReentrantLock實例
lock() : 獲得鎖
unlock() : 釋放鎖
注:ReentrantLock()還有一個可以創建公平鎖的構造方法,但由於能大幅度降低程序運行效率,不推薦使用
Bank.java代碼修改如下:

  1. packagethreadTest;
  2.  
  3. importjava.util.concurrent.locks.Lock;
  4. importjava.util.concurrent.locks.ReentrantLock;
  5.  
  6. /**
  7. *@authorww
  8. *
  9. */
  10. publicclassBank{
  11.  
  12. privateintcount=0;//賬戶余額
  13.  
  14. //需要聲明這個鎖
  15. privateLocklock=newReentrantLock();
  16.  
  17. //存錢
  18. publicvoidaddMoney(intmoney){
  19. lock.lock();//上鎖
  20. try{
  21. count+=money;
  22. System.out.println(System.currentTimeMillis()+"存進:"+money);
  23.  
  24. }finally{
  25. lock.unlock();//解鎖
  26. }
  27. }
  28.  
  29. //取錢
  30. publicvoidsubMoney(intmoney){
  31. lock.lock();
  32. try{
  33.  
  34. if(count-money<0){
  35. System.out.println("余額不足");
  36. return;
  37. }
  38. count-=money;
  39. System.out.println(+System.currentTimeMillis()+"取出:"+money);
  40. }finally{
  41. lock.unlock();
  42. }
  43. }
  44.  
  45. //查詢
  46. publicvoidlookMoney(){
  47. System.out.println("賬戶余額:"+count);
  48. }
  49. }

運行效果怎麼樣呢?

  1. 余額不足
  2. 賬戶余額:0
  3.  
  4.  
  5. 余額不足
  6. 賬戶余額:0
  7.  
  8.  
  9. 1441792891934存進:100
  10. 賬戶余額:100
  11.  
  12.  
  13. 1441792892935存進:100
  14. 賬戶余額:200
  15.  
  16.  
  17. 1441792892954取出:100
  18. 賬戶余額:100

效果和前兩種方法差不多。

如果synchronized關鍵字能滿足用戶的需求,就用synchronized,因為它能簡化代碼 。如果需要更高級的功能,就用ReentrantLock類,此時要注意及時釋放鎖,否則會出現死鎖,通常在finally代碼釋放鎖

(5)使用局部變量實現線程同步

Bank.java代碼如下:

  1. packagethreadTest;
  2.  
  3.  
  4. /**
  5. *@authorww
  6. *
  7. */
  8. publicclassBank{
  9.  
  10. privatestaticThreadLocal<Integer>count=newThreadLocal<Integer>(){
  11.  
  12. @Override
  13. protectedIntegerinitialValue(){
  14. //TODOAuto-generatedmethodstub
  15. return0;
  16. }
  17.  
  18. };
  19.  
  20.  
  21. //存錢
  22. publicvoidaddMoney(intmoney){
  23. count.set(count.get()+money);
  24. System.out.println(System.currentTimeMillis()+"存進:"+money);
  25.  
  26. }
  27.  
  28. //取錢
  29. publicvoidsubMoney(intmoney){
  30. if(count.get()-money<0){
  31. System.out.println("余額不足");
  32. return;
  33. }
  34. count.set(count.get()-money);
  35. System.out.println(+System.currentTimeMillis()+"取出:"+money);
  36. }
  37.  
  38. //查詢
  39. publicvoidlookMoney(){
  40. System.out.println("賬戶余額:"+count.get());
  41. }
  42. }

運行效果:

  1. 余額不足
  2. 賬戶余額:0
  3.  
  4.  
  5. 余額不足
  6. 賬戶余額:0
  7.  
  8.  
  9. 1441794247939存進:100
  10. 賬戶余額:100
  11.  
  12.  
  13. 余額不足
  14. 1441794248940存進:100
  15. 賬戶余額:0
  16.  
  17.  
  18. 賬戶余額:200
  19.  
  20.  
  21. 余額不足
  22. 賬戶余額:0
  23.  
  24.  
  25. 1441794249941存進:100
  26. 賬戶余額:300

看了運行效果,一開始一頭霧水,怎麼只讓存,不讓取啊?看看ThreadLocal的原理:

如果使用ThreadLocal管理變量,則每一個使用該變量的線程都獲得該變量的副本,副本之間相互獨立,這樣每一個線程都可以隨意修改自己的變量副本,而不會對其他線程產生影響。現在明白了吧,原來每個線程運行的都是一個副本,也就是說存錢和取錢是兩個賬戶,知識名字相同而已。所以就會發生上面的效果。

ThreadLocal與同步機制
a.ThreadLocal與同步機制都是為了解決多線程中相同變量的訪問沖突問題

b.前者采用以"空間換時間"的方法,後者采用以"時間換空間"的方式

現在都明白了吧。各有優劣,各有適用場景。手工,吃飯去了。

問啊-定制化IT教育平台,牛人一對一服務,有問必答,開發編程社交頭條 官方網站:www.wenaaa.com

QQ群290551701 聚集很多互聯網精英,技術總監,架構師,項目經理!開源技術研究,歡迎業內人士,大牛及新手有志於從事IT行業人員進入!

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved