并发最佳实践
本文是我們名為“ 高級Java ”的學院課程的一部分。
本課程旨在幫助您最有效地使用Java。 它討論了高級主題,包括對象創建,并發,序列化,反射等。 它將指導您完成Java掌握的過程! 在這里查看 !
目錄
1.簡介 2.線程和線程組 3.并發,同步和不變性 4.期貨,執行人和線程池 5.鎖 6.線程調度器 7.原子操作 8.并行收集 9.探索Java標準庫 10.明智地使用同步 11.等待/通知 12.對并發問題進行故障排除 13.接下來是什么 14.下載1.簡介
多處理器和多核硬件體系結構極大地影響了當今運行在其上的應用程序的設計和執行模型。 為了充分利用可用計算單元的全部功能,應用程序應準備好支持同時運行并爭奪資源和內存的多個執行流。 并發編程帶來了許多與數據訪問和事件的不確定性相關的挑戰,這些挑戰可能導致意外崩潰和奇怪的故障。
在本教程的這一部分中,我們將研究Java可以為開發人員提供什么,以幫助他們在并發世界中編寫健壯而安全的應用程序。
2.線程和線程組
線程是Java中并發應用程序的基礎構建塊。 線程有時被稱為輕量級進程 ,它們允許多個執行流并發進行。 Java中的每個應用程序都有至少一個稱為主線程的線程 。 每個Java線程僅存在于JVM內部,并且可能不反映任何操作系統線程。
Java中的Thread是Thread類的實例。 通常,不建議使用Thread類的實例直接創建和管理線程( Futures和Executors部分中介紹的執行器和線程池提供了一種更好的方法),但是這樣做非常容易:
public static void main(String[] args) {new Thread( new Runnable() {@Overridepublic void run() {// Some implementation here}} ).start(); }或使用Java 8 lambda函數的相同示例:
public static void main(String[] args) {new Thread( () -> { /* Some implementation here */ } ).start(); }但是,用Java創建新線程看起來非常簡單,線程具有復雜的生命周期,并且可以處于以下狀態之一(在給定的時間點,線程只能處于一種狀態)。
| 線程狀態 | 描述 |
| NEW | 尚未啟動的線程處于此狀態。 |
| RUNNABLE | 在Java虛擬機中執行的線程處于這種狀態。 |
| BLOCKED | 等待監視器鎖定而被阻塞的線程處于此狀態。 |
| WAITING | 無限期地等待另一個線程執行特定操作的線程處于此狀態。 |
| TIMED_WAITING | 無限期地等待另一個線程執行特定操作的線程處于此狀態。 |
| TERMINATED | 退出的線程處于此狀態。 |
表格1
目前并不是所有的線程狀態都明確,但是在本教程的后面,我們將介紹其中的大多數內容,并討論導致線程處于一種或另一種狀態的事件類型。
線程可以組裝成組。 線程組代表一組線程,并且還可以包括其他線程組(因此形成樹)。 線程組旨在成為一個不錯的功能,但是由于執行程序和線程池(請參閱Futures,Executor和Thread Pools )是更好的替代方法,因此如今不建議使用它們。
3.并發,同步和不變性
在大多數每個Java應用程序中,都需要多個運行線程相互通信并訪問共享數據。 讀取這些數據并不是什么大問題,但是對其進行不協調的修改將直接導致災難(所謂的賽車狀況)。 這就是英寸同步 同步踢是確保在同一時間幾個同時運行的線程將不執行的應用程序代碼的具體守衛(同步)塊中的機構的點。 如果其中一個線程已開始執行代碼的同步塊,則任何其他試圖執行同一塊的線程都必須等待,直到第一個線程完成。
Java語言具有內置的同步支持,形式為synchronized關鍵字。 該關鍵字可以應用于實例方法,靜態方法,也可以在任意執行塊周圍使用,并確保一次僅一個線程將能夠調用它。 例如:
public synchronized void performAction() {// Some implementation here }public static synchronized void performClassAction() {// Some implementation here }或者,使用與代碼塊同步的示例:
public void performActionBlock() {synchronized( this ) {// Some implementation here} }synchronized關鍵字還有另一個非常重要的作用:它會為同一對象的synchronized方法或代碼塊的任何調用自動建立先發生的關系( http://en.wikipedia.org/wiki/Happened-before )。 這保證了對象狀態的更改對所有線程都是可見的。
請注意,構造函數無法同步(將synchronized關鍵字與構造函數一起使用會引起編譯器錯誤),因為在構造實例時,只有創建實例的線程才能訪問它。
在Java中,同步是圍繞稱為監視器的內部實體(或固有/監視器鎖定, http://en.wikipedia.org/wiki/Monitor_ (synchronization))構建的。 Monitor強制對對象狀態進行獨占訪問,并建立事前關聯。 當任何線程調用synchronized方法時,它將自動獲取該方法實例(或靜態方法中的類)的內在(監視)鎖,并在方法返回時釋放它。
最后,同步是Java可重入的 :這意味著線程可以獲取它已經擁有的鎖。 由于線程具有較少的阻塞自身的機會,因此重新進入可大大簡化并發應用程序的編程模型。
如您所見,并發在Java應用程序中引入了很多復雜性。 但是,有一種解決方法: 不變性 。 我們已經討論過很多次了,但是對于多線程應用程序來說,這確實非常重要:不可變對象不需要同步,因為它們永遠不會被多個線程更新。
4.期貨,執行人和線程池
用Java創建新線程很容易,但是管理它們確實很困難。 Java標準庫以執行程序和線程池的形式提供了極其有用的抽象,旨在簡化線程管理。
本質上,在其最簡單的實現中,線程池創建并維護線程列表,這些線程列表可立即使用。 應用程序無需每次都生成新線程,而只是從池中借用一個(或所需的多個線程)。 一旦借用的線程完成其工作,它將返回到池中,并可以用來接管下一個任務。
盡管可以直接使用線程池,但是Java標準庫提供了執行程序外觀,該外觀具有一組工廠方法來創建常用的線程池配置。 例如,下面的代碼段創建了一個具有固定線程數(10)的線程池:
ExecutorService executor = Executors.newFixedThreadPool( 10 );執行程序可以用來卸載任何任務,因此它將在與線程池分開的線程中執行(注意,不建議將執行程序用于長時間運行的任務)。 執行程序的外觀允許自定義基礎線程池的行為,并支持以下配置:
| 方法 | 描述 |
| Executors.newCachedThreadPool | 創建一個線程池,該線程池根據需要創建新線程,但是將在先前構造的線程可用時重用它們。 |
| Executors.newFixedThreadPool | 創建一個線程池,該線程池重用在共享的無邊界隊列上運行的固定數量的線程。 |
| Executors.newScheduledThreadPool | 創建一個線程池,該線程池可以安排命令在給定的延遲后運行或定期執行。 |
| Executors.newSingleThreadExecutor | 創建一個執行程序,該執行程序使用在不受限制的隊列上操作的單個工作線程。 |
| Executors.newSingleThreadScheduledExecutor | 創建一個單線程執行器,該執行器可以計劃命令在給定的延遲后運行或定期執行。 |
表2
在某些情況下,執行的結果不是很重要,因此執行程序支持即發即棄的語義,例如:
executor.execute( new Runnable() { @Overridepublic void run() {// Some implementation here} } );等效的Java 8示例更加簡潔:
executor.execute( () -> {// Some implementation here } );但是,如果執行的結果很重要,則Java標準庫會提供另一個抽象來表示將來會發生的計算,稱為Future<T> 。 例如:
Future< Long > result = executor.submit( new Callable< Long >() {@Overridepublic Long call() throws Exception {// Some implementation herereturn ...;} } );Future<T>的結果可能無法立即獲得,因此應用程序應使用一系列get(…)方法來等待它。 例如:
Long value = result.get( 1, TimeUnit.SECONDS );如果計算結果在指定的超時時間內不可用,則將引發TimeoutException異常。 有一個重載版本的get()會一直等待,但是請您優先使用超時的版本。
自Java 8發行以來,開發人員擁有Future<T>另一個版本CompletableFuture<T> ,該版本支持在其完成時觸發的附加功能和操作。 不僅如此,通過引入流,Java 8引入了一種簡單,非常直接的方式來使用parallelStream()方法執行并行收集處理,例如:
final Collection< String > strings = new ArrayList<>(); // Some implementation herefinal int sumOfLengths = strings.parallelStream().filter( str -> !str.isEmpty() ).mapToInt( str -> str.length() ).sum();執行程序和并行流帶到Java平臺的簡單性使Java中的并行和并行編程變得更加容易。 但是有一個陷阱:線程池和并行流的不受控制的創建可能會破壞應用程序的性能,因此,對它們進行相應的管理很重要。
5.鎖
除了監視器之外,Java還支持可重入互斥鎖(具有與監視器鎖相同的基本行為和語義,但具有更多功能)。 這些鎖可通過java.util.concurrent.locks包中的ReentrantLock類獲得。 這是一個典型的鎖用法習慣用法:
private final ReentrantLock lock = new ReentrantLock();public void performAction() {lock.lock();try { // Some implementation here} finally {lock.unlock();} }請注意,必須通過調用unlock()方法顯式地釋放任何鎖(對于synchronized方法和執行塊,Java編譯器在內部發出釋放監視器鎖的指令)。 如果鎖需要編寫更多的代碼,為什么它們比監視器更好? 好吧,出于幾個原因,但最重要的是,鎖可以在等待獲取時使用超時并快速失敗(監控器始終無限期地等待,并且無法指定所需的超時)。 例如:
public void performActionWithTimeout() throws InterruptedException {if( lock.tryLock( 1, TimeUnit.SECONDS ) ) {try {// Some implementation here} finally {lock.unlock();}} }現在,當我們對監視器和鎖有了足夠的了解時,讓我們討論它們的使用如何影響線程狀態。
當任何線程正在使用lock()方法調用等待鎖(由另一個線程獲取lock() ,它處于WAITING狀態。 但是,當任何線程使用帶有超時的tryLock()方法調用等待鎖(由另一個線程獲取tryLock()時,它處于TIMED_WAITING狀態。 相反,當任何線程正在使用synchronized方法或執行塊等待監視器(由另一個線程獲取)時,它處于BLOCKED狀態。
到目前為止,我們看到的示例非常簡單,但是鎖管理確實很困難,而且充滿陷阱。 其中最臭名昭著的是僵局:兩個或兩個以上競爭線程正在互相等待進行操作,因此從來沒有這樣做的情況。 當涉及多個鎖或監視器鎖時,通常會發生死鎖。 JVM通常能夠檢測正在運行的應用程序中的死鎖并警告開發人員(請參閱“并發問題疑難解答”部分)。 僵局的典型示例如下所示:
private final ReentrantLock lock1 = new ReentrantLock(); private final ReentrantLock lock2 = new ReentrantLock();public void performAction() {lock1.lock();try {// Some implementation here try {lock2.lock(); // Some implementation here} finally {lock2.unlock();} // Some implementation here} finally {lock1.unlock();} }public void performAnotherAction() {lock2.lock();try {// Some implementation here try {lock1.lock(); // Some implementation here} finally {lock1.unlock();} // Some implementation here} finally {lock2.unlock();} }performAction()方法嘗試先獲取lock1 ,然后獲取lock2 ,而performAnotherAction()方法lock2不同的順序lock2它, lock2 ,再獲取lock1 。 如果通過程序執行流在兩個不同線程中的同一類實例上調用這兩個方法,則很可能發生死鎖:第一個線程將無限期地等待第二個線程獲取的lock2 ,而第二個線程將無限期等待無限期地等待第一個獲得的lock1 。
6.線程調度器
在JVM中,線程調度程序確定應運行哪個線程以及運行多長時間。 Java應用程序創建的所有線程都具有優先級,該優先級在決定何時調度線程及其時間范圍時,會基本上影響線程調度算法。 但是,此功能的聲譽是不可移植的(因為大多數技巧都依賴于線程調度程序的特定行為)。
Thread類還通過使用yield()方法提供了另一種干預線程調度實現的方法。 它暗示線程調度程序當前線程愿意放棄其當前使用的處理器時間(并且還具有不可移植的聲譽)。
總的來說,依靠Java線程調度程序的實現細節并不是一個好主意。 這就是為什么Java標準庫中的執行者和線程池(請參閱“ 期貨,執行者和線程池”部分)試圖不向開發人員公開那些不可移植的細節(但如果確實有必要的話,仍然可以這樣做) )。 沒有什么比精心設計更好地工作了,精心設計試圖考慮應用程序所運行的實際硬件(例如,使用Runtime類可以輕松檢索可用的CPU和內核數量)。
7.原子操作
在多線程世界中,有一組特定的指令稱為比較交換 (CAS)。 這些指令將它們的值與給定值進行比較,并且只有它們相同時,才設置新的給定值。 這是通過單個原子操作完成的,該操作通常是無鎖且高效的。
Java標準庫中有大量支持原子操作的類列表,所有這些類都位于java.util.concurrent.atomic包下。
| 類 | 描述 |
| AtomicBoolean | 可以自動更新的布爾值 |
| AtomicInteger | 一個可以自動更新的int值。 |
| AtomicIntegerArray | 一個可能會自動更新的長值。 |
| AtomicLongArray | 一個長數組,其中的元素可以原子更新。 |
| AtomicReference<V> | 可以原子更新的對象引用。 |
| AtomicReferenceArray<E> | 對象引用的數組,其中的元素可以原子更新。 |
表3
Java 8版本通過一組新的原子操作(累加器和加法器)擴展了java.util.concurrent.atomic 。
| 類 | 描述 |
| DoubleAccumulator | 一個或多個變量一起保持使用提供的函數更新的運行雙精度值。 |
| DoubleAdder | 一個或多個變量共同保持初始為零的雙和。 |
| LongAccumulator | 一個或多個變量一起保持使用提供的函數更新的運行時長值。 |
| LongAdder | 一個或多個變量共同保持初始為零的長和。 |
表4
8.并行收集
可以由多個線程訪問和修改的共享集合不是一個例外,而是一個規則。 Java標準庫在Collections類中提供了兩個有用的靜態方法,這些方法使任何現有的collection都是線程安全的。 例如:
final Set< String > strings = Collections.synchronizedSet( new HashSet< String >() );final Map< String, String > keys = Collections.synchronizedMap( new HashMap< String, String >() );但是,返回的通用集合包裝器是線程安全的,通常不是最好的選擇,因為它們在實際應用程序中的性能相當中等。 這就是為什么Java標準庫包含一組針對并發調整的豐富的收集類的原因。 以下只是最廣泛使用的列表,所有列表都托管在java.util.concurrent包下。
| 類 | 描述 |
| ArrayBlockingQueue<E> | 由數組支持的有界阻塞隊列。 |
| ConcurrentHashMap<K,V> | 哈希表支持檢索的完全并發性和可更新的可調整預期并發性。 |
| ConcurrentLinkedDeque<E> | 基于鏈接節點的無限制并發雙端隊列。 |
| ConcurrentLinkedQueue<E> | 基于鏈接節點的無界線程安全隊列。 |
| ConcurrentSkipListMap<K,V> | 可擴展的并發地圖實現 |
| ConcurrentSkipListSet<E> | 基于ConcurrentSkipListMap可伸縮并發集實現。 |
| CopyOnWriteArrayList<E> | ArrayList的線程安全變體,其中所有可變操作(添加,設置等)都通過對基礎數組進行全新復制來實現。 |
| CopyOnWriteArraySet<E> | 一個使用內部CopyOnWriteArrayList進行所有操作的Set。 |
| DelayQueue<E extends Delayed> | 延遲元素的無限制阻塞隊列,在該隊列中,僅當元素的延遲到期時才可以使用該元素。 |
| LinkedBlockingDeque<E> | 基于鏈接節點的可選綁定的阻塞雙端隊列。 |
| LinkedBlockingQueue<E> | 基于鏈接節點的可選綁定的阻塞隊列。 |
| LinkedTransferQueue<E> | 基于鏈接節點的無界TransferQueue 。 |
| PriorityBlockingQueue<E> | 一個無界阻塞隊列,它使用與類PriorityQueue相同的排序規則,并提供阻塞檢索操作。 |
| SynchronousQueue<E> | 一個阻塞隊列,其中每個插入操作必須等待另一個線程進行相應的刪除操作,反之亦然。 |
表5
這些類是專門為在多線程應用程序中使用而設計的。 他們利用許多技術來使對集合的并發訪問盡可能高效,并且是synchronized集合包裝器的推薦替代者。
9.探索Java標準庫
對于編寫并發應用程序的Java開發人員來說, java.util.concurrent和java.util.concurrent.locks包是真正的瑰寶。 由于那里有很多類,因此在本節中,我們將介紹其中最有用的類,但是請不要猶豫地查閱Java官方文檔并進行探索。
| 類 | 描述 |
| CountDownLatch | 一種同步幫助,允許一個或多個線程等待,直到在其他線程中執行的一組操作完成為止。 |
| CyclicBarrier | 一種同步輔助工具,它允許一組線程全部互相等待以到達一個公共的障礙點。 |
| Exchanger<V> | 線程可以配對并在配對中交換元素的同步點。 |
| Phaser | 可重用的同步屏障,其功能類似于CyclicBarrier和CountDownLatch但支持更靈活的用法。 |
| Semaphore | 計數信號量。 |
| ThreadLocalRandom | 隔離到當前線程的隨機數生成器 |
| ReentrantReadWriteLock | 讀/寫鎖的實現 |
表6
不幸的是, ReentrantReadWriteLock的Java實現不是那么出色,從Java 8開始,有了新的鎖:
| 類 | 描述 |
| StampedLock | 一種基于功能的鎖,具有三種模式來控制讀/寫訪問。 |
表7
10.明智地使用同步
鎖定和synchronized關鍵字是功能強大的工具,可在多線程應用程序中極大地幫助保持數據模型和程序狀態的一致性。 但是,不明智地使用它們會導致線程爭用,并且可能會大大降低應用程序性能。 另一方面,不使用同步原語可能(并且將)導致怪異的程序狀態和數據損壞,最終導致應用程序崩潰。 因此,平衡很重要。
建議是在確實需要的地方嘗試使用鎖或/和synchronized 。 在執行此操作時,請確保盡快釋放鎖定,并且將需要鎖定或同步的執行塊保持在最小限度。 那些技術至少應該有助于減少競爭,但不會消除競爭。
近年來,出現了許多所謂的無鎖算法和數據結構(例如, Atomic Operations部分中的Java 原子操作 )。 與使用同步原語構建的等效實現相比,它們提供了更好的性能。
很高興知道JVM有一些運行時優化,以消除可能不必要的鎖定。 最有名的是偏壓鎖定 :一種優化,它通過消除與Java同步原語相關的操作來提高無競爭的同步性能(有關更多詳細信息,請參閱http://www.oracle.com/technetwork/java/6-performance-137236 .html#2.1.1 )。
不過,JVM會盡力而為,消除應用程序中不必要的同步是更好的選擇。 過多使用同步會對應用程序性能產生負面影響,因為線程將浪費昂貴的CPU周期來爭奪資源,而不是進行實際工作。
11.等待/通知
在Java標準庫( java.util.concurrent )中引入并發實用程序之前,使用Object的wait()/notify()/notifyAll()方法是在Java中的線程之間建立通信的方式。 僅當線程擁有有關對象的監視器時,才必須調用所有這些方法 。 例如:
private Object lock = new Object();public void performAction() {synchronized( lock ) {while( <condition> ) {// Causes the current thread to wait until// another thread invokes the notify() or notifyAll() methods.lock.wait();}// Some implementation here } }方法wait()釋放當前線程持有的監視器鎖定,因為它尚未滿足其等待的條件( wait()方法必須在循環中調用,并且絕不能在循環外部調用 )。 因此,在同一監視器上等待的另一個線程有機會運行。 完成此線程后,它應調用notify()/notifyAll()方法之一來喚醒等待監視器鎖定的一個或多個線程。 例如:
public void performAnotherAction() {synchronized( lock ) { // Some implementation here// Wakes up a single thread that is waiting on this object's monitor.lock.notify();} }notify()和notifyAll()之間的區別在于,第一個喚醒單個線程,而第二個喚醒所有等待的線程(它們開始爭奪監視器鎖定)。
不建議在現代Java應用程序中使用wait()/notify()習慣用法。 它不僅復雜,還需要遵循一組強制性規則。 因此,它可能會在正在運行的程序中引起細微的錯誤,這將是非常困難且耗時的調查。 java.util.concurrent有很多可以用更簡單的替代方法來替換wait()/notify()方法(在現實情況下,它很可能會具有更好的性能)。
12.對并發問題進行故障排除
在多線程應用程序中,很多事情可能出錯。 復制問題成為噩夢。 調試和故障排除可能要花費數小時甚至數天甚至數周。 Java開發工具包(JDK)包括幾個工具,這些工具至少能夠提供有關應用程序線程及其狀態的一些詳細信息,并診斷死鎖條件(請參閱線程和線程組以及鎖部分)。 首先是一個好點。 這些工具是(但不限于):
- JVisualVM ( http://docs.oracle.com/javase/7/docs/technotes/tools/share/jvisualvm.html )
- Java任務控制 ( http://docs.oracle.com/javacomponents/jmc.htm )
- jstack ( https://docs.oracle.com/javase/7/docs/technotes/tools/share/jstack.html )
13.接下來是什么
在這一部分中,我們研究了現代軟件和硬件平臺的非常重要的方面-并發性。 特別是,我們已經看到Java作為一種語言及其標準庫為開發人員提供了哪些工具,以幫助他們處理并發和異步執行。 在本教程的下一部分中,我們將介紹Java中的序列化技術。
14.下載
您可以在此處下載本課程的源代碼: advanced-java-part-9
翻譯自: https://www.javacodegeeks.com/2015/09/concurrency-best-practices.html
總結
- 上一篇: 隐私设置错误怎么处理(隐私设置错误怎么处
- 下一篇: 亚马逊标题自动抓取_15分钟内开始使用A