1、線程的實現
一、線程的三種實現方式
首先併發並非咱們一般咱們認爲的必須依靠線程才能實現,可是在Java中併發的實現是離不開線程的,線程的主要實現有三種方式:java
使用內核線程(Kernel Thread,KLT)實現
使用用戶線程實現sql
使用用戶線程加輕量級進程混合實現數據庫
(1)使用內核線程(Kernel Thread,KLT)實現:安全
直接由OS(操做系統)內核(Kernel)支持的線程,程序中通常不會使用內核線程,而是會使用內核線程的高級接口,即輕量級進程(Light Weight Process,LWP),也就是一般意義上的線程。多線程
每一個輕量級線程與內核線程之間1:1的關係稱之爲一對一的線程模型。架構
優勢:每一個LWP是一個獨立調度單元,即便阻塞了,也不會影響整個進程。併發
缺點:須要在User Mode與Kernel Mode中來回切換,系統調用代價比較高;因爲內核線程的支持會消耗必定的內核資源,所以一個系統支持輕量級進程的數量是有限的。分佈式
(2)使用用戶線程實現:ide
廣義上來講,一個線程只要不是內核線程就能夠認爲是用戶線程(User Thread,UT),但其實現仍然創建在內核之上;狹義上來講,就是UT是指徹底創建在用戶空間的線程庫上,Kernel徹底不能感到線程的實現,線程的全部操做徹底在User Mode中完成,不須要內核幫助(部分高性能數據庫中的多線程就是UT實現的)高併發
缺點:全部的線程都須要用戶程序本身處理,以致於「阻塞如何解決」等問題很難解決,甚至沒法實現。因此如今Java等語言中已經拋棄使用用戶線程。
優勢:不須要內核支持
(3)使用用戶線程加輕量級進程混合實現:
內核線程與用戶線程一塊兒使用的實現方式,而OS提供支持的輕量級進程則是做爲用戶線程與內核線程之間的橋樑。UT與LWP的數量比是不定的,是M:N的關係(許多Unix系列的OS都提供M:N的線程模型)
二、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次方)
2、線程安全
一、Java中五種共享數據
(1)不可變:典型的final修飾是不可變的(在構造器結束以後),還有String對象以及枚舉類型這些自己不可變的。
(2)絕對線程安全:無論運行時環境如何,調用者都不須要任何額外的同步措施(一般須要很大甚至不切實際的代價),在Java API中不少線程安全的類大多數都不是絕對線程安全,好比java.util.Vector是一個線程安全容器,它的不少方法(get()、add()、size())方法都是被synchronized修飾,可是並不表明調用它的時候就不須要同步手段了。
(3)相對線程安全:就是咱們一般說的線程安全,Java API中不少這樣的例子,好比HashTable、Vector等。
(4)線程兼容:就是咱們一般說的線程不安全的,須要額外的同步措施才能保證併發環境下安全使用,好比ArrayList和HashMap
(5)線程對立:無論採用何種手段,都沒法在多線程環境中併發使用。
二、線程安全的實現方法
(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 = 51000;//等待時間
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, 歡迎工做一到五年的Java工程師朋友們加入Java羣: 891219277羣內提供免費的Java架構學習資料(裏面有高可用、高併發、高性能及分佈式、Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用本身每一分每一秒的時間來學習提高本身,不要再用"沒有時間「來掩飾本身思想上的懶惰!趁年輕,使勁拼,給將來的本身一個交代!ThreadLocalHashCode-LocalValue),ThreadLocal就是當前線程ThreadLocalMap的入口。