java編程思想併發

21.1.2java線程作用

1.可以創建模擬真實的事件,使每個元素都有其資金的處理器並且都是獨立的任務
2.解決這個問題典型的方式是使用協作多線程。
java的線程機制是搶佔式的,這表示調度機制會週期性的的中斷線程,將上下文切換到另一個線程。
3.理解併發可以掌握基於消息機制的架構,這些架構在創建分佈式系統時是更主要的方式
4.線程模型爲編程帶來了便利,它簡化了在單一程序中同時交織在一起的多個操作的處理。雖然不知道如何切分CPU時間(比如多個cpu)的具體情況,但線程機制會做這些事情,使程序員從這個層次抽身出來,它是一種建立透明的、可擴展的程序的方法

21.2基本的線程機制

21.2.1使用線程的編寫方式

1.一個類實現Runnable接口變成任務類,然後再new Thread類的構造方法中加入這個任務類,最後start();
2.new 一個Thread類,重寫run方法;
3.一個類實現Runnable接口變成任務類,利用Executor這個執行器管理,常用方法是ExecutorService.execute(任務類對象)
4.一個類實現Callable接口,在ExecutorService.submit()中以參數的形式傳入調用。這個任務有返回值。

21.2.3執行器——Executor

Executor在客戶端和任務執行之間提供了一個間接層;他是啓動任務的優選方法。
Executor可以創建的線程池:
1.Executor.newCachedThreadPool()——創建所需數量相同的線程,這個是首選
2.Executor.newFixedThreadPool(number)——固定數量的線程池,預先創建,這可以節省時間
3.Executor.singleThreadExecutor()——多個任務按照提交的順序執行,每個任務都會在下一個任務執行前執行結束;比如文件系統,使用這個確保任意時刻任何線程中只有一個任務在運行,不需要在共享資源上解決同步問題。

21.2.6優先級

設置優先級:
Thread.currentThread().setPriority(優先級)
可移植的優先級:MAX _PRIORITY、NORM_PRIORITY、MIN_PRIORITY

21.2.9編碼的變體,可以有5種內部類寫法隱藏線程代碼

1.InnerThread1——類中有一個完整的內部類,此內部類繼承Thread,外部類構造方法中創建內部類,內部類的構造方法調用start()
2.InnerThread2——類中有一個Thread成員變量,構造方法中初始化這個Thread成員變量,並調用start()
3.InnerRunnable1——類中有一個完整的實現Runnable接口內部類,內部類構造方法中new一個Thread對象,傳入此Runnable接口對象this,並調用start(),外部類構造方法創建這個內部類對象。
4.InnerRunnable2——外部類構造方法中new 一個Thread對象,以匿名內部類的方式創建Runnable對象,並傳入Thread對象構造方法,調用start()
5.ThreadMethod——一個方法中調用線程的start()方法

21.2.10任務和線程

從概念上講,我們希望創建獨立於其他任務運行的任務,因此我們應該能夠定義任務,然後說“開始”,並且不用操心其細節。但是在物理上,創建線程可能會代價高昂,因此你必須保存並管理他們。這樣,從實現的角度看,將任務從線程中分離出來是很有意義的。

21.2.11 加入線程

我常常困惑join方法到底是調用完誰在誰的前面執行,直到我看見join()方法沒有參數,它是線程實例調用的,比如有兩線程A、B他們都有各自的join()方法,在A的run方法中執行new B().join(),那麼A會暫停,直到B線程結束後再執行A

21.2.14 捕獲異常

線程直接在調用創建線程的線程上,是不能捕獲新創建的線程的異常的,如何捕獲呢:
1.在創建線程池的時候,在構造參數中傳入一個繼承自ThreadFactory對象,之後在這個線程池運行的任務拋異常會用這個處理,這個對象會實現一個抽象方法public Thread newThread(Runnable r)方法,在這個方法中加入如下代碼:
Thread t = new Thread®;
t.setUncaughtExceptionHandler(一個實現Thread.UncaughtExceptionHandler接口的對象,簡稱線程未捕獲異常處理器);
return t;
實現這個線程未捕獲異常處理器的對象,會實現一個抽象方法public void uncaughtException(Thread t,Throwable e){},可以在方法體重直接寫
System.out.println(e);
2.在線程池創建前,調用
Thread.setDefaultuncaughtExceptionHandler(線程未捕獲異常處理器);
注意:如果同時存在1方法優先於2方法捕獲異常

