併發思想

 從性能的角度看,如果沒有任務會阻塞,那麼在單處理器機器上使用併發就沒有任何意義。


線程機制分爲:協作式和搶佔式。Java的線程機制是搶佔式的,這表示調度機制會週期性地中斷線程,將上下文切換到另一個線程,從而爲每個線程都提供時間片,使得每個線程都會分配到數量合理的時間去驅動它的任務。在協作式系統中,每個任務都會自動地放棄控制,這要求程序員要有意識地在每個任務中插入某種類型的讓步語句。協作式系統的優勢是雙重的:上下文切換的開銷通常比搶佔式系統要低廉的許多,並且對可以同時執行的線程數量在理論上沒有任何限制。當你處理大量的仿真元素時,這可以一種理想的解決方案。但是注意,某些協作式系統並未設計爲可以在多個處理器之間分佈任務,這可能會非常受限。


非常常見的情況是,單個的Executor被用來創建和管理系統中所有的任務。對shutdown()方法的調用可以防止新任務被提交給這個Executor,當前線程將繼續運行在shutdown()被調用之前提交的所有任務。


線程創建和管理類:CachedThreadPool, SingleThreadExecutor, FixedThreadPool


CachedThreadPool在程序執行過程中通常會創建與所需數量相同的線程,然後在它回收舊線程時停止創建新線程,因此它是合理的Executor的首選。只有當這種方式會引發問題時,你才需要切換到FixedThreadPool.如果向SingleThreadExecutor提交了多個任務,那麼這些任務將排隊,每個任務都會在下一個任務開始之前運行結束,所有的任務將使用相同的線程。


Runnable是執行工作的獨立任務,但是它並不返回任何值。如果你希望任務在完成時能夠返回一個值,那麼可以實現Callable接口而不是Runnable接口。在Java SE5中引入的Callable是一種具有類型參數的範性,它的類型參數表示的是從方法call()中返回的值,並且必須使用ExecutorService.submit()方法調用它。


你可以在一個任務的內部,通過調用Thread.currentThread()來獲得對驅動該任務的Thread對象的引用。


所謂後臺線程,是指在程序運行的時候在後臺提供一種通用服務的線程,並且這種線程並不屬於程序中不可或缺的部分。因此,當所有的非後臺線程結束時,程序也就終止了,同時會殺死進程中的所有後臺線程。反過來說,只要有任何非後臺線程還在運行,程序就不會終止。


可以通過調用isDaemon()方法來確定線程是否是一個後臺線程。如果是一個後臺線程,那麼它創建的任何線程將被自動設置成後臺線程。後臺線程在不執行finally子句的情況下就會終止其run()方法。


在構造器中啓動線程可能會變得很有問題,因爲另一個任務可能會在構造器結束之前開始執行,這意味着該任務能夠訪問處於不穩


定狀態的對象。這是優選Executor而不是顯式地創建Thread對象的另一個原因。



一個線程可以在其他線程之上調用join()方法,其效果是等待一段時間直到第二個線程結束才繼續執行。如果某個線程在另一個線程t上調用t.join(),此線程將被掛起,直到目標線程t結束才恢復。


當另一個線程在該線程上調用interrupt()時,將給該線程設定一個標誌,表明該線程已經被中斷。然而,異常被捕獲時將清理這個標誌,所以在catch子句中,在異常被捕獲的時候這個標誌總是爲假。除異常之外,這個標誌還可用於其他情況,比如線程可能會檢查其中斷狀態。



Thread.UncaughtExceptionHandler是Java SE5中的新接口,它允許你在每個Thread對象上都附着一個異常處理器。Thread.UncaughtExceptionHandler.uncaughtException()會在線程因未捕獲的異常而臨近死亡時被調用。爲了使用它,我們創建了一個新類型的ThreadFactory,它將在每個新創建的Thread對象上附着一個Thread.UncaughtExceptionHandler.


class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {


public void uncaughtException(Thread t, Throwable e) {


    //to do some functions.


}


}


class HandlerThreadFactory implements ThreadFactory {


public Thread newThread(Runnable r) {


  Thread t = new Thread(r);


  t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());


  return t;


}


}


ExecutorService exec = Executors.newCachedThreadPool(new HandlerThreadFactory());


exec.execute(...);





