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部分的知識尤為重要!之后會抽時間細細研究。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM