線程同步
當多個線程訪問同一個數據時,非常容易出現線程安全問題。這時候就需要用線程同步
Case:銀行取錢問題,有以下步驟:
A、用戶輸入賬戶、密碼,系統判斷是否登錄成功
B、用戶輸入取款金額
C、系統判斷取款金額是否大於現有金額
D、如果金額大於取款金額,就成功,否則提示小於餘額
現在模擬2個人同時對一個賬戶取款,多線程操作就會出現問題。這時候需要同步才行;
同步代碼塊:
synchronized (object) {
//同步代碼
}
Java多線程支持方法同步,方法同步只需用用synchronized來修飾方法即可,那麼這個方法就是同步方法了。
對於同步方法而言,無需顯示指定同步監視器,同步方法監視器就是本身this
同步方法:
public synchronized void editByThread() {
//doSomething
}
需要用同步方法的類具有以下特徵:
A、該類的對象可以被多個線程訪問
B、每個線程調用對象的任意都可以正常的結束,返回正常結果
C、每個線程調用對象的任意方法後,該對象狀態保持合理狀態
不可變類總是線程安全的,因爲它的對象狀態是不可改變的,但可變類對象需要額外的方法來保證線程安全。
例如Account就是一個可變類,它的money就是可變的,當2個線程同時修改money時,程序就會出現異常或錯誤。
所以要對Account設置爲線程安全的,那麼就需要用到同步synchronized關鍵字。
下面的方法用synchronized同步關鍵字修飾,那麼這個方法就是一個同步的方法。這樣就只能有一個線程可以訪問這個方法,
在當前線程調用這個方法時,此方法是被鎖狀態,同步監視器是this。只有當此方法修改完畢後其他線程才能調用此方法。
這樣就可以保證線程的安全,處理多線程併發取錢的的安全問題。
public synchronized void drawMoney(double money) {
//取錢操作
}
注意:synchronized可以修飾方法、代碼塊,但不能修飾屬性、構造方法
可變類的線程安全是以降低程序的運行效率爲代價,爲了減少線程安全所帶來的負面影響,可以採用以下策略:
A、不要對線程安全類的所有方法都採用同步模式,只對那些會改變競爭資源(共享資源)的方法進行同步。
B、如果可變類有2中運行環境:單線程環境和多線程環境,則應該爲該可變提供2種版本;線程安全的和非線程安全的版本。
在單線程下采用非線程安全的提高運行效率保證性能,在多線程環境下采用線程安全的控制安全性問題。
釋放同步監視器的鎖定
任何線程進入同步代碼塊、同步方法之前,必須先獲得對同步監視器的鎖定,那麼何時會釋放對同步監視器鎖定?
程序無法顯示的釋放對同步監視器的鎖定,線程可以通過以下方式釋放鎖定:
A、當線程的同步方法、同步代碼庫執行結束,就可以釋放同步監視器
B、當線程在同步代碼庫、方法中遇到break、return終止代碼的運行,也可釋放
C、當線程在同步代碼庫、同步方法中遇到未處理的Error、Exception,導致該代碼結束也可釋放同步監視器
D、當線程在同步代碼庫、同步方法中,程序執行了同步監視器對象的wait方法,導致方法暫停,釋放同步監視器
下面情況不會釋放同步監視器:
A、當線程在執行同步代碼庫、同步方法時,程序調用了Thread.sleep()/Thread.yield()方法來暫停當前程序,當前程序不會釋放同步監視器
B、當線程在執行同步代碼庫、同步方法時,其他線程調用了該線程的suspend方法將該線程掛起,該線程不會釋放同步監視器。注意儘量避免使用suspend、resume
同步鎖(Lock)
通常認爲:Lock提供了比synchronized方法和synchronized代碼塊更廣泛的鎖定操作,Lock更靈活的結構,有很大的差別,並且可以支持多個Condition對象
Lock是控制多個線程對共享資源進行訪問的工具。通常,鎖提供了對共享資源的獨佔訪問,每次只能有一個線程對Lock對象加鎖,
線程開始訪問共享資源之前應先獲得Lock對象。不過某些鎖支持共享資源的併發訪問,如:ReadWriteLock(讀寫鎖),在線程安全控制中,
通常使用ReentrantLock(可重入鎖)。使用該Lock對象可以顯示加鎖、釋放鎖。
class C {
//鎖對象
private final ReentrantLock lock = new ReentrantLock();
......
//保證線程安全方法
public void method() {
//上鎖
lock.lock();
try {
//保證線程安全操作代碼
} catch() {
} finally {
lock.unlock();//釋放鎖
}
}
}
使用Lock對象進行同步時,鎖定和釋放鎖時注意把釋放鎖放在finally中保證一定能夠執行。
使用鎖和使用同步很類似,只是使用Lock時顯示的調用lock方法來同步。而使用同步方法synchronized時系統會隱式使用當前對象作爲同步監視器,
同樣都是“加鎖->訪問->釋放鎖”的操作模式,都可以保證只能有一個線程操作資源。
同步方法和同步代碼塊使用與競爭資源相關的、隱式的同步監視器,並且強制要求加鎖和釋放鎖要出現在一個塊結構中,而且獲得多個鎖時,
它們必須以相反的順序釋放,且必須在與所有鎖被獲取時相同的範圍內釋放所有資源。
Lock提供了同步方法和同步代碼庫沒有的其他功能,包括用於非塊結構的tryLock方法,已經試圖獲取可中斷鎖lockInterruptibly()方法,
還有獲取超時失效鎖的tryLock(long, timeUnit)方法。
ReentrantLock具有重入性,也就是說線程可以對它已經加鎖的ReentrantLock再次加鎖,ReentrantLock對象會維持一個計數器來追蹤lock方法的嵌套調用,
線程在每次調用lock()加鎖後,必須顯示的調用unlock()來釋放鎖,所以一段被保護的代碼可以調用另一個被相同鎖保護的方法。
死鎖
當2個線程相互等待對方是否同步監視器時就會發生死鎖,JVM沒有采取處理死鎖的措施,這需要我們自己處理或避免死鎖。
一旦死鎖,整個程序既不會出現異常,也不會出現錯誤和提示,只是線程將處於阻塞狀態,無法繼續。
主線程保持對Foo的鎖定,等待對Bar對象加鎖,而副線程卻對Bar對象保持鎖定,等待對Foo加鎖2條線程相互等待對方先釋放鎖,進入死鎖狀態。
由於Thread類的suspend也很容易導致死鎖,所以Java不推薦使用此方法暫停線程。
線程通信
(1)、線程的協調運行
場景:用2個線程,這2個線程分別代表存款和取款。——現在系統要求存款者和取款者不斷重複的存款和取款的動作,
而且每當存款者將錢存入賬戶後,取款者立即取出這筆錢。不允許2次連續存款、2次連續取款。
實現上述場景需要用到Object類,提供的wait、notify和notifyAll三個方法,這3個方法並不屬於Thread類。但這3個方法必須由同步監視器調用,可分爲2種情況:
A、對於使用synchronized修飾的同步方法,因爲該類的默認實例this就是同步監視器,所以可以在同步中直接調用這3個方法。
B、對於使用synchronized修改的同步代碼塊,同步監視器是synchronized後可括號中的對象,所以必須使用括號中的對象調用這3個方法
方法概述:
一、wait方法:導致當前線程進入等待,直到其他線程調用該同步監視器的notify方法或notifyAll方法來喚醒該線程。
wait方法有3中形式:無參數的wait方法,會一直等待,直到其他線程通知;帶毫秒參數的wait和微妙參數的wait,
這2種形式都是等待時間到達後甦醒。調用wait方法的當前線程會釋放對該對象同步監視器的鎖定。
二、notify:喚醒在此同步監視器上等待的單個線程。如果所有線程都在此同步監視器上等待,則會隨機選擇喚醒其中一個線程。
只有當前線程放棄對該同步監視器的鎖定後(用wait方法),纔可以執行被喚醒的線程。
三、notifyAll:喚醒在此同步監視器上等待的所有線程。只有當前線程放棄對該同步監視器的鎖定後,才能執行喚醒的線程。
(2)、條件變量控制協調
如果程序不使用synchronized關鍵字來保證同步,而是直接使用Lock對象來保證同步,則系統中不存在隱式的同步監視器對象,
也不能使用wait、notify、notifyAll方法來協調進程的運行。
當使用Lock對象同步,Java提供一個Condition類來保持協調,使用Condition可以讓那些已經得到Lock對象卻無法組合使用,
爲每個對象提供了多個等待集(wait-set),這種情況下,Lock替代了同步方法和同步代碼塊,Condition替代同步監視器的功能。
Condition實例實質上被綁定在一個Lock對象上,要獲得特定的Lock實例的Condition實例,調用Lock對象的newCondition即可。
Condition類方法介紹:
一、await:類似於隱式同步監視器上的wait方法,導致當前程序等待,直到其他線程調用Condition的signal方法和signalAll方法來喚醒該線程。
該await方法有跟多獲取變體:long awaitNanos(long nanosTimeout),void awaitUninterruptibly()、awaitUntil(Date daadline)
二、signal:喚醒在此Lock對象上等待的單個線程,如果所有的線程都在該Lock對象上等待,則會選擇隨機喚醒其中一個線程。
只有當前線程放棄對該Lock對象的鎖定後,使用await方法,纔可以喚醒在執行的線程。
三、signalAll:喚醒在此Lock對象上等待的所有線程。只有當前線程放棄對該Lock對象的鎖定後,纔可以執行被喚醒的線程。
(3)、使用管道流
線程通信使用管道流,管道流有3種形式:
PipedInputStream、PipedOutputStream、PipedReader和PipedWriter以及Pipe.SinkChannel和Pipe.SourceChannel,
它們分別是管道流的字節流、管道字符流和新IO的管道Channel。
管道流通信基本步驟:
A、使用new操作法來創建管道輸入、輸出流
B、使用管道輸入流、輸出流的connect方法把2個輸入、輸出流連接起來
C、將管道輸入、輸出流分別傳入2個線程
D、2個線程可以分別依賴各自的管道輸入流、管道輸出流進行通信
線程組和未處理異常
ThreadGroup表示線程組,它可以表示一批線程進行分類管理,Java允許程序對
Java允許直接對線程組控制,對線程組控制相對於同時控制這批線程。用戶創建的所有線程都屬於指定的線程組。
如果程序沒有值得線程屬於哪個組,那這個線程就屬於默認線程組。在默認情況下,子線程和創建它父線程屬於同一組。
一旦某個線程加入了指定線程組之後,該線程將屬於該線程組,直到該線程死亡,線程運行中途不能改變它所屬的線程組。
Thread類提供一些構造設置線程所屬的哪個組,具有以下方法:
A、Thread(ThreadGroup group, Runnable target):target的run方法作爲線程執行體創建新線程,屬於group線程組
B、Thread(ThreadGroup group, Runnalbe target, String name):target的run方法作爲線程執行體創建的新線程,該線程屬於group線程組,且線程名爲name
C、Thread(ThreadGroup group, String name):創建新線程,新線程名爲name,屬於group組
因爲中途不能改變線程所屬的組,所以Thread提供ThreadGroup的setter方法,但提供了getThreadGroup方法來返回該線程所屬的線程組,
getThreadGroup方法的返回值是ThreadGroup對象的表示,表示一個線程組。
ThreadGroup有2個構造形式:
A、ThreadGroup(String name):name線程組的名稱
B、ThreadGroup(ThreadGroup parent, String name):指定名稱、指定父線程組創建的一個新線程組
上面的構造都指定線程名稱,也就是線程組都必須有自己的一個名稱,可以通過調用ThreadGroup的getName方法得到,
但不允許中途改變名稱。ThreadGroup有以下常用的方法:
A、activeCount:返回線程組活動線程數目
B、interrupt:中斷此線程組中的所有線程
C、isDeamon:判斷該線程是否在後臺運行
D、setDeamon:把該線程組設置爲後臺線程組,後臺線程具有一個特徵,當後臺線程的最後一個線程執行結束或最後一個線程被銷燬,後臺線程組自動銷燬。
E、setMaxPriority:設置線程組最高優先級
uncaughtException(Thread t, Throwable e)該方法可以處理該線程組內的線程所拋出的未處理的異常,
Thread.UncaughtExceptionHandler是Thread類的一個內部公共靜態接口,
該接口內只有一個方法:void uncaughtException(Thread t, Throwable e) 該方法中的t代表出現異常的線程,而e代表該線程拋出的異常
Thread類中提供2個方法來設置異常處理器:
A、staticsetDefaultUnaughtExceptionHandler(Thread.UncaughtExceptionHandler eh):爲該線程類的所有線程實例設置默認的異常處理器
B、setUncaughtExceptionHandler(Thread.UncaughtExceptionHander eh):爲指導線程實例設置異常處理器
ThreadGroup實現了Thread.UncaughtExceptionHandler接口,所以每個線程所屬的線程組將會作爲默認的異常處理器。當一個線程拋出未處理異常時,
JVM會首先查找該異常對應的異常處理器,(setUncaughtExceptionHandler設置異常處理器),如果找到該異常處理器,將調用該異常處理器處理異常。
否則,JVM將會調用該線程的所屬線程組的uncaughtException處理異常,線程組處理異常流程如下:
A、如果該線程有父線程組,則調用父線程組的uncaughtException方法來處理異常
B、如果該線程實例所屬的線程類有默認的異常處理器(setDefaultUnaughtExceptionHandler方法設置異常處理器),那就調用該異常處理器來處理異常信息
C、將異常調用棧的信息打印到System.err錯誤輸出流,並結束該線程
Callable和Future
Callable接口定義了一個call方法可以作爲線程的執行體,但call方法比run方法更強大:
A、call方法可以有返回值
B、call方法可以申明拋出異常
Callable接口是JDK5後新增的接口,而且不是Runnable的子接口,所以Callable對象不能直接作爲Thread的target。而且call方法還有一個返回值,
call方法不能直接調用,它作爲線程的執行體被調用。那麼如何接收call方法的返回值?
JDK1.5提供了Future接口來代表Callable接口裏的call方法的返回值,併爲Future接口提供了一個FutureTask實現類,該實現類實現Future接口,
並實現了Runnable接口—可以作爲Thread的target。
Future接口裏定義瞭如下幾個公共方法控制他關聯的Callable任務:
A、boolean cancel(Boolean mayInterruptlfRunning):試圖取消該Future裏關聯的Callable任務
B、V get():返回Callable任務裏的call方法的返回值,調用該方法將導致線程阻塞,必須等到子線程結束纔得到返回值
C、V get(long timeout, TimeUnit unit):返回Callable任務裏的call方法的返回值,該方法讓程序最多阻塞timeout和unit指定的時間。
如果經過指定時間後Callable任務依然沒有返回值,將會拋出TimeoutException。
D、boolean isCancelled:如果在Callable任務正常完成前被取消,則返回true。
E、boolean isDone:如果Callable任務已經完成,則返回true
創建、並啓動有返回值的線程的步驟如下:
一、創建Callable接口的實現類,並實現call方法,該call方法的返回值,並作爲線程的執行體。
二、創建Callable實現類的實例,使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的call方法的返回值
三、使用FutureTask對象作爲Thread對象的target創建、並啓動新線程
四、調用FutureTask對象的方法來獲得子線程執行結束後的返回值