咱們在編寫程序的時候,通常是有個順序的,就是先實現再優化,並非全部的牛P程序都是一次就寫出來的,確定都是不斷的優化完善來持續實現的。所以咱們在考慮實現高併發程序的時候,要先保證併發的正確性,而後在此基礎上來實現高效。因此線程安全是高併發程序首先須要保證的。java
對於線程安全的定義能夠理解爲:當多個線程同時訪問一個對象時,若是不用考慮這些線程在運行時環境下的調度和交替執行,也不須要進行額外的同步,或者在調用方進行任何其餘的協調操做,調用這個對象的行爲均可以得到正確的結果,那就稱這個對象是線程安全的。
這個定義是很嚴謹且有可操做性,它要求線程安全的代碼都必須具有一個共同特徵:代碼自己封裝了全部必要的正確性保障手段(互斥、同步等),令調用者無須關心多線程下的調用問題,更無須本身實現任何措施來保證多線程環境下的正確調用。程序員
要討論Java中的線程安全,咱們要以多個線程之間存在共享數據訪問爲前提。咱們能夠不把線程安全看成一個非真即假的二元排他選項來看待,而是按照線程安全的「安全程度」由強至弱來排序,將Java中各操做共享的數據分爲如下五類:不可變、絕對線程安全、相對相對安全、線程兼容和線程對立。數據庫
Java內存模型中,不可變的對象必定是線程安全的,不管對象的方法實現仍是方法的調用者,都不須要再進行任何線程安全保障措施。在學習Java內存模型這一篇文章中咱們在介紹Java內存模型的三個特性的可見性的時候說到,被final修飾的字段在構造器中一旦被初始化完成,而且構造器沒有吧「this」的引用傳遞出去,那麼在其餘線程中就能看見final字段的值。而且外部可見狀態永遠都不會改變,永遠都不會看到它在多個線程之中處於不一致的狀態。「不可變」帶來的安全性是最直接、最純粹的。數組
在Java中若是共享數據是一個基本類型,那麼在定義時使用final修飾它就能夠保證它是不可變的。若是共享數據是一個對象,那就須要對象自行保證其行爲不會對其狀態產生任何影響才行。例如java.lang.String
類的對象實例,它的substring()、replace()、concat()這些方法都不會影響它原來的值,只會返回一個新構造的字符串對象。
保證對象行爲不影響本身狀態的途徑有不少種,最簡單的一種就是把對象裏面帶有狀態的變量都聲明爲final,這樣在構造函數結束後,他就是不可變的。
例如java.lang.Integer
構造函數。安全
/** * The value of the {@code Integer}. * * @serial */ private final int value; /** * Constructs a newly allocated {@code Integer} object that * represents the specified {@code int} value. * * @param value the value to be represented by the * {@code Integer} object. */ public Integer(int value) { this.value = value; }
除了String以外,還有枚舉類型以及java.lang.Number
的部分子類,如Long
和Double
等數值包裝類型、BigInteger
和BigDecimal
等大數據類型。服務器
絕對線程安全是可以徹底知足上面的線程安全的定義,這個絕對線程安全的定義是很嚴格的:「無論運行時環境如何,調用者都不須要任何額外的同步措施」。Java的API中標註本身是線程安全的類,大多數都不是絕對的線程安全。
例如java.util.Vector
是一個線程安全的容器,相信全部的Java程序員對此都不會有異議,由於它的add()、get()、和size()等方法都被synhronized
修飾。可是這樣並不意味着調用它的時候,就永遠再也不須要同步手段了。多線程
public class VectorTest { private static Vector<Integer> vector = new Vector<Integer>(); public static void main(String[] args){ while (true){ for (int i=0;i<10;i++){ vector.add(i); } Thread removeThread = new Thread(new Runnable() { @Override public void run() { for (int i=0;i<vector.size();i++){ vector.remove(i); } } }); Thread printThread = new Thread(new Runnable() { @Override public void run() { for(int i=0;i<vector.size();i++){ System.out.println(vector.get(i)); } } }); removeThread.start(); printThread.start(); while (Thread.activeCount() > 20); } } }
運行結果:架構
Exception in thread "Thread-653" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 18 at java.util.Vector.get(Vector.java:748) at com.eurekaclient2.test.jvm3.VectorTest$2.run(VectorTest.java:33) at java.lang.Thread.run(Thread.java:748)
經過上述代碼的例子,就能夠看出來,儘管Vector的get()、remove()和size()方法都是同步的,可是在多線程的環境中,若是調用端不作額外的同步措施,使用這段代碼仍然是不安全的。由於在併發運行中,若是提早刪除了一個元素,然後面還要去打印它,就會拋出數組越界的異常。
若是非要這段代碼正確執行下去,就必須把removeThread
和printThread
進行加鎖操做。併發
Thread removeThread = new Thread(new Runnable() { @Override public void run() { synchronized (vector){ for (int i=0;i<vector.size();i++){ vector.remove(i); } } } }); Thread printThread = new Thread(new Runnable() { @Override public void run() { synchronized (vector){ for(int i=0;i<vector.size();i++){ System.out.println(vector.get(i)); } } } });
相對線程安全就是咱們一般意義上所講的線程安全,它須要保證對這個對象單詞的操做時線程安全的,咱們在調用的時候不須要進行額外的保證措施,可是對於一些特定順序的連續調用,就可能須要在調用端使用額外的同步手段來保證調用的正確性。上面的代碼例子就是相對線程安全的案例。jvm
線程兼容是指對象自己並不線程安全的,可是能夠經過在調用端正確地使用同步手段來保證對象在併發環境中能夠安全地使用。Java類庫API中大部分的類都是線程兼容的,如ArrayList
、HashMap
等。
線程對立是指無論調用端是否採用了同步措施,都沒法在多線程環境中併發是使用代碼。因爲Java語言天生就支持多線程的特性,此案從對立這種排斥多線程的代碼時不多出現的,並且一般都是有害的,應當儘可能避免。
Java虛擬機爲實現線程安全,提供了同步和鎖機制,在瞭解了Java虛擬機線程安全措施的原理與運做過程,再去用代碼實現線程安全就不是一件困難的事情了。
互斥同步(Mutual Exclusion & Synchronization)是一種常見也是最主要的併發正確性保障手段。
同步是指多個線程併發訪問共享數據時,保證共享數據在同一時刻只被一條線程使用。
互斥是指實現同步的一種手段,臨界區(Critical Section)、互斥量(Mutex)和信號量(Semaphore)都是常見的互斥實現方式。
在Java裏,最基本的互斥同步手段就是synchronized
關鍵字,這是一種塊結構的同步語法。在Java代碼裏若是synchronized
明確指定了對象參數,那就以這個對象的引用做爲reference
;若是沒有明確指定,那將根據synchronized
修飾的方法類型(如實例方法或類方法),來決定是取代碼所在的對象實例仍是取類型對應的Class對象來做爲線程要持有的鎖。
在使用sychronized
時須要特別注意的兩點:
synchronized
修飾的同步塊對同一條線程來講是可重入的。這意味着同一線程反覆進入同步塊也不會出現本身把本身鎖死的狀況。synchronized
修飾的同步塊在持有鎖的線程執行完畢並釋放鎖以前,會無條件地阻塞後面其餘線程的進入。這意味着沒法像處理某些數據庫中的鎖那樣,強制已獲取鎖的線程釋放鎖;也沒法強制正在等待鎖的線程中斷等待或超時退出。除了synchronized
關鍵字之外,自JDK5起,Java類庫中新提供了java.util.concurrent
包(J.U.C包),其中java.util.concurrent.locks.Lock
接口便成了Java的另外一種全新的互斥同步手段。
重入鎖(ReentrantLock)是Lock接口最多見的一種實現,它與synchronized
同樣是可重入的。在基本用法是,ReentrantLock
與synchronized
很類似,只是代碼寫法上稍有區別而已。
可是ReentrantLock
與synchronized
相比增長了一些高級特性,主要有如下三項:
synchronized
是非公平鎖,ReentrantLock在默認狀況系也是非公平鎖,但能夠經過構造函數的參數設置成公平鎖,不過一旦設置了公平鎖,ReentrantLock性能急劇降低,會明顯影響性能。synchronized
中,鎖對象的wait()跟它的notify()或者notifyAll()方法配合能夠實現一個隱含條件,若是要和多於一個的條件關聯的時候,就不得不額外添加一個鎖;而ReentrantLock則無須這樣作,屢次調用newCondition()方法便可。雖說ReentrantLock比synchronized
增長了一些高級特性,可是從JDK6對synchronized
作了不少的優化後,他倆的性能其實幾乎相差無幾了。而且在如下的幾種狀況下雖然synchronized
和ReentrantLock均可以知足需求時,建議優先使用synchronized
。
synchronized
是在Java語法層面的同步,清晰簡單。而且被普遍熟知,但J.U.C中的Lock接口並不是如此。所以在只須要基礎的同步功能時,更推薦synchronized
。synchronized
,但這已是十多年以前的勝利。從長遠看,Java虛擬機更容易針對synchronized
來進行優化,由於Java虛擬機能夠在線程和對象的元數據中記錄synchronized
中鎖的相關信息。互斥同步面臨的主要問題時進行線程阻塞和喚醒所帶來的性能開銷,所以這種同步也被稱爲阻塞同步(Blocking Synchronized)。從解決問題的角度來看,互斥同步是一種悲觀的併發策略,不管共享的數據是否真的會出現競爭,都會進行加鎖。
隨着硬件指令集的發展,出現了另外一種選擇,基於衝突檢測的樂觀併發策略,通俗地說就是無論風險,先進行操做,發生了衝突,在進行補償,最經常使用的補償就是不斷重試,直到出現沒有競爭的數據爲止。使用這種樂觀併發策略再也不須要線程阻塞掛起,所以這種同步操做被稱爲非阻塞同步(Non-Blocking Synchronized)。
在進行操做和衝突檢測時這個步驟要保證原子性,硬件能夠只經過一條處理器指令就能完成,這類指令經常使用的有:
Java類庫從JDK5以後纔開始使用CAS操做,而且該操做有sun.misc.Unsafe類裏面的compareAndSwapInt()和compareAndSwapLong()等幾個方法包裝提供。可是Unsafe的限制了不提供給用戶調用,所以在JDK9以前只有Java類庫可使用CAS,譬如J.U.C包裏面的整數原子類,其中的compareAndSet()和getAndIncrement()等方法都使用了Unsafe類的CAS操做來實現。直到JDK9,Java類庫纔在VarHandle類裏開放了面向用戶程序使用的CAS操做。
下面來看一個例子:
這是以前的一個例子在驗證volatile變量不必定徹底具有原子性的時候的代碼。20個線程自增10000次的操做最終的結果一直不會獲得200000。若是按以前的理解就會把race++操做或increase()方法用同步塊包起來。
可是若是改爲下面的代碼,效率將會提升許多。
public class AtomicTest { public static AtomicInteger race = new AtomicInteger(0); public static void increase(){ race.incrementAndGet(); } private static final int THREADS_COUNT = 20; public static void main(String[] args) throws Exception{ Thread[] threads = new Thread[THREADS_COUNT]; for (int i = 0;i<THREADS_COUNT;i++){ threads[i] = new Thread(() -> { for(int i1 = 0; i1 <10000; i1++){ increase(); } }); threads[i].start(); } while (Thread.activeCount() > 2){ Thread.yield(); } System.out.println(race); } }
運行效果:
200000
使用哦AtomicInteger代替int後,獲得了正確結果,主要歸功於incrementAndGet()方法的原子性,incrementAndGet()使用的就是CAS,在此方法內部有一個無限循環中,不斷嘗試講一個比當前值大一的新值賦值給本身。若是失敗了,那說明在執行CAS操做的時候,舊值已經發生改變,因而再次循環進行下一次操做,直到設置成功爲止。
要保證線程安全,也不必定非要用同步,線程安全與同步沒有必然關係,若是能讓一個方法原本就不涉及共享數據,那它天然就不須要任何同步措施去保證正確性,所以有一些代碼天生就是線程安全的,主要有這兩類:
可重入代碼:是指能夠在代碼執行的任什麼時候刻中斷它,而後去執行另一段代碼,而控制權返回後,原來的程序不會出現任何錯誤,也不會對結果有所影響。
可重入代碼有一些共同特徵:
不依賴全局變量、存儲在堆上的數據和公用的系統資源,用到的狀態量都由參數傳入,不調用非可重入的方法等。
簡單來講就是一個原則:若是一個方法的返回結果是能夠預測的,只要輸入了相同的數據,就能返回相同的結果,那它就知足可重入性的要求,固然也就是線程安全的。
線程本地存儲(Thread Local Storage):若是一段代碼中所需的數據必須與其餘代碼共享,那就看看這些共享數據的代碼是否能保證在同一個線程中執行。若是能,就能夠把共享數據的可見範圍限制在同一個線程內,這樣無須同步也能保證線程之間不出現數據爭用的問題。
如大部分使用消費隊列的架構模式,都會將產品的消費過程限制在一個線程中消費完,最經典一個實例就是Web交互模式中的「一個請求對應一個服務器線程」的處理方式,這種處理方式的普遍應用使得不少Web服務端應用均可以使用線程本地存儲來解決線程安全問題。
.