在Java中,遞增不是原子性操作。因此,如果不保護任務,即使單一的遞增也不是安全的。





關於鎖:所有對象都自動含有單一的鎖(也稱爲監視器)。當在對象上調用其任意synchronized方法的時候,此對象都被加鎖,這時該對象上的其他synchronized方法只有等到前一個方法調用完畢並釋放了鎖之後才能被調用。


針對每個類,也有一個鎖(作爲類的Class對象的一部分),所以synchronized static 方法可以在類的範圍內防止對static數據的併發訪問。





每個訪問臨界共享資源的方法都必須被同步,否則他們就不會正確地工作。





private Lock lock = new ReentrantLock();





public int next(){


lock.lock();


try{


   //to do some work here!


   return 123;


} finally {


  lock.unlock();


}


}


當你在使用Lock對象時,將這裏所示的慣用法內部化是很重要的:緊接着的對lock()的調用,你必須放置在finally子句中帶有unlock()的try-finally語句中。注意:return語句必須在try子句中出現,以確保unlock()不會過早發生,從而將數據暴露給第二個任務。





原子性與易變性:


原子性可以應用於除long和double之外的所有基本類型之上的“簡單操作”。對於讀取和寫入除long和double之外的基本類型變量這樣的操作,可以保證它們會被當作不可分(原子)的操作來操作內存。但是,當你定義long或double變量時,如果使用volatile關鍵字,就會獲得(簡單的賦值與返回操作的)原子性。


在多處理器系統上,相對於單處理器系統而言,可視性問題遠比原子性問題多得多。一個任務做出的修改,即使在不中斷的意義上講是原子性的,對其他任務也可能是不可視的(例如,修改只是暫時性地存儲在本地處理器的緩存中),因此不同的任務對應用的狀態有不同的視圖。另一方面,同步機制強制在處理器系統中,一個任務做出的修改必須在應用中是可視的。如果沒有同步機制,那麼修改時可視將無法確定。


volatile關鍵字還確保了應用中的可視性。如果你將一個域聲明爲volatile的,那麼只要對這個域產生了寫操作,那麼所有的讀操作都可以看到這個修改。即便使用了本地緩存,情況也確實如此,volatile域會立即被寫入到主存中,而讀取操作就發生在主存中。


理解原子性和易變性是不同的概念這一點很重要。在非volatile域上的原子操作不必刷新到主存中去,因此其他讀取改域的任務也不必看到這個新值。如果多個任務在同時訪問某個域,那麼這個域就應該是volatile的,否則,這個域就應該只能經由同步來訪問。同步也會導致向主存中刷新,因此如果一個域完全由synchronized方法或語句塊來防護,那就不必將其設置爲是volatile的。


一個任務所作的任何寫入操作對這個任務來說都是可視的,因此如果它只需要在這個任務內部可視,那麼你就不需要將其設置爲volatile的。


當一個域的值依賴於它之前的值時(例如遞增一個計數器),volatile就無法工作了。如果某個域的值受到其他域的值的限制,那麼volatile也無法工作,例如Range類的lower和upper邊界就必須遵循lower<upper的限制。


使用volatile而不是synchronized的唯一安全的情況是類中只有一個可變的域。


synchronized關鍵字不屬於方法特徵簽名的組成部分,所以可以在覆蓋方法的時候加上去。


使用同步控制塊而不是對整個方法進行同步控制的典型原因:使得其他線程能更多地訪問(在安全的情況下儘可能多)。





中斷:


Thread類包含interrupt()方法,因此你可以終止被阻塞的任務,這個方法將設置線程的中斷狀態。如果一個線程已經被阻塞,或者試圖執行一個阻塞操作,那麼設置這個線程的終端狀態將拋出InterruptedException。當拋出異常或者改任務調用Thread.interrupted()時,中斷狀態將被複位。


如果你在Executor上調用shutdownNow(),那麼它將發送一個Interrupt()調用給它啓動的所有線程。這麼做是有意義的,因爲當你完成工程中的某個部分或者整個程序時,通常會希望同時關閉某個特定Executor的所有任務。然而,你有時也會希望只中斷某個單一任務。如果使用Executor,那麼通過調用submit()而不是executor()來啓動任務,就可以持有改任務的上下文。submit()將返回一個範性Future<?>,其中有一個未修飾的參數,因爲你永遠都不會在其上調用get()-持有這種Future的關鍵在於你可以在其上調用cancel(),並因此可以使用它來中斷某個特定任務。如果你將true傳遞給cancel(),那麼它就會擁有在改線程上調用interrupt()以停止這個線程的權限。


