多線程與高併發(二)線程安全

来源:https://www.cnblogs.com/yuanqinnan/archive/2019/06/30/11111788.html
-Advertisement-
Play Games

上一篇學習了多線程的一些基礎知識:多線程的基本概念,及創建和操作多線程。內容相對簡單,但多線程的知識肯定不會這麼簡單,否則我們也不需要花這麼多心思去學習,因為多線程中容易出現線程安全問題。 那麼什麼是線程安全呢,定義如下: 當多個線程訪問同一個對象時,如果不用考慮這些線程在運行時環境下的調度和交替運 ...


上一篇學習了多線程的一些基礎知識:多線程的基本概念,及創建和操作多線程。內容相對簡單,但多線程的知識肯定不會這麼簡單,否則我們也不需要花這麼多心思去學習,因為多線程中容易出現線程安全問題。

那麼什麼是線程安全呢,定義如下:

當多個線程訪問同一個對象時,如果不用考慮這些線程在運行時環境下的調度和交替運行,也不需要進行額外的同步,或者在調用方進行任何其他的協調操作,調用這個對象的行為都可以獲取正確的結果,那這個對象是線程安全的。

簡單的理解就是在多線程情況下代碼的運行結果與預期的正確結果不一致,而產生線程安全的問題一般是由是主記憶體和工作記憶體數據不一致性重排序導致的。

要理解這些的必須先理解java的記憶體模型。

一 Java記憶體模型

在併發編程領域,有兩個關鍵問題:線程之間的通信同步

1.1 通信與同步

線程通信是指線程之間以何種機制來交換信息,在命令式編程中,線程之間的通信機制有兩種共用記憶體消息傳遞

在共用記憶體的併發模型里,線程之間共用程式的公共狀態,線程之間通過寫-讀記憶體中的公共狀態來隱式進行通信,典型的共用記憶體通信方式就是通過共用對象進行通信。

在消息傳遞的併發模型里,線程之間沒有公共狀態,線程之間必須通過明確的發送消息來顯式進行通信,在java中典型的消息傳遞方式就是wait()和notify()。

線程同步是指程式用於控制不同線程之間操作發生相對順序的機制。在共用記憶體併發模型里,同步是顯式進行的。程式員必須顯式指定某個方法或某段代碼需要線上程之間互斥執行。在消息傳遞的併發模型里,由於消息的發送必須在消息的接收之前,因此同步是隱式進行的。

java記憶體模型是共用記憶體的併發模型,線程之間主要通過讀-寫共用變數來完成隱式通信。如果不能理解Java的共用記憶體模型在編寫併發程式時一定會遇到各種各樣關於記憶體可見性的問題。

1.2 java記憶體模型(JMM)

CPU的處理速度和主存的讀寫速度不是一個量級的,為了平衡這種巨大的差距,每個CPU都會有緩存。因此,共用變數會先放在主存中,每個線程都有屬於自己的工作記憶體,並且會把位於主存中的共用變數拷貝到自己的工作記憶體,之後的讀寫操作均使用位於工作記憶體的變數副本,併在某個時刻將工作記憶體的變數副本寫回到主存中去。JMM就從抽象層次定義了這種方式,並且JMM決定了一個線程對共用變數的寫入何時對其他線程是可見的。

如圖為JMM抽象示意圖,線程A和線程B之間要完成通信的話,要經歷如下兩步:

  1. 線程A從主記憶體中將共用變數讀入線程A的工作記憶體後併進行操作,之後將數據重新寫回到主記憶體中;

  2. 線程B從主存中讀取最新的共用變數

java的記憶體模型內容還有很多,推薦看這篇文章:https://blog.csdn.net/suifeng3051/article/details/52611310

1.3 可見性和競爭現象

當對象和變數存儲到電腦的各個記憶體區域時,必然會面臨一些問題,其中最主要的兩個問題是:

  1. 共用對象對各個線程的可見性

  2. 共用對象的競爭現象

共用對象的可見性

當多個線程同時操作同一個共用對象時,如果沒有合理的使用volatile和synchronization關鍵字,一個線程對共用對象的更新有可能導致其它線程不可見。

