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