因此,cancel()是一種由Executor啓動的單個線程的方式。


你能夠中斷對sleep()的調用,但是,你不能中斷正在試圖獲取synchronized鎖或者試圖執行I/O操作的線程。


Java SE5併發類庫中添加了一個特性,即在ReentrantLock上阻塞的任務具備可以被中斷的能力,這與在synchronized方法或臨界區上阻塞的任務完全不同:


public void f(){


try{


   lock.lockInterruptibly();


   //do something


  } catch(InterruptedException e) {


  }


}





被設計用來響應interrupt()的類必須建立一種策略,來確保它將保持一致的狀態。這通常意味着所有需要清理的對象創建操作的後面,都必須緊跟try-finally子句,從而使得無論run()循環如何退出,清理都會發生。





使用notify()時,在衆多等待同一個鎖的任務中只有一個會被喚醒,因此如果你希望使用notify(),就必須保證被喚醒的是恰當的任務。另外,爲了使用notify(),所有任務必須等待相同的條件,因爲你有多個任務在等待不同的條件,那麼你就不會知道是否喚醒了恰當的任務。如果使用notify(),當條件發生變化時,必須只有一個任務能夠從中受益。最後,這些限制對所有可能存在的子類都必須總是起作用的。如果這些規則中有任何一條不滿足,那麼你就必須使用notifyAll()而不是notify();





Interface BlockingQueue<E>


A Queue that additionally supports operations that wait for the queue to become non-empty when retrieving an element, and wait for space to become available in the queue when storing an element.





BlockingQueue methods come in four forms, with different ways of handling operations that cannot be satisfied immediately, but may be satisfied at some point in the future: one throws an exception, the second returns a special value (either null or false, depending on the operation), the third blocks the current thread indefinitely until the operation can succeed, and the fourth blocks for only a given maximum time limit before giving up. These methods are summarized in the following table:











               Throws exception     Special value        Blocks            Times out


Insert               add(e)           offer(e)          put(e)       offer(e, time, unit)


Remove        remove()              poll()          take()           poll(time, unit)


Examine      element()           peek()           not applicable     not applicable


你通常可以使用LinkedBlockingQueue, 它是一個無界隊列,還可以使用ArrayBlockingQueue,它具有固定的尺寸,因此你可以在它被阻塞之前,向其中放置有限數量的元素。





任務間使用管道進行輸入/輸出:PipedReader與普通I/O之間最重要的差異——PipedReader是可中斷的。





CountDownLatch: 它被用來同步一個或多個任務,強制他們等待由其他任務執行的一組操作完成。可以向CountDownLatch對象設置一個初始計數值,任何在這個對象上調用wait()的方法都將阻塞,直至這個計數值到達。其他任務在結束其工作時,可以在改對象


上調用countDown()來減小這個計數值。CountDownLatch被設計爲只觸發一次,計數值不能被重置。


CyclicBarrier適用於這樣的情況:你希望創建一組任務,他們並行地執行工作,然後在進行下一個步驟之前等待,直至所有任務都完成。它使得所有的並行任務都將在柵欄處列隊,因此可以一致地向前移動。這非常像CountDownLatch,只是CountDownLatch是隻觸發一次的事件,而CyclicBarrier可以多次重用。


DelayQueue:這是一個無界的BlockingQueue,用於放置實現了Delayed接口的對象,其中的對象只能在其到期時才能從隊列中取走。這種隊列是有序的,即隊頭對象的延遲到期的時間最長。如果沒有任何延遲到期,那麼就不會由任何頭元素,並且poll()將返回null(正因爲這樣,你不能將null放置到這種隊列中)。


Class PriorityBlockingQueue<E>


An unbounded blocking queue that uses the same ordering rules as class PriorityQueue and supplies blocking retrieval operations.


While this queue is logically unbounded, attempted additions may fail due to resource exhaustion (causing OutOfMemoryError). This class does not permit null elements. A priority queue relying on natural ordering also does not permit insertion of non-comparable objects (doing so results in ClassCastException).





Class ScheduledThreadPoolExecutor


