Java線程實現與安全

来源:https://www.cnblogs.com/jian0110/archive/2018/08/08/9446060.html
-Advertisement-
Play Games

目錄 1. 線程的實現 線程的三種實現方式 Java線程的實現與調度 2. 線程安全 Java的五種共用數據 保證線程安全的三種方式 前言 本篇博文主要是是在Java記憶體模型的基礎上介紹Java線程更多的內部細節,但不是簡單的代碼舉例,更多的是一些理論概念,可以說是對自己的一種理論知識的補充 註:建 ...


目錄

1. 線程的實現

  線程的三種實現方式

  Java線程的實現與調度  

2. 線程安全

  Java的五種共用數據

  保證線程安全的三種方式

 


 

前言

本篇博文主要是是在Java記憶體模型的基礎上介紹Java線程更多的內部細節,但不是簡單的代碼舉例,更多的是一些理論概念,可以說是對自己的一種理論知識的補充

註:建議先瞭解Java的記憶體模型,再理解本篇博文效果更佳。具體可以看我的總結的關於Java記憶體模型的博文

本文主要參考《深入理解JVM》中高效併發編程部分

 


 

一、線程的實現

1、線程的三種實現方式  

  首先併發並不是我們通常我們認為的必須依靠線程才能實現,但是在Java中併發的實現是離不開線程的,線程的主要實現有三種方式:

  • 使用內核線程(Kernel ThreadKLT)實現
  • 使用用戶線程實現

  • 使用用戶線程加輕量級進程混合實現

  (1)使用內核線程(Kernel ThreadKLT)實現:

  直接由OS(操作系統)內核(Kernel)支持的線程,程式中一般不會使用內核線程,而是會使用內核線程的高級介面,即輕量級進程(Light Weight ProcessLWP,也就是通常意義上的線程

每個輕量級線程與內核線程之間1:1的關係稱之為一對一的線程模型

  優點:每個LWP是一個獨立調度單元,即使阻塞了,也不會影響整個進程

  缺點:需要在User ModeKernel Mode中來回切換,系統調用代價比較高由於內核線程的支持會消耗一定的內核資源,因此一個系統支持輕量級進程的數量是有限的

  (2)使用用戶線程實現:

  廣義上來說,一個線程只要不是內核線程就可以認為是用戶線程User ThreadUT,但其實現仍然建立在內核之上;狹義上來說,就是UT是指完全建立在用戶空間的線程庫上,Kernel完全不能感到線程的實現線程的所有操作完全在User Mode中完成,不需要內核幫助(部分高性能資料庫中的多線程就是UT實現的)

  缺點:所有的線程都需要用戶程式自己處理,以至於“阻塞如何解決”等問題很難解決,甚至無法實現。所以現在Java等語言中已經拋棄使用用戶線程

  優點:不需要內核支持

  (3)使用用戶線程加輕量級進程混合實現:

  內核線程與用戶線程一起使用的實現方式,而OS提供支持的輕量級進程則是作為用戶線程與內核線程之間的橋梁。UTLWP的數量比是不定的,是M:N的關係(許多Unix系列的OS都提供M:N的線程模型)

2、Java線程的實現與調度

  (1)Java線程的實現:

  OS支持怎樣的線程模型,都是由JVM的線程怎麼映射決定的。

  在Sun JDK中,WindowsLinux都是使用一對一的線程模型實現(一條Java線程映射到一條輕量級進程之中);

  在Solaris平臺中,同時支持一對一與多對多的線程模型

  (2)Java線程調度:

  是指系統內部為線程分配處理使用權的過程,主要調度分為兩種,分別是協同式線程調度和搶占式線程調度。

    1)協同式調度線程執行時間由線程本身控制,線程工作結束後主動通知系統切換到另一個線程去。    

      ① 缺點:線程執行時間不可控,切換時間不可預知。如果一直不告訴系統切換線程,那麼程式就一直阻塞在那裡。

      ② 優點:實現簡單,由於是先把線程任務完成再切換,所以切換操作對線程自己是可知的。

    2)搶占式調度線程執行時間由系統來分配,切換不由線程本身決定,Java使用就是搶占式調度。並且可以分配優先順序(Java線程中設置了10中級別),但並不是靠譜的(優先順序可能會在OS中被改變),這是因為線程調度最終被映射到OS上,由OS說了算,所以不見得與Java線程的優先順序一一對應(事實上Windows7中,Solairs中有231次方)

