若是多線程下使用這個類,不過多線程如何使用和調度這個類,這個類老是表示出正確的行爲,這個類就是線程安全的。git
類的線程安全表現爲:github
不作正確的同步,在多個線程之間共享狀態的時候,就會出現線程不安全。面試
全部的變量都是在方法內部聲明的,這些變量都處於棧封閉狀態。算法
好比下面的例子,a和b都是在方法內部定義的,沒法被外部線程所訪問,當方法結束後,棧內存被回收,因此是線程安全的。後端
void fun(){ int a = 1; int b= 2; // do something }
沒有任何成員變量的類,就叫無狀態的類,這種類不存在共享的資源,顯然是安全的。安全
public class StatelessClass { public int service(int a,int b) { return a*b; } }
讓狀態不可變,兩種方式:性能優化
下面例子中的,成員變量都是final而且也沒有提供給外部修改變量的地方,所以是線程安全的。多線程
public class ImmutableFinal { private final int a; private final int b; public ImmutableFinal(int a, int b) { super(); this.a = a; this.b = b; } public int getA() { return a; } public int getB() { return b; } }
下面的例子中,雖然User成員變量是final的,沒法修改引用。可是外部依然能夠經過getUser獲取到User的引用以後,修改User對象。併發
public class ImmutableFinalRef { private final int a; private final int b; private final User user;//這裏就不能保證線程安全了 public ImmutableFinalRef(int a, int b) { super(); this.a = a; this.b = b; this.user = new User(); } public int getA() { return a; } public int getB() { return b; } public User getUser() { return user; } public static class User{ private int age; public User(int age) { super(); this.age = age; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } } public static void main(String[] args) { ImmutableFinalRef ref = new ImmutableFinalRef(12,23); User u = ref.getUser(); //u.setAge(35); } }
volitile在ConcurrentHashMap等併發容器中都有使用,用於保證變量的可見性。最適合一個線程寫,多個線程讀的情景。app
加鎖能夠顯示地控制線程對類的訪問,使用正確能夠保證線程安全。
CAS操做經過不斷的循環對比,試圖對目標對象進行修改,也能保證線程安全。普遍用於JDK併發容器的實現中。
類中持有的成員變量,特別是對象的引用,若是這個成員對象不是線程安全的,經過get等方法發佈出去,會形成這個成員對象自己持有的數據在多線程下不正確的修改,從而形成整個類線程不安全的問題。
這個類能使線程中的某個值與保存值的對象關聯起來。ThreadLocal提供了get與set等訪問接口與方法,這些方法爲使用該變量的每一個線程都存有一份獨立的副本,所以get老是返回由當前執行線程在調用set時設置的最新值。
當某個線程初次調用ThreadLocal.get方法時,就會調用initialValue來獲取初始值。從概念上講,你能夠將ThreadLocal
死鎖是指兩個或兩個以上的進程在執行過程當中,因爲競爭資源或者因爲彼此通訊而形成的一種阻塞的現象,若無外力做用,它們都將沒法推動下去。此時稱系統處於死鎖狀態或系統產生了死鎖。
死鎖的根本成因:獲取鎖的順序不一致致使。
能夠利用下面的示意圖幫助理解:
下面的程序中,兩個線程分別獲取到了first和second,而後相互等待,產生了死鎖。
public class DeadLockSample extends Thread { private String first; private String second; public DeadLockSample(String name, String first, String second) { super(name); this.first = first; this.second = second; } public void run() { synchronized (first) { System.out.println(this.getName() + " obtained: " + first); try { Thread.sleep(1000L); synchronized(second) { System.out.println(this.getName() + " obtained: " + second); } } catch (InterruptedException e) { // Do nothing } } } public static void main(String[] args) throws InterruptedException { String lockA = "lockA"; String lockB = "lockB"; DeadLockSample t1 = new DeadLockSample("Thread1", lockA, lockB); DeadLockSample t2 = new DeadLockSample("Thread2", lockB, lockA); t1.start(); t2.start(); t1.join(); t2.join(); } }
Debug時可使用 jps 或者系統的 ps 命令、任務管理器等工具,肯定進程 ID。其次,調用 jstack 獲取線程棧,jstack your_pid. jstack 自己也會把相似的簡單死鎖抽取出來,直接打印出來。
若是是開發本身的管理工具,須要用更加程序化的方式掃描服務進程、定位死鎖,能夠考慮使用 Java 提供的標準管理 API,ThreadMXBean,其直接就提供 findDeadlockedThreads() 方法用於定位,上面的例子中用到了這個方法。
若是可能的話,儘可能避免使用多個鎖,而且只有須要時才持有鎖。
若是必須使用多個鎖,儘可能設計好鎖的獲取順序。若是對於兩個線程的狀況,能夠參考以下的實現:
在實現轉帳的類時,爲了防止因爲相互轉帳致使的死鎖,下面的實現中,經過對比帳戶的hash值來肯定獲取鎖的順序。當二者的hash值相等時,雖然這種狀況很是少見,使用了單獨的鎖,來控制兩個線程的訪問順序。
注意System.identityHashCode()是JDK自帶的hash實現,在絕大部分狀況下,保證了對象hash值的惟一性。
public class SafeOperate implements ITransfer { private static Object tieLock = new Object();//加時賽鎖 @Override public void transfer(UserAccount from, UserAccount to, int amount) throws InterruptedException { int fromHash = System.identityHashCode(from); int toHash = System.identityHashCode(to); //先鎖hash小的那個 if(fromHash<toHash) { synchronized (from){ synchronized (to){ System.out.println(Thread.currentThread().getName() +" get"+to.getName()); from.flyMoney(amount); to.addMoney(amount); } } }else if(toHash<fromHash) { synchronized (to){ Thread.sleep(100); synchronized (from){ from.flyMoney(amount); to.addMoney(amount); } } }else {//解決hash衝突的方法 synchronized (tieLock) { synchronized (from) { synchronized (to) { from.flyMoney(amount); to.addMoney(amount); } } } } } }
相似 Object.wait(…) 或者 CountDownLatch.await(…),都支持所謂的 timed_wait,咱們徹底能夠就不假定該鎖必定會得到,指定超時時間,併爲沒法獲得鎖時準備退出邏輯。
併發 Lock 實現,如 ReentrantLock 還支持非阻塞式的獲取鎖操做 tryLock(),這是一個插隊行爲(barging),並不在意等待的公平性,若是執行時對象剛好沒有被獨佔,則直接獲取鎖。
標準的使用流程以下:
while(true) { if(A.getLock().tryLock()) { try { if(B.getLock().tryLock()) { try { //兩把鎖都拿到了,開始執行業務代碼 break; }finally { B.getLock().unlock(); } } }finally { A.getLock().unlock(); } } // 很是重要,sleep隨機的時間,以防兩個線程謙讓,產生長時間的等待,也就是活鎖 SleepTools.ms(r.nextInt(10)); }
活鎖偏偏與死鎖相反,死鎖是你們都拿不到資源都佔用着對方的資源,而活鎖是拿到資源卻又相互釋放不執行。當多線程中出現了相互謙讓,都主動將資源釋放給別的線程使用,這樣這個資源在多個線程之間跳動而又得不到執行,這就是活鎖。
在上面解決死鎖的第四個方案中,爲了不活鎖,採用了隨機休眠的機制。
線程執行中有線程優先級,優先級高的線程可以插隊並優先執行,這樣若是優先級高的線程一直搶佔優先級低線程的資源,致使低優先級線程沒法獲得執行,這就是飢餓。固然還有一種飢餓的狀況,一個線程一直佔着一個資源不放而致使其餘線程得不到執行,與死鎖不一樣的是飢餓在之後一段時間內仍是可以獲得執行的,如那個佔用資源的線程結束了並釋放了資源。
對於併發控制而言,鎖是一種悲觀的策略,它老是假設每一次的臨界區操做會產生衝突,由此,若是有多個線程同時須要訪問臨界區資源,則寧肯犧牲資源讓線程進行等待。
無鎖是一種樂觀的策略,它假設對資源的訪問是沒有衝突的。既然沒有衝突,天然不須要等待,因此全部的線程均可以在不停頓地狀態下持續執行。當遇到衝突,則使用CAS來檢測線程衝突,若是發現衝突,則重試直到沒有衝突爲止。
CAS算法的過程是,它包含三個參數CAS(V,E,N),V表示要更新的變量,E表示預期值,N表示新值。僅當V值等於E值時,纔將V的值設置爲N,若是V值和E值不一樣,說明已經有其餘線程作了更新,則當前線程什麼都不作。使用CAS操做一個變量時,只有一個會勝出,併成功更新,其他均會失敗。
減小鎖的持有時間有助於下降鎖衝突的可能性,進而提高系統的併發能力。
這種技術的典型使用場景就是ConcurrentHashMap。
對於HashMap來講,最重要的兩個方法就是get() 和put(),一種最天然的想法就是對整個HashMap加鎖,必然能夠獲得一個線程安全的對象.可是這樣作,咱們就認爲加鎖粒度太大.對於ConcurrentHashMap,它內部進一步細分了若干個小的hashMap,稱之爲段(SEGMENT).默認的狀況下,一個ConcurrentHashMap被進一步細分爲16個段
若是須要在ConcurrentHashMap中增長一個新的表項,並非整個HashMap加鎖,而是首先根據hashcode獲得該表項應該被存放到哪一個段中,而後對該段加鎖,並完成put()操做.在多線程環境中,若是多個線程同時進行put()操做,只要被加入的表項不存放在同一個段中,則線程間即可以作到真正的並行。
在讀多寫少的場合,使用讀寫鎖能夠有效提高系統的併發能力
若是將讀寫鎖的思想進一步的延伸,就是鎖分離.讀寫鎖根據讀寫鎖操做功能上的不一樣,進行了有效的鎖分離.使用相似的思想,也能夠對獨佔鎖進行分離.
以LinkedBlockingQueue爲例,take函數和put函數分別實現了衝隊列取和往隊列加數據,雖然兩個方法都對隊列進項了修改,可是LinkedBlockingQueue是基於鏈表的因此一個操做的是頭,一個是隊列尾端,從理論狀況下將並不衝突
若是使用獨佔鎖則take和put就不能完成真正的併發,因此jdk並無才用這種方式取而代之的是兩把不一樣的鎖分離了put和take的操做
凡事都有一個度,若是對同一個鎖不停地進行請求,同步和釋放,其自己也會消耗系統寶貴的資源,反而不利於性能的優化。
爲此,虛擬機在遇到一連串連續地對同一鎖不斷進行請求和釋放的操做時,便會把全部的鎖操做整合成對鎖的一次請求,從而減小對鎖的請求同步次數,這個操做叫作鎖的粗化.
public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }
線程安全,而且解決了多實例的問題,可是它並不高效。由於在任什麼時候候只能有一個線程調用 getInstance() 方法。
public class Singleton { private static volatile Singleton singleton = null; private Singleton() { } public static Singleton getSingleton() { if (singleton == null) { // 儘可能避免重複進入同步塊 synchronized (Singleton.class) { // 同步.class,意味着對同步類方法調用 if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
在這段代碼中,爭論較多的是 volatile 修飾靜態變量,當 Singleton 類自己有多個成員變量時,須要保證初始化過程完成後,才能被 get 到。 在現代 Java 中,內存排序模型(JMM)已經很是完善,經過 volatile 的 write 或者 read,能保證所謂的 happen-before,也就是避免常被提到的指令重排。換句話說,構造對象的 store 指令可以被保證必定在 volatile read 以前。
這種方法很是簡單,由於單例的實例被聲明成 static 和 final 變量了,在第一次加載類到內存中時就會初始化,因此建立實例自己是線程安全的。
public class Singleton{ //類加載時就初始化 private static final Singleton instance = new Singleton(); private Singleton(){} public static Singleton getInstance(){ return instance; } }
public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } }
靜態內部類是在被調用時纔會被加載,所以它是懶漢式的。
本文由『後端精進之路』原創,首發於博客 http://teckee.github.io/ , 轉載請註明出處
搜索『後端精進之路』關注公衆號,馬上獲取最新文章和價值2000元的BATJ精品面試課程。