A ThreadPoolExecutor that can additionally schedule commands to run after a given delay, or to execute periodically. This class is preferable to Timer when multiple worker threads are needed, or when the additional flexibility or capabilities of ThreadPoolExecutor (which this class extends) are required.


Delayed tasks execute no sooner than they are enabled, but without any real-time guarantees about when, after they are enabled, they will commence. Tasks scheduled for exactly the same execution time are enabled in first-in-first-out (FIFO) order of submission.


Semaphore:正常的鎖在任何時刻都只允許一個任務訪問一項資源,而計數信號量允許n個任務同時訪問這個資源。你還可以將信號量看作是在向外分發使用資源的“許可證”, 儘管實際上沒有使用任何許可證對象。


Exchanger是在兩個任務之間互換對象的柵欄。當這些任務進入柵欄時,他們各自擁有一個對象,當他們離開時,他們都擁有之前由對象持有的對象。Exchanger的典型應用場景是:一個任務在創建對象,這些對象的生產代價很高昂,而另一個任務在消費這些對象。通過這種方式,可以有更多的對象在被創建的同時被消費。





所有的控制系統都具有穩定性問題,如果它們隊變化反映過快,那麼他們就是不穩定的,而如果他們放映過慢,則系統會遷移到它的某種極端情況。





使用Lock通常會比使用synchronized要高效許多,而且synchronized的開銷看起來變化範圍太大,而Lock相對比較一致。Atomic對象只有在非常簡單的情況下才有用,這些情況通常包括你只有一個要被修改的Atomic對象,並且這個對象獨立與其他所有的對象。更安全的做法是:以更加傳統的互斥方式入手,只有在性能方面的需求能夠明確指示時,再替換爲Atomic.免鎖容器 免鎖容器背後的通用策略是:對容器的修改可以與讀取操作同時發生,只要讀取者只能看到完成修改的結果即可。修改是在容器數據結構的某個部分的一個單獨的副本(有時是整個數據結構的副本)上執行的,並且這個副本在修改過程中是不可視的。只有當修改完成時,被修改的結構纔會自動地與主數據結構進行交換,之後讀取者就可以看到這個修改了。


儘管Atomic對象將執行像decrementAndGet()這樣的原子性操作,但是某些Atomic類還允許你執行所謂的“樂觀加鎖”。這意味着當你執行某項計算時,實際上沒有使用互斥,但是在這項計算完成,並且你準備更新整個Atomic對象時,你需要使用一個稱爲compareAndSet()的方法。你將舊值和新值一起提交給這個方法,如果舊值與它在Atomic對象中發現的值不一致,那麼這個操作就失敗——這意味着某個其他的任務已經與此操作執行期間修改了這個對象。記住,我們在正常情況下將使用互斥來防止多個任務同時修改一個對象,但是這裏我們是“樂觀的”,因爲我們保持數據爲未鎖定狀態,並希望沒有任何其他任務插入修改它。所有這些又都是以性能的名義執行的——通過使用Atomic來替代synchronized或Lock,可以獲得性能上的好處。





活動對象:


活動對象或行動者,之所以稱這些對象是“活動的”,是因爲每個對象都維護着它自己的工作器線程和消息隊列,並且所有對這種對象的請求都將進入隊列排隊,任何時刻都只能運行其中的一個。因此,有了活動對象,我們就可以串行化消息而不是方法,這意味着不再需要防備一個任務在其循環的中間被中斷這種問題了。


爲了能夠在不經意間就可以防止線程之間的耦合,任何傳遞給活動對象方法調用的參數都必須是隻讀的其他活動對象,或者是不連接對象,即沒有連接任何其他任務的對象。有了活動對象:


1. 每個對象都可以擁有自己的工作器線程。


2.每個對象都將維護對它自己的域的全部控制權。


3.所有活動對象之間的通信都將以在這些對象之間的消息形式發生。


4.活動對象之間的所有消息都要排隊。





線程的一個額外的好處是它們提供了輕量級的執行上下文切換(大約100條指令),而不是重量級的進程上下文切換(要上千條指令)。因爲一個給定進程內的所有線程共享相同的內存空間,輕量級的上下文切換隻是改變了程序的執行序列和局部變量。進程切換(重量級的上下文切換)必須改變所有內存空間。
發佈了17 篇原創文章 · 獲贊 1 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章