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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...