二、線程安全

1、Java中五種共用數據

  (1)不可變典型的final修飾是不可變的(在構造器結束之後),還有String對象以及枚舉類型這些本身不可變的。

  (2)絕對線程安全:不管運行時環境如何,調用者都不需要任何額外的同步措施(通常需要很大甚至不切實際的代價),在Java API中很多線程安全的類大多數都不是絕對線程安全,比如java.util.Vector是一個線程安全容器,它的很多方法(get()add()size())方法都是被synchronized修飾,但是並不代表調用它的時候就不需要同步手段了

  (3)相對線程安全:就是我們通常說的線程安全,Java API中很多這樣的例子,比如HashTableVector等。

  (4)線程相容:就是我們通常說的線程不安全的,需要額外的同步措施才能保證併發環境下安全使用,比如ArrayListHashMap

  (5)線程對立:不管採用何種手段,都無法在多線程環境中併發使用。

2、線程安全的實現方法

(1)互斥同步(Mutual Exclision & Synchronization)

  同步:保證同一時刻共用數據被一個線程(在使用信號量的時候也可以是一些線程)使用。

  互斥:互斥是實現同步的一種手段,臨界區、互斥量和信號量都是主要的互斥手段。

  1)Java中最常用的互斥同步手段就是synchronized關鍵字,synchronized關鍵字經過編譯後會在代碼塊前後生成monitorenter(鎖計數器加1)與monitorexit(鎖計數器減1)位元組碼指令,而這兩個指令需要一個引用類型參數指明要鎖定和解鎖的對象,也就是synchronized(object/Object.class)傳入的對象參數,如果沒有參數指定,那就看synchronized修飾的是實例方法還是類方法,去取對應的對象實例與Class對象作為鎖對象。

  Java線程要映射到OS原生線程上,也就是需要從用戶態轉為核心(系統)態,這個轉換可能消耗的時間會很長,儘管VM對synchronized做了一些優化,但還是一種重量級的操作。

  2)另一個就是java.util.concurrent包下的重入鎖(ReentrantLock),與synchronized相似,都具有線程重入(後面會介紹重入概念)特性,但是ReentrantLock有三個主要的不同於synchronized的功能:

    等待可中斷:持有鎖長時間不釋放,等待的線程可以選擇先放棄等待,改做其他事情。

    可實現公平鎖:多個線程等待同一個鎖時,是按照時間先後順序依次獲得鎖,相反非公平鎖任何一個線程都有機會獲得鎖。

    鎖綁定多個條件:是指ReentrantLock對象可以同時綁定多個Condition對象。

  JDK 1.6之後synchronized與ReentrantLock性能上基本持平,但是VM在未來改進中更傾向於synchronized,所以在大部分情況下優先考慮synchronized。

2)非阻塞同步

  1)“悲觀”併發策略------非阻塞同步概念

    互斥同步主要問題或者說是影響性能的問題是線程阻塞與喚醒問題,它是一種“悲觀”併發策略:總是會認為自己不去做相應的同步措施,無論共用數據是否存在競爭它都會去加鎖。

    而相反有一種“樂觀”併發策略,也就是先操作,如果沒有其他線程使用共用數據,那操作就算是成功了,但是如果共用數據被使用,那麼就會一直不斷嘗試,直到獲得鎖使用到共用數據為止(這是最常用的策略),這樣的話就線程就根本不需要掛起。這就是非阻塞同步(Non-Blocking Synchronization)

    使用“樂觀”併發策略需要操作和衝突檢測兩個步驟具有原子性而這個原子性只能靠硬體完成,保證一個從語義上看起來需要多次操作的行為只通過一條處理器指令就能完成。常用的指令有:測試並設置(Test-and-Set)、獲取並增加(Fetch-and-Increment)、交換(Swap)、比較並交換(Compare-and-Swap,CAS)、載入鏈接/條件儲存(Load-Linked/Store-Conditional,LL/SC)

  2)CAS介紹

