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

來源:互聯網
上載者:User

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行業人員進入!

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.