目錄 1. 線程的實現 線程的三種實現方式 Java線程的實現與調度 2. 線程安全 Java的五種共用數據 保證線程安全的三種方式 前言 本篇博文主要是是在Java記憶體模型的基礎上介紹Java線程更多的內部細節,但不是簡單的代碼舉例,更多的是一些理論概念,可以說是對自己的一種理論知識的補充 註:建 ...
目錄
1. 線程的實現
線程的三種實現方式
Java線程的實現與調度
2. 線程安全
Java的五種共用數據
保證線程安全的三種方式
前言
本篇博文主要是是在Java記憶體模型的基礎上介紹Java線程更多的內部細節,但不是簡單的代碼舉例,更多的是一些理論概念,可以說是對自己的一種理論知識的補充
註:建議先瞭解Java的記憶體模型,再理解本篇博文效果更佳。具體可以看我的總結的關於Java記憶體模型的博文
本文主要參考《深入理解JVM》中高效併發編程部分
一、線程的實現
1、線程的三種實現方式
首先併發並不是我們通常我們認為的必須依靠線程才能實現,但是在Java中併發的實現是離不開線程的,線程的主要實現有三種方式:
- 使用內核線程(Kernel Thread,KLT)實現
-
使用用戶線程實現
-
使用用戶線程加輕量級進程混合實現
(1)使用內核線程(Kernel Thread,KLT)實現:
直接由OS(操作系統)內核(Kernel)支持的線程,程式中一般不會使用內核線程,而是會使用內核線程的高級介面,即輕量級進程(Light Weight Process,LWP),也就是通常意義上的線程。
每個輕量級線程與內核線程之間1:1的關係稱之為一對一的線程模型。
優點:每個LWP是一個獨立調度單元,即使阻塞了,也不會影響整個進程。
缺點:需要在User Mode與Kernel Mode中來回切換,系統調用代價比較高;由於內核線程的支持會消耗一定的內核資源,因此一個系統支持輕量級進程的數量是有限的。
(2)使用用戶線程實現:
廣義上來說,一個線程只要不是內核線程就可以認為是用戶線程(User Thread,UT),但其實現仍然建立在內核之上;狹義上來說,就是UT是指完全建立在用戶空間的線程庫上,Kernel完全不能感到線程的實現,線程的所有操作完全在User Mode中完成,不需要內核幫助(部分高性能資料庫中的多線程就是UT實現的)
缺點:所有的線程都需要用戶程式自己處理,以至於“阻塞如何解決”等問題很難解決,甚至無法實現。所以現在Java等語言中已經拋棄使用用戶線程。
優點:不需要內核支持
(3)使用用戶線程加輕量級進程混合實現:
內核線程與用戶線程一起使用的實現方式,而OS提供支持的輕量級進程則是作為用戶線程與內核線程之間的橋梁。UT與LWP的數量比是不定的,是M:N的關係(許多Unix系列的OS都提供M:N的線程模型)
2、Java線程的實現與調度
(1)Java線程的實現:
OS支持怎樣的線程模型,都是由JVM的線程怎麼映射決定的。
在Sun JDK中,Windows與Linux都是使用一對一的線程模型實現(一條Java線程映射到一條輕量級進程之中);
在Solaris平臺中,同時支持一對一與多對多的線程模型
(2)Java線程調度:
是指系統內部為線程分配處理使用權的過程,主要調度分為兩種,分別是協同式線程調度和搶占式線程調度。
1)協同式調度:線程執行時間由線程本身控制,線程工作結束後主動通知系統切換到另一個線程去。
① 缺點:線程執行時間不可控,切換時間不可預知。如果一直不告訴系統切換線程,那麼程式就一直阻塞在那裡。
② 優點:實現簡單,由於是先把線程任務完成再切換,所以切換操作對線程自己是可知的。
2)搶占式調度:線程執行時間由系統來分配,切換不由線程本身決定,Java使用就是搶占式調度。並且可以分配優先順序(Java線程中設置了10中級別),但並不是靠譜的(優先順序可能會在OS中被改變),這是因為線程調度最終被映射到OS上,由OS說了算,所以不見得與Java線程的優先順序一一對應(事實上Windows有7中,Solairs中有2的31次方)
二、線程安全
1、Java中五種共用數據
(1)不可變:典型的final修飾是不可變的(在構造器結束之後),還有String對象以及枚舉類型這些本身不可變的。
(2)絕對線程安全:不管運行時環境如何,調用者都不需要任何額外的同步措施(通常需要很大甚至不切實際的代價),在Java API中很多線程安全的類大多數都不是絕對線程安全,比如java.util.Vector是一個線程安全容器,它的很多方法(get()、add()、size())方法都是被synchronized修飾,但是並不代表調用它的時候就不需要同步手段了。
(3)相對線程安全:就是我們通常說的線程安全,Java API中很多這樣的例子,比如HashTable、Vector等。
(4)線程相容:就是我們通常說的線程不安全的,需要額外的同步措施才能保證併發環境下安全使用,比如ArrayList和HashMap
(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和新值B,CAS指令執行時,當且僅當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(;;)迴圈不斷嘗試將當前current加1後的新值(mext)賦值(compareAndSet)給自己,如果失敗的話就重新迴圈嘗試,值到成功為止返回current值。
3)CAS的ABA問題
這是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對象都有一個ThreadLocalMap(key-value, ThreadLocalHashCode-LocalValue),ThreadLocal就是當前線程ThreadLocalMap的入口。
註:這裡只是簡單瞭解概念,實際上ThreadLocal部分的知識尤為重要!之後會抽時間細細研究。