有三個操作數,分別是記憶體位置V,舊的預期值A和新值BCAS指令執行時,當且僅當V符合舊預期值A時,處理器用新值B更新V的值,否則不更新,但是都會返回V的舊值,整個過程都是一個原子過程

               

                 

    之前我在Java記憶體模型博文中介紹volatile關鍵字的在高併發下並非安全的例子中,最後的結果並不是我們想要的結果,但是在java.util.concurrent整數原子類( 如AtomicInteger)中,compareAndSet()與getAndIncrement()方法使用了Unsafe類的CAS操作現在我們將int換成AtomicInteger,結果都是我們所期待的10000

 1 package cas;
 2 /**
 3  * Atomic 變數自增運算測試
 4  * @author Lijian
 5  *
 6  */
 7 import java.util.concurrent.ExecutorService;
 8 import java.util.concurrent.Executors;
 9 import java.util.concurrent.TimeUnit;
10 import java.util.concurrent.atomic.AtomicInteger;
11 
12 public class CASDemo {
13 
14     private static final int THREAD_NUM = 10;//線程數目
15     private static final long AWAIT_TIME = 5*1000;//等待時間
16     public static AtomicInteger race = new AtomicInteger(0);
17     
18     public static void increase() { race.incrementAndGet(); }
19     
20     public static void main(String[] args) throws InterruptedException {
21         ExecutorService exe = Executors.newFixedThreadPool(THREAD_NUM);
22         for (int i = 0; i < THREAD_NUM; i++) {
23             exe.execute(new Runnable() {
24                 @Override
25                 public void run() {
26                     for (int j = 0; j < 1000; j++) {
27                         increase();
28                     }
29                 }
30             });
31         }
32         //檢測ExecutorService線程池任務結束並且是否關閉:一般結合shutdown與awaitTermination共同使用
33         //shutdown停止接收新的任務並且等待已經提交的任務
34         exe.shutdown();
35         //awaitTermination等待超時設置,監控ExecutorService是否關閉
36         while (!exe.awaitTermination(AWAIT_TIME, TimeUnit.SECONDS)) {
37                 System.out.println("線程池沒有關閉");
38         }
39         System.out.println(race);
40     }
41 }

通過觀察incrementAndGet()方法源碼我們發現:

public final int getAndIncrement() {
    for(;;){
        int current = get();
        int next = current+1;
        if(compareAndSet(current, next)) {
            return current;
        }
     }               
}

 

 通過for(;;)迴圈不斷嘗試將當前current1後的新值(mext)賦值(compareAndSet)給自己,如果失敗的話就重新迴圈嘗試,值到成功為止返回current值。  

  3)CASABA問題

    這是CAS的一個邏輯漏洞,比如V值在第一次讀取的時候是A值,即沒有被改變過,這時候正要準備賦值,但是A的值真沒有被改變過嗎?

    答案是不一定的,因為在檢測A值這個過程中A的值可能被改為B最後又改回A,而CAS機制就認為它沒有被改變過,這也就是ABA問題,解決這個問題就是增加版本控制變數但是大部分情況下ABA問題不會影響程式併發的正確性。

