上次修改時間:2020年4月17日html
做者 亞歷杭德羅·烏加特java
Java支持開箱即用的多線程。這意味着,經過同時多個分隔的工做線程來運行不一樣的字節碼,JVM 可以提升應用程序性能。git
儘管多線程很強大,但它也是有代價的。在多線程環境中,咱們須要以線程安全的方式編寫實現。這意味着不一樣的線程能夠訪問共享的資源,而不會因錯誤的行爲或產生不可預測的結果。這種編程方法被稱爲「線程安全」。github
在本教程中,咱們將探討實現它的不一樣方法。編程
在大多數狀況下,多線程應用中的錯誤是錯誤地在多個線程之間共享狀態的結果。api
所以,咱們要研究的第一種方法是 使用無狀態實現來實現線程安全。數組
爲了更好地理解這種方法,讓咱們考慮一個帶有靜態方法的簡單工具類,該方法能夠計算數字的階乘:緩存
public class MathUtils { public static BigInteger factorial(int number) { BigInteger f = new BigInteger("1"); for (int i = 2; i <= number; i++) { f = f.multiply(BigInteger.valueOf(i)); } return f; } }
factorial
方法是一種無狀態肯定性函數。 肯定性是指:給定特定的輸入,它將始終產生相同的輸出。安全
該方法既不依賴外部狀態,也不維護自身的狀態。所以,它被認爲是線程安全的,而且能夠同時被多個線程安全地調用。多線程
全部線程均可以安全地調用 factorial
方法,而且將得到預期結果,而不會互相干擾,也不會更改該方法爲其餘線程生成的輸出。
所以,無狀態實現是實現線程安全的最簡單方法。
若是咱們須要在不一樣線程之間共享狀態,則能夠經過使它們成爲不可變對象來建立線程安全類。
不變性是一個功能強大,與語言無關的概念,在Java中至關容易實現。
當類實例的內部狀態在構造以後沒法修改時,它是不可變的。
在Java中建立不可變類的最簡單方法是聲明全部字段爲 private 和 final ,且不提供 setter:
public class MessageService { private final String message; public MessageService(String message) { this.message = message; } // 標準 getter }
一個 MessageService 對象其實是不可變的,由於它的狀態在構造以後不能更改。所以,它是線程安全的。
此外,若是 MessageService 其實是可變的,可是多個線程僅對其具備只讀訪問權限,那麼它也是線程安全的。
所以,不變性是實現線程安全的另外一種方法。
在面向對象編程(OOP)中,對象實際上須要經過字段維護狀態並經過一種或多種方法來實現行爲。
若是咱們確實須要維護狀態,則能夠經過使它們的字段成爲線程局部的來建立不在線程之間共享狀態的線程安全類。
經過簡單地在 Thread 類中定義私有字段,咱們能夠輕鬆建立其字段爲線程局部的類。
例如,咱們能夠定義一個存儲整數數組的 Thread 類:
public class ThreadA extends Thread { private final List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6); @Override public void run() { numbers.forEach(System.out::println); } }
而另外一個類可能擁有一個字符串數組:
public class ThreadB extends Thread { private final List<String> letters = Arrays.asList("a", "b", "c", "d", "e", "f"); @Override public void run() { letters.forEach(System.out::println); } }
在這兩種實現中,這些類都有其本身的狀態,可是不與其餘線程共享。所以,這些類是線程安全的。
一樣,咱們能夠經過將 ThreadLocal 實例分配給一個字段來建立線程私有字段。
例如,讓咱們考慮如下 StateHolder 類:
public class StateHolder { private final String state; // 標準的構造函數和 getter }
咱們能夠很容易地使其成爲線程局部(ThreadLocal)變量,以下所示:
public class ThreadState { public static final ThreadLocal<StateHolder> statePerThread = new ThreadLocal<StateHolder>() { @Override protected StateHolder initialValue() { return new StateHolder("active"); } }; public static StateHolder getState() { return statePerThread.get(); } }
線程局部字段與普通類字段很是類似,不一樣之處在於,每一個經過setter / getter訪問它們的線程都將得到該字段的獨立初始化副本,以便每一個線程都有本身的狀態。
經過使用collections框架 中包含的一組同步包裝器,咱們能夠輕鬆地建立線程安全的collections。
例如,咱們可使用如下同步包裝之一來建立線程安全的集合:
Collection<Integer> syncCollection = Collections.synchronizedCollection(new ArrayList<>()); Thread thread1 = new Thread(() -> syncCollection.addAll(Arrays.asList(1, 2, 3, 4, 5, 6))); Thread thread2 = new Thread(() -> syncCollection.addAll(Arrays.asList(7, 8, 9, 10, 11, 12))); thread1.start(); thread2.start();
讓咱們記住,同步集合在每種方法中都使用內在鎖定(咱們將在後面介紹內在鎖定)。
這意味着該方法一次只能由一個線程訪問,而其餘線程將被阻塞,直到該方法被第一個線程解鎖。
所以,因爲同步訪問的基本邏輯,同步會對性能形成不利影響。
除了同步集合,咱們可使用併發集合來建立線程安全的集合。
Java提供了 java.util.concurrent 包,其中包含多個併發集合,例如 ConcurrentHashMap :
Map<String,String> concurrentMap = new ConcurrentHashMap<>(); concurrentMap.put("1", "one"); concurrentMap.put("2", "two"); concurrentMap.put("3", "three");
與同步對象不一樣,併發集合經過將其數據劃分爲段來實現線程安全。例如,在 ConcurrentHashMap 中,多個線程能夠獲取不一樣 Map 段上的鎖,所以多個線程能夠同時訪問 Map 。
因爲併發線程訪問的先天優點,併發集合類具有遠超同步集合類更好的性能。
值得一提的是,同步集合和併發集合僅使集合自己具備線程安全性,而不使content變得線程安全。
使用Java提供的一組原子類(包括 AtomicInteger,AtomicLong,AtomicBoolean 和 AtomicReference )也能夠實現線程安全。
原子類使咱們可以執行安全的原子操做,而無需使用同步。原子操做在單個機器級別的操做中執行。
要了解解決的問題,讓咱們看下面的 Counter 類:
public class Counter { private int counter = 0; public void incrementCounter() { counter += 1; } public int getCounter() { return counter; } }
讓咱們假設在競爭條件下,兩個線程同時訪問 increasingCounter() 方法。
從理論上講, counter 字段的最終值爲2。可是咱們不肯定結果如何,由於線程在同一時間執行同一代碼塊,而且增量不是原子的。
讓咱們使用 AtomicInteger 對象建立 Counter 類的線程安全實現:
public class AtomicCounter { private final AtomicInteger counter = new AtomicInteger(); public void incrementCounter() { counter.incrementAndGet(); } public int getCounter() { return counter.get(); } }
這是線程安全的,由於在++增量執行多個操做的同時, 增量和獲取 是原子的。
儘管較早的方法對於集合和基元很是有用,但有時咱們須要的控制權要強於此。
所以,可用於實現線程安全的另外一種常見方法是實現同步方法。
簡而言之,一次只能有一個線程能夠訪問同步方法,同時阻止其餘線程對該方法的訪問。其餘線程將保持阻塞狀態,直到第一個線程完成或該方法引起異常。
咱們能夠經過使它成爲同步方法,以另外一種方式建立線程安全版本的 creationCounter() :
public synchronized void incrementCounter() { counter += 1; }
咱們經過與前綴的方法簽名建立一個同步方法 synchronized 關鍵字。
因爲一次一個線程能夠訪問一個同步方法,所以一個線程將執行 crementCounter() 方法,而其餘線程將執行相同的操做。任何重疊的執行都不會發生。
同步方法依賴於「內部鎖」或「監視器鎖」的使用。固有鎖是與特定類實例關聯的隱式內部實體。
在多線程上下文中,術語 monitor 是指對關聯對象執行鎖的角色,由於它強制對一組指定的方法或語句進行排他訪問。
當線程調用同步方法時,它將獲取內部鎖。線程完成執行方法後,它將釋放鎖,從而容許其餘線程獲取鎖並得到對方法的訪問。
咱們能夠在實例方法,靜態方法和語句(已同步的語句)中實現同步。
有時,若是咱們只須要使方法的一部分紅爲線程安全的,那麼同步整個方法可能就顯得過度了。
爲了說明這個用例,讓咱們重構 increascountCounter 方法:
public void incrementCounter() { // 此處可有額外不需同步的操做 // ... synchronized(this) { counter += 1; } }
該示例很簡單,可是它顯示瞭如何建立同步語句。假設該方法如今執行了一些不須要同步的附加操做,咱們僅經過將相關的狀態修改部分包裝在一個同步塊中來對其進行同步。
與同步方法不一樣,同步語句必須指定提供內部鎖的對象,一般是this
引用。
同步很是昂貴,所以使用此選項,咱們儘量只同步方法的相關部分。
咱們能夠經過將另外一個對象用做監視器鎖定,來稍微改善 Counter 類 的線程安全實現。
這不只能夠在多線程環境中提供對共享資源的協調訪問,還可使用外部實體來強制對資源進行獨佔訪問:
public class ObjectLockCounter { private int counter = 0; private final Object lock = new Object(); public void incrementCounter() { synchronized(lock) { counter += 1; } } // 標準 getter }
咱們使用一個普通的 Object 實例來強制相互排斥。此實現稍好一些,由於它能夠提升鎖定級別的安全性。
將 this 用於內部鎖定時,攻擊者可能會經過獲取內部鎖定並觸發拒絕服務(DoS)條件來致使死鎖。
相反,在使用其餘對象時, 沒法從外部訪問該私有實體。這使得攻擊者更難得到鎖定並致使死鎖。
即便咱們能夠將任何Java對象用做內部鎖定,也應避免將 _Strings_用於鎖定目的:
public class Class1 { private static final String LOCK = "Lock"; // 使用 LOCK 做爲內部鎖 } public class Class2 { private static final String LOCK = "Lock"; // 使用 LOCK 做爲內部鎖 }
乍一看,這兩個相似乎將兩個不一樣的對象用做其鎖。可是,intern,這兩個「 Lock」值實際上可能引用字符串池上的同一對象。也就是說, Class1 和 Class2 共享相同的鎖!
反過來,這可能會致使在併發上下文中發生某些意外行爲。
除了字符串以外,咱們還應避免將任何可緩存或可重用的對象用做內部鎖。例如, Integer.valueOf() 方法緩存少許數字。所以,即便在不一樣的類中,調用 Integer.valueOf(1) 也會返回相同的對象。
同步的方法和塊很是適合解決線程之間的可變可見性問題。即便這樣,常規類字段的值也可能會被CPU緩存。所以,即便是同步的,對特定字段的後續更新也可能對其餘線程不可見。
爲了不這種狀況,咱們可使用 volatile 修飾的類字段:
public class Counter { private volatile int counter; // 標準構造函數、getter }
使用 volatile 關鍵字,咱們指示 JVM 和編譯器將 counter 變量存儲在主內存中。這樣,咱們確保每次 JVM 讀取 counter 變量的值時,實際上都會從主內存而不是從 CPU 緩存讀取它。一樣,每次 JVM 將值寫入 counter 變量時,該值將被寫入主內存。
此外,使用 volatile 變量可確保也將從主內存中讀取給定線程可見的全部變量。
讓咱們考慮如下示例:
public class User { private String name; private volatile int age; // 標準構造函數、getter }
在這種狀況下,JVM 每次將 age _volatile_ 變量寫入主內存時,也會將非易失性 name 變量也寫入主內存。這確保了兩個變量的最新值都存儲在主存儲器中,所以對變量的後續更新將自動對其餘線程可見。
一樣,若是線程讀取 易失性 變量的值,則該線程可見的全部變量也將從主內存中讀取。
易失性 變量提供的這種擴展保證稱爲 徹底易失性可見性保證。
Java 提供了一組改進的 Lock 實現,其行爲比上面討論的固有鎖稍微複雜一些。
對於固有鎖,鎖獲取模型至關嚴格:一個線程獲取鎖,而後執行方法或代碼塊,最後釋放鎖,以便其餘線程能夠獲取它並訪問該方法。
沒有底層機制能夠檢查排隊的線程並優先訪問等待時間最長的線程。
ReentrantLock 實例使咱們可以作到這一點,從而防止排隊的線程遭受某些類型的資源匱乏):
public class ReentrantLockCounter { private int counter; private final ReentrantLock reLock = new ReentrantLock(true); public void incrementCounter() { reLock.lock(); try { counter += 1; } finally { reLock.unlock(); } } // 標準構造函數、getter... }
該 ReentrantLock 的構造函數有一個可選的 公平 _boolean_ 參數。若是設置爲 true ,而且多個線程正試圖獲取鎖,則 JVM 將優先考慮等待時間最長的線程,並授予對該鎖的訪問權限。
咱們能夠用來實現線程安全的另外一種強大機制是使用 ReadWriteLock 實現。
一個 ReadWriteLock中 鎖定實際使用一對相關的鎖,一個用於只讀操做和其餘寫操做。
結果,只要沒有線程寫入資源,就有可能有許多線程在讀取資源。此外,將線程寫入資源將阻止其餘線程讀取資源。
咱們可使用 ReadWriteLock 鎖,以下所示:
public class ReentrantReadWriteLockCounter { private int counter; private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); private final Lock readLock = rwLock.readLock(); private final Lock writeLock = rwLock.writeLock(); public void incrementCounter() { writeLock.lock(); try { counter += 1; } finally { writeLock.unlock(); } } public int getCounter() { readLock.lock(); try { return counter; } finally { readLock.unlock(); } } // 標準構造函數... }
在本文中,咱們瞭解了Java中的線程安全性,並深刻研究了實現它的各類方法。
像往常同樣,本文中顯示的全部代碼示例均可以在GitHub上得到。