21.3共享受限資源

synchronized——鎖對象,鎖類

對象鎖:
將域設置爲private,否則synchronized關鍵字就不能防止其他任務之間訪問域,會產生衝突。
synchronized是用在普通方法上的:synchronized void f(){…},鎖的是對象
一個任務可以多次獲得對象的鎖。如果一個方法在同一個對象上調用了第二個方法,後者又調用了對象上的另一個方法,就會發生這種情況。
類鎖:
synchronized static 可以在類的範圍內防止static數據的併發訪問,鎖的是類;
什麼時候應該同步呢?
如果正在寫一個變量,它可能接下來被另一個線程讀取,或者正在讀一個上一次已經被另一個線程寫過的變量,那麼必須使用同步,並且線程要使用相同的鎖。
多個方法在處理共享資源時,必須同步所有相關方法;

synchronized臨界區

就是同步代碼塊;
在synchronized(this)這種方式中,如果獲得該對象的鎖,那麼該對象的其他同步方法和臨界區就不能被調用了;

顯示的lock對象——ReentrantLock

用法:先創建ReentrantLock對象,緊接着調用lock()方法,再緊接着寫一個try-finally語句,unlock()方法必須放置在finally語句中,return語句必須在try子句中出現,以確保unlock()不會構造發生,把數據暴露給第二個任務。
與synchronized比較的優點:
1.如果拋異常,synchronized沒有機會做任何清理工作,而ReentrantLock可以在finally語句中維護系統狀態(但處理異常來使系統恢復穩定狀態,也不是java推薦,只是有這個功能);
2.可以嘗試獲取鎖且最終獲取會失敗;
3.可以嘗試獲取鎖一段時間,然後放棄它;
優點2、3是你可以嘗試獲取但是最終未獲取鎖,這樣這一判定其他人獲取了這個鎖,那麼你就可以決定去執行其他一些事情。

原子性與易變性

一個不正確的知識是“原子操作不需要進行同步控制”。原子操作是不能被線程調度機制中斷的操作;一旦操作開始,那麼它一定可以在可能發生的“上下文切換”之前執行完畢,或者理解爲切換到其他線程前執行完畢。
不要考慮用原子性代替同步除非你能做到:“可以編寫用於現代微處理器的高性能JVM”
除了long和double,所有基本類型都有原子性,因爲long跟double型佔64位,而JVM可以將他們的讀取和寫入當作兩個分離的32位操作來執行,這樣如果長生了一個在讀取和寫入中間的上下文切換,導致不同任務可以看到不同結果。可以使用volatile來回到原子性

volatile的可視性

在多處理系統中,可視性遠比原子性問題多得多。一個任務作出的修改,即使是原子性的,對其他任務也可能是不可視的,例如,修改只是暫時性的儲存在本地處理器的緩存中,而讀取操作是發生在主存中。
如果任務所做的任何寫入操作都是針對這個任務來說的,那麼這個域是可視的,也不需要將其設爲volatile。
解決這個問題可以使用volatile,它的原理是即便使用了本地處理器緩存volatile域也會立即寫入到主存中。
同步也會導致向主存中刷新,所以如果一個域不是synchronized方法或語句塊防護,那就不必將其設置爲volatile。當然鎖的是在併發情況下;

一個線程不安全的重要例子

public class SerialNumberGenerator{
	private static volatile int serialNumber = 0;
	public static int nextSerialNumber(){
		return serialNumber ++;
	}
}

這個例子在併發中也是不安全的,因爲遞增在java中不是原子性操作,它包括一個讀操作和一個寫操作,不信可以javap -c SerialNumberGenerator反編譯看一下:
在這裏插入圖片描述
基本上,如果一個域可能被多個任務同時訪問,或者這些任務中至少有一個是寫入任務,那麼就應該將這個域設爲volatile。如果將一個域設爲volatile,那麼它就會告訴編譯器不要只想任何移除讀取和寫入操作的優化,這些操作的目的是用線程中局部變量維護隊這個域的精確同步。實際上,讀取和寫入都是針對內存的,而去沒有被緩存。但是volatile並不能對遞增不是原子性操作這一事實產生影響。所以上邊的例子即使加入volatile這個程序也不是線程安全的。

關於synchronized方法被繼承後重寫