一個CPU中的線程讀取主存數據到CPU緩存,然後對共用對象做了更改,但CPU緩存中的更改後的對象還沒有flush到主存,此時線程對共用對象的更改對其它CPU中的線程是不可見的。最終就是每個線程最終都會拷貝共用對象,而且拷貝的對象位於不同的CPU緩存中。

要解決共用對象可見性這個問題,我們可以使用volatile關鍵字,volatile 關鍵字可以保證變數會直接從主存讀取,而對變數的更新也會直接寫到主存,這個後面會詳講。

競爭現象

如果多個線程共用一個對象,如果它們同時修改這個共用對象,這就產生了競爭現象。

線程A和線程B共用一個對象obj。假設線程A從主存讀取Obj.count變數到自己的CPU緩存,同時,線程B也讀取了Obj.count變數到它的CPU緩存,並且這兩個線程都對Obj.count做了加1操作。此時,Obj.count加1操作被執行了兩次,不過都在不同的CPU緩存中。

要解決競爭現象我們可以使用synchronized代碼塊。synchronized代碼塊可以保證同一個時刻只能有一個線程進入代碼競爭區,synchronized代碼塊也能保證代碼塊中所有變數都將會從主存中讀,當線程退出代碼塊時,對所有變數的更新將會flush到主存,不管這些變數是不是volatile類型的。

二 重排序

指令重排序是指編譯器和處理器為了提高性能對指令進行重新排序,重排序一般有以下三種:

  1. 編譯器優化重排序:編譯器在不改變單線程程式語義的前提下,可以重新安排語句的執行順序。

  2. 指令級並行的重排序:如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。

  3. 記憶體系統的重排序:處理器使用緩存和讀寫緩衝區,這使得載入和存儲操作看上去可能是在亂序執行。

1屬於編譯器重排序,而2和3統稱為處理器重排序。這些重排序會導致線程安全的問題,JMM確保在不同的編譯器和不同的處理器平臺之上,通過插入特定類型的Memory Barrier來禁止特定類型的編譯器重排序和處理器重排序,為上層提供一致的記憶體可見性保證。

那麼什麼情況下一定不會重排序呢?編譯器和處理器不會改變存在數據依賴性關係的兩個操作的執行順序,即不會重排序,這裡有個數據依賴性概念是什麼意思呢?看如下代碼:

int a = 1;//A
int b = 2;//B
int c = a + b;//c

這段代碼中A和B沒有任何關係,改變A和B的執行順序,不會對結果產生影響,這裡就可以對A和B進行指令重排序,因為不管是先執行A或者B都對結果沒有影響,這個時候就說這兩個操作不存在數據依賴性,數據依賴性是指如果兩個操作訪問同一個變數,且這兩個操作有一個為寫操作,此時這兩個操作就存在數據依賴性,如果我們對變數a進行了寫操作,後又進行了讀取操作,那麼這兩個操作就是有數據依賴性,這個時候就不能進行指令重排序,這個很好理解,因為如果重排序的話會影響結果。

這裡還有一個概念要理解:as-if-serial:不管怎麼重排序,單線程下的執行結果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語義。

這裡也比較好理解,就是在單線程情況下,重排序不能影響執行結果,這樣程式員不必擔心單線程中重排序的問題干擾他們,也無需擔心記憶體可見性問題。

三 happens-before規則

我們知道處理器和編譯器會對指令進行重排序,但是如果要我們去瞭解底層的規則,那對我們來說負擔太大了,因此,JMM為程式員在上層提供了規則,這樣我們就可以根據規則去推論跨線程的記憶體可見性問題,而不用再去理解底層重排序的規則。

3.1 happens-before

我們無法就所有場景來規定某個線程修改的變數何時對其他線程可見,但是我們可以指定某些規則,這規則就是happens-before。

在JMM中,如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作之間必須存在happens-before關係。

