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

来源:http://www.cnblogs.com/8hao/archive/2016/03/02/5235435.html
-Advertisement-
Play Games

一、引言 前幾天面試,被大師虐殘了,好多基礎知識必須得重新拿起來啊。閑話不多說,進入正題。 二、為什麼要線程同步 因為當我們有多個線程要同時訪問一個變數或對象時,如果這些線程中既有讀又有寫操作時,就會導致變數值或對象的狀態出現混亂,從而導致程式異常。舉個例子,如果一個銀行賬戶同時被兩個線程操作,一個


一、引言

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

二、為什麼要線程同步

因為當我們有多個線程要同時訪問一個變數或對象時,如果這些線程中既有讀又有寫操作時,就會導致變數值或對象的狀態出現混亂,從而導致程式異常。舉個例子,如果一個銀行賬戶同時被兩個線程操作,一個取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行業人員進入!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ------------------------------------------------ 重點提示: 1、程式的註釋:單行註釋、多行註釋; ------------------------------------------------ 第1節 .Net學習路線及幾個容易混淆的概念 C#過程
  • 在我們的程式中,經常會有一些耗時較長的運算,為了保證用戶體驗,不引起界面不響應,我們一般會採用多線程操作,讓耗時操作在後臺完成,完成後再進行處理或給出提示,在運行中,也會時時去刷新界面上的進度條等顯示,必要時還要控制後臺線程中斷當前操作。 以前,類似的應用會比較麻煩,需要寫的代碼較多,也很容易出現異
  • 函數功能:該函數將指定的消息發送到一個或多個視窗。此函數為指定的視窗調用視窗程式,直到視窗程式處理完消息再返回。該函數是應用程式和應用程式之間進行消息傳遞的主要手段之一。 函數原型:LRESULT SendMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM
  • 完成Model中的findAll/updateAll/deleteAll/insert/update和delete方法~~
  • // 字串含中文 by Aone function IsIncludeChinese(Str: String): Boolean; var i: Integer; UCS4Str: UCS4String; begin Result := False; UCS4Str := UnicodeString
  • 如果要應聘高級開發工程師職務,僅僅懂得Java的基礎知識是遠遠不夠的,還必須懂得常用數據結構、演算法、網路、操作系統等知識。因此本文不會講解具體的技術,筆者綜合自己應聘各大公司的經歷,整理了一份大公司對Java高級開發工程師職位的考核綱要,希望可以幫助到需要的人。 當前,市面上有《Java XX寶典》
  • http://fanli7.net/a/JAVAbiancheng/ANT/20101003/43604.html 級別: 中級 Roderick W. Smith ,顧問和作家 2008 年6 月02 日 Ext4 是眾多Linux? 文件系統中的最新版本,它將像以前的版本一樣重要和流行。作為Li
  • LeetCode QJ 是一個很好的刷題網站.有一天和同事交流一道有意思的題目. 在這裡分享一下. 是一個在重覆數組中查找不重覆的兩個.
一周排行
    -Advertisement-
    Play Games
  • .Net8.0 Blazor Hybird 桌面端 (WPF/Winform) 實測可以完整運行在 win7sp1/win10/win11. 如果用其他工具打包,還可以運行在mac/linux下, 傳送門BlazorHybrid 發佈為無依賴包方式 安裝 WebView2Runtime 1.57 M ...
  • 目錄前言PostgreSql安裝測試額外Nuget安裝Person.cs模擬運行Navicate連postgresql解決方案Garnet為什麼要選擇Garnet而不是RedisRedis不再開源Windows版的Redis是由微軟維護的Windows Redis版本老舊,後續可能不再更新Garne ...
  • C#TMS系統代碼-聯表報表學習 領導被裁了之後很快就有人上任了,幾乎是無縫銜接,很難讓我不想到這早就決定好了。我的職責沒有任何變化。感受下來這個系統封裝程度很高,我只要會調用方法就行。這個系統交付之後不會有太多問題,更多應該是做小需求,有大的開發任務應該也是第二期的事,嗯?怎麼感覺我變成運維了?而 ...
  • 我在隨筆《EAV模型(實體-屬性-值)的設計和低代碼的處理方案(1)》中介紹了一些基本的EAV模型設計知識和基於Winform場景下低代碼(或者說無代碼)的一些實現思路,在本篇隨筆中,我們來分析一下這種針對通用業務,且只需定義就能構建業務模塊存儲和界面的解決方案,其中的數據查詢處理的操作。 ...
  • 對某個遠程伺服器啟用和設置NTP服務(Windows系統) 打開註冊表 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpServer 將 Enabled 的值設置為 1,這將啟用NTP伺服器功 ...
  • title: Django信號與擴展:深入理解與實踐 date: 2024/5/15 22:40:52 updated: 2024/5/15 22:40:52 categories: 後端開發 tags: Django 信號 松耦合 觀察者 擴展 安全 性能 第一部分:Django信號基礎 Djan ...
  • 使用xadmin2遇到的問題&解決 環境配置: 使用的模塊版本: 關聯的包 Django 3.2.15 mysqlclient 2.2.4 xadmin 2.0.1 django-crispy-forms >= 1.6.0 django-import-export >= 0.5.1 django-r ...
  • 今天我打算整點兒不一樣的內容,通過之前學習的TransformerMap和LazyMap鏈,想搞點不一樣的,所以我關註了另外一條鏈DefaultedMap鏈,主要調用鏈為: 調用鏈詳細描述: ObjectInputStream.readObject() DefaultedMap.readObject ...
  • 後端應用級開發者該如何擁抱 AI GC?就是在這樣的一個大的浪潮下,我們的傳統的應用級開發者。我們該如何選擇職業或者是如何去快速轉型,跟上這樣的一個行業的一個浪潮? 0 AI金字塔模型 越往上它的整個難度就是職業機會也好,或者說是整個的這個運作也好,它的難度會越大,然後越往下機會就會越多,所以這是一 ...
  • @Autowired是Spring框架提供的註解,@Resource是Java EE 5規範提供的註解。 @Autowired預設按照類型自動裝配,而@Resource預設按照名稱自動裝配。 @Autowired支持@Qualifier註解來指定裝配哪一個具有相同類型的bean,而@Resourc... ...