synchronized不屬於方法特徵前面的組成部分,所以可以在覆蓋方法的時候取消;

線程的狀態

1.新建——可以轉換爲運行狀態和阻塞狀態
2.就緒——只要調度器分配時間片給線程,它就可以運行
3.阻塞——調度器將忽略線程不會分配給線程任何cpu時間片;直到轉換爲就緒狀態,它纔可能執行操作
4.死亡

進入阻塞有如下原因

1.通過sleep()是任務進入休眠狀態
2.通過wait()是線程掛起,知道notify()或notifyAll()
3.任務在等待輸入/輸出完成
4.在試圖調用同步控制方法,但是鎖被其他任務獲得

兩種不能中斷阻塞的情況

1.同步控制塊
2.IO流輸入輸出
解決辦法,同步控制塊,用ReentrantLock可以中斷;IO流輸入輸出中斷可以關閉IO底層資源;

21.5線程之間的協作

21.5.1錯失的信號

T1:
synchronized(sharedMonitor){
	<setup condition for T2>
	sharedMonitor.notify();
}
T2:
while(someCondition){
//point1
	synchronized(sharedMonitor){
		sharedMonitor.wait();
	}
}
  • < setup condition for T2>是防止T2調用wait()的一個動作,當然前提是T2還沒有調用wait()。
  • 假設T2對someCondition求值並發現其爲true.在Ponit1,線程調度器可能切換到T1,而T1將執行其設置,然後調用notify()。當T2得以繼續執行時,此時對於T2來說,時機已經太晚了,一直與不能意識到這個條件已經發生了變化,因此會盲目的進入wait(0.此時notify()將錯失,而T2也將無限制地等待這個已經發生過的信號,從而產生死鎖。

21.5.2notify()與notifyAll()

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

21.5.3生成者與消費者

  • 使用顯示的lock和Condition對象
    使用互斥並允許任務掛起的基本類是Condition,你可以通過在Condition上調用await()來掛起一個任務。當外部條件發生變化,意味着某個任務應該繼續執行時,你可以通過調用sign()來通知這個任務,從而喚醒一個任務,或者調用signalAll()來喚醒所有在這個Condition上被其自身掛起的任務(與使用notifyAll()相比,signalAll()是更安全的方式)
  • Condition對象可以通過Lock創建
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();

21.5.4生產者-消費者與隊列

wait()和notifyAll()方法以一種非常低級的方式解決了任務互操作問題,即每次交互時都握手,可以瞄向更的抽象級別,使用同步隊列來解決任務協作問題,同步隊列在任何時刻只允許一個任務插入或移除元素。

  • 父接口BlockingQueue
  • 實現類1:linkedBlockingQueue——無界隊列
  • 實現類2:ArrayBlockingQueue——有界隊列
  • 使用BlockingQueue的實現類對象時,只需要正常編寫代碼就可以,只是在放入和取出的時候用put()和take()方法即可。

21.5.5任務間使用管道進行輸入/輸出

  • 可以通過管道在線程之間進行通信。提供線程功能的類庫以“管道”的形式對線程間的輸入/輸出提供了支持。他們在java輸入/輸出類庫中的對應物就是PipedWriter和PipedReader。這個模型可以看做是“生產者-消費者 ”問題的變體,這裏的管道就是一個封裝好的解決方案。
  • PipedReader和PipedWriter是可以通過線程池的shutdownNow()關閉的,它與普通I/O流不同,普通的I/O流不能通過線程池的shutdownNow()關閉。

21.6 死鎖

  • 死鎖必須同時滿足四個條件,纔會死鎖
    1.互斥條件。任務使用的資源至少有一種資源是不能共享的
    2.至少有一個任務它必須有一個資源並當前正在等待獲取被別的任務持有的資源
    3.資源不能被搶佔,任務必須把資源釋放當作普通條件
    4.必須循環等待,這時一個任務等待其他任務所持有的資源,後者有等待另一個任務持有的資源,這樣一直等待下去,直到一個任務等待第一個任務持有的資源。使得大家都鎖住
  • 科學家加餐死鎖案例
    有5個科學家,5只筷子,做座一個圓圈,他們都是先拿起右邊的筷子,再拿起左邊的筷子,當有兩雙筷子時才能吃飯,當只拿起右邊的筷子時會等待,直到左邊的筷子能拿到,最後一個人(第五個人)的右手筷子是第一個人的左手筷子(他們坐成一個圓)。吃完飯就把筷子放回去,然後去思考,思考一會後再去吃飯,吃飯和思考交替進行;
    當思考的時間非常短時,他們會頻繁的吃飯,導致筷子產生競爭,當所有人都拿着右手的筷子等待左手的筷子時,就產生死鎖了(他們是一個圓)
    解決辦法之一:可以把最後一個人(或任意一人)拿筷子的順序改成先左後右,那麼就不會有死鎖了
  • 練習題31中,引用LinkedBlockingQueue,把5根筷子都放入LinkedBlockingQueue中,每次科學家從這個阻塞隊列裏面拿筷子,是否會避免死鎖?
    通過跑習題答案代碼,把思考的時間設爲零,很快結果顯示也會死鎖,雖然LinkedBlockingQueue能很好的互斥資源,但是他並不能完全消除每個科學家都拿起同一方向的筷子,等待另一個方向筷子的可能性。

21.7.1CountDownLatch

可以向CountDownLatch對象設置一個初始計數值,任何在這個對象上調用wait()的方法都將阻塞,直至這個計數器值到達0。而其他任務調用相同的CountDownlatch對象上的CountDown()來減小這個計數值。CountDownLatch被設計只觸發一次,這個值不能被重置。重置版本是CyclicBarrier。

21.7.3 DelayQueue、PriorityBlockingQueue、ScheduledThreadPoolExecutor、Semaphore、Exchanger

  • DelayQueue——一個無界的BlockingQueue,用於放置實現了Delayed接口的對象(Delayed接口又繼承Comparable接口),其中的對象只能在其到期時才能從隊列中取走。它是有序的,估計使用Comparable接口排的序。直接打印這個隊列是無序的,take()方法取元素的時候纔會找到最先到期的元素,是優先級隊列的一種變體
  • PriorityBlockingQueue——一個基礎的優先級隊列,對象是按照優先級順序從隊列中出現的任務,也用take(),元素也要實現Comparable接口,否則拋異常,PriorityBlockingQueue本身是無序,在調用take()方法的時候會排序。
  • ScheduledThreadPoolExecutor——每個任務在預定的時間運行。通過schedule()(運行一次)或者scheduleAtFixedRate()(每隔規則的時間重複執行任務)
  • Semaphore——簡稱計數信號量,允許多個任務同時訪問這個資源,它的構造函數new Semaphore(SIZE,true),允許有SIZE個共享次數,它的簽入、簽出調用的方法是acquire()和release();每次acquire執行一次,共享次數減一,release()後共享次數加一。
  • Exchanger——典型應用場景是:一個任務在創建對象,這些對象的生產代價很高昂,而另一個任務在消費這些對象。通過Exchanger,可以有更多的對象在創建的同時被消費。
    創建一個生產者任務和一個消費者任務,他們都有一個共同的Exchanger,在各自的Exchanger中調用exchanger(有參函數)方法,會阻塞直到有對象消費;

仿真

  • 仿真中使用併發,仿真的每個構件都可以爲其自身的任務,這使得仿真更容易編程。
  • 仿真中使用隊列的一個好處就是,反轉控制,把原來的併發需要注意的,比如synchronized方法或者各種鎖問題,抽離出來,交給隊列來處理,這樣我們可以像編寫普通程序一樣編寫代碼;用處理消息(就是一個方法)方式來對待併發;這樣構建出健壯的併發系統的可能性就會大大增加。
  • 還有一個啓發:一個資源可能會被多個線程使用,因此我們需要以明顯的方式使其成爲線程安全的。把這種方式可以描述爲:在公園中,你會在陡峭的坡路上發現一些保護圍欄,並且可能會發現標記聲明:“不要依靠圍欄。”當然,這條規則的真是目的不是要阻止你藉助圍欄,而是防止你跌落懸崖。但是“不要依靠圍欄”與“不要跌落懸崖”相比,是一條遵循起來要容易得多的規則。

免鎖容器

CopyOnWriteArrayList——較SynchronizedArraylist,速度快
CopyOnWriteSet
ConcurrentHashMap——較SynchronizedHashMap,速度快
ConcurrentLinkedQueue

樂觀加鎖

原子類調用comparaAndSet(oldValue,newValue),返回boolean

ReadwriteLock

多個讀,少量寫性能快

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章