因此,JMM可以通過happens-before關係向程式員提供跨線程的記憶體可見性保證(如果A線程的寫操作a與B線程的讀操作b之間存在happens-before關係,儘管a操作和b操作在不同的線程中執行,但JMM向程式員保證a操作將對b操作可見)。具體的定義為:

  1. 如果一個操作happens-before另一個操作,那麼第一個操作的執行結果將對第二個操作可見,而且第一個操作的執行順序排在第二個操作之前。
  2. 兩個操作之間存在happens-before關係,並不意味著Java平臺的具體實現必須要按照happens-before關係指定的順序來執行。如果重排序之後的執行結果,與按happens-before關係來執行的結果一致,那麼這種重排序並不非法(也就是說,JMM允許這種重排序)。

3.2 具體規則

具體的規則有8條:

  1. 程式次序規則:一個線程內,按照代碼順序,書寫在前面的操作先行發生於書寫在後面的操作。

  2. 鎖定規則:一個unLock操作先行發生於後面對同一個鎖額lock操作。

  3. volatile變數規則:對一個變數的寫操作先行發生於後面對這個變數的讀操作。

  4. 傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C。

  5. 線程啟動規則:Thread對象的start()方法先行發生於此線程的每個一個動作。

  6. 線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生。

  7. 線程終結規則:線程中所有的操作都先行發生於線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行。

  8. 對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始。

參考文章:

https://blog.csdn.net/suifeng3051/article/details/52611310


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

-Advertisement-
Play Games
更多相關文章
  • Django form表單 組件 [TOC] Form 組件介紹 普通方式手寫註冊功能 views.py 使用form組件實現註冊功能 views.py 先定義好一個RegForm類: 再寫一個視圖函數: login2.html Form 常用欄位與插件 常用欄位(必備) 欄位參數(必備) 內置驗證 ...
  • 內容簡介 本書是深度學習真正意義上的入門書,深入淺出地剖析了深度學習的原理和相關技術。書中使用Python3,儘量不依賴外部庫或工具,從基本的數學知識出發,帶領讀者從零創建一個經典的深度學習網路,使讀者在此過程中逐步理解深度學習。書中不僅介紹了深度學習和神經網路的概念、特征等基礎知識,對誤差反向傳播 ...
  • 這玩意兒基本上還是遍歷的那一套, 這裡使用先序遍歷的方式,直接對左右子樹進行對調即可。 (雖然看題目的時候,感覺都一樣,但真正寫出來之後,印象還是深刻了很多) ...
  • 合併,就是兩個樹的結構交集部分,數據相加,否則,取非空部分。 所以,這裡相當於是對兩棵樹同時遍歷: 如果兩棵樹節點都不為空,則數據相加, 否則,直接指針把不為空的節點複製過來。 註:這裡沒有申請記憶體,而直接對原有的樹進行改造,這樣可以節省申請記憶體的時間,且節省一些記憶體。 ...
  • 很多題目涉及到二叉樹,所以先把二叉樹的一些基本的創建和遍歷寫一下,方便之後的本地代碼調試。 為了方便,這裡使用的數據為char類型數值,初始化數據使用一個數組。 因為這些東西比較簡單,這裡就不做過多詳述。 創建 1、定義一些內容: 2、使用遞歸方式創建原始二叉樹。 其基本思想與先序遍歷基本一樣,只不 ...
  • Python反射和內置方法(雙下方法) 一、反射 1. 什麼是反射 反射的概念是由Smith在1982年首次提出的,主要是指程式可以訪問、檢測和修改它本身狀態或行為的一種能力(自省)。這一概念的提出很快引發了電腦科學領域關於應用反射性的研究。它首先被程式語言的設計領域所採用,併在Lisp和麵向對象 ...
  • 依賴註入的概念 當一個對象要調用另一個對象時,一般是new一個被調用的對象,示例: class A{ private B b=new B(); public void test(){ b.say(); } } A類的對象依賴於B類對象,如果沒有B類對象,A類對象就不能正常工作,稱A依賴於B。 以上方 ...
  • 外觀模式:為子系統中的一組介面提供一個一致的界面,該模式定義了一個高層介面,這個介面使這個子系統更加容易使用。 我們先看一個使用外觀模式的例子,首先定義了三個子系統類 使用外觀模式定義了一個高層介面,這個介面定義了調用子系統的方法 介面調用子系統的具體實現 使用外觀模式的調用方法 執行結果: met ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...