3)無同步方案

  “要保障線程安全,必須採用相應的同步措施”這句話實際上是不成立的,因為有些本身就是線程安全的,它可能不涉及共用數據自然就不需要任何同步措施保證正確性。主要有兩類:

  1)可重入代碼(Reentrant Code

    也就是經常所說的純代碼(Pure Code),可以在任何時刻中斷它,之後轉入其他的程式(當然也包括自身的recursion)。最後返回到原程式中而不會發生任何的錯誤所有可重入的代碼都是線程安全的,而所有線程安全的代碼都是可重入的

    其主要特征是以下幾點:

    ① 不依賴存儲在堆(堆中對象是共用的)上的數據和公用的系統資源(方法區中可以共用的數據。比如:static修飾的變數,類的可以相關共用的數據),可以換句話說就是不含有全局變數等;

    ② 用到的狀態由參數形式傳入;

    ③ 不調用任何非可重入的方法。

  即可以以這樣的原則來判斷:我們如果能預測一個方法的返回結果並且方法本身是可預測的,那麼輸入相同的數據,都會得到相應我們所期待的結果,就滿足了可重入性的要求。

  2)線程本地存儲(Thread Lock Storage

    如果一段代碼中所需要的數據必須與其他代碼共用,那麼能保證將這些共用數據放到同一個可見線程內,那麼無須同步也能保證線程之間不存在競爭關係。

    在Java中如果一個變數要被多線程訪問,可以使用volatile關鍵字修飾保證可見性,如果一個變數要被某個線程共用,可以通過java.lang.ThreadLocal類實現本地存儲的功能。每個線程Thread對象都有一個ThreadLocalMapkey-value, ThreadLocalHashCode-LocalValue),ThreadLocal就是當前線程ThreadLocalMap的入口。

    註:這裡只是簡單瞭解概念,實際上ThreadLocal部分的知識尤為重要!之後會抽時間細細研究。


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

-Advertisement-
Play Games
更多相關文章
  • Celery 什麼是Celery? Celery是一種簡單/高效/靈活的即插即用的分散式任務隊列. Celery應用場景? 需要非同步處理的任務,發郵件/發簡訊/上傳等耗時的操作.最終到達提升用戶體驗的目的. Celery的模式 Celery主要是由Broker(中間人)和Worker(任務處理者)組 ...
  • 一、String的解析 1.String的含義 ①String是不可以被繼承的,String類是final類,String類是由char[]數組來存儲字元串。 ②String是不可變的字元序列,如果存儲abc則在字元串常量池中開闢長度固定為3的字元數組,無論怎麼改變均會產生新的實例。 2.Strin ...
  • 一、JDBC常用類和介面 JDBC(Java DataBase Connectivity,java資料庫連接)是一種用於執行SQL語句的Java API。JDBC是Java訪問資料庫的標準規範,可以為不同的關係型資料庫提供統一訪問,它由一組用Java語言編寫的介面和類組成。 JDBC與資料庫驅動的關 ...
  • 最近一些學習Java的小伙伴,向我請教了一些關於Java圖形化界面的問題,以下就是我對Java圖形化界面的一些總結。 一:為何J Frame無法顯示添加的顏色 public class Login extends JFrame{ public Login(){ this.setLayout(null ...
  • Demo: hello_pycharm 根目錄文件:hello_pycharm [__init__.py __pycache__ settings.py urls.py wsgi.py] App:hello [admin.py apps.py __init__.py migrations model ...
  • GIL應該是面試的一個常考題,什麼是GIL? GIL的全程是Global Interpre Lock(全局解釋器鎖)。 不是Python中有GIL,而是CPython中有全局解釋器鎖。(JPython中沒有GIL) GIL是一個互斥鎖,CPython在執行多線程的時候並不是線安全的,為了程式的安全性 ...
  • 電腦技術的演進過程 1946-1981年 電腦系統結構時代(35年) 解決電腦能力的問題 1981-2008年 網路和視窗時代(27年) 解決交互問題 2008-2016年 複雜信息系統時代(8年) 解決數據問題 2016- 人工智慧時代 解決人類的問題 編程語言有哪些呢? 編程語言的種類錯綜 ...
  • python創始人:Guido Van Rossm (荷蘭人) python的官網:https://www.python.org ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...