Java多線程(二) —— 線程安全、線程同步、線程間通信(含面試題集)
一、線程安全
多個線程在執行同一段代碼的時候,每次的執行結果和單線程執行的結果都是一樣的,不存在執行結果的二義性,就可以稱作是線程安全的。
講到線程安全問題,其實是指多線程環境下對共享資源的訪問可能會引起此共享資源的不一致性。因此,為避免線程安全問題,應該避免多線程環境下對此共享資源的並發訪問。
線程安全問題多是由全局變量和靜態變量引起的,當多個線程對共享數據只執行讀操作,不執行寫操作時,一般是線程安全的;當多個線程都執行寫操作時,需要考慮線程同步來解決線程安全問題。
二、線程同步(synchronized/Lock)
線程同步:將操作共享數據的代碼行作為一個整體,同一時間只允許一個線程執行,執行過程中其他線程不能參與執行。目的是為了防止多個線程訪問一個數據對象時,對數據造成的破壞。
(1)同步方法(synchronized)
對共享資源進行訪問的方法定義中加上synchronized關鍵字修飾,使得此方法稱為同步方法。可以簡單理解成對此方法進行了加鎖,其鎖對象為當前方法所在的對象自身。多線程環境下,當執行此方法時,首先都要獲得此同步鎖(且同時最多只有一個線程能夠獲得),只有當線程執行完此同步方法後,才會釋放鎖對象,其他的線程才有可能獲取此同步鎖,以此類推...格式如下:
public synchronized void run() { // .... }
(2)同步代碼塊(synchronized)
使用同步方法時,使得整個方法體都成為了同步執行狀態,會使得可能出現同步範圍過大的情況,於是,針對需要同步的代碼可以直接另一種同步方式——同步代碼塊來解決。格式如下:
synchronized (obj) { // .... }
其中,obj為鎖對象,因此,選擇哪一個對象作為鎖是至關重要的。一般情況下,都是選擇此共享資源對象作為鎖對象。
(3)同步鎖(Lock)
使用Lock對象同步鎖可以方便地解決選擇鎖對象的問題,唯一需要註意的一點是Lock對象需要與資源對象同樣具有一對一的關系。Lock對象同步鎖一般格式為:
class X { // 顯示定義Lock同步鎖對象,此對象與共享資源具有一對一關系 private final Lock lock = new ReentrantLock(); publicvoid m(){ // 加鎖 lock.lock(); //... 需要進行線程安全同步的代碼 // 釋放Lock鎖 lock.unlock(); } }
什麽時候需要同步:
(1)可見性同步:在以下情況中必須同步: 1)讀取上一次可能是由另一個線程寫入的變量 ;2)寫入下一次可能由另一個線程讀取的變量
(2)一致性同步:當修改多個相關值時,您想要其它線程原子地看到這組更改—— 要麽看到全部更改,要麽什麽也看不到。
這適用於相關數據項(如粒子的位置和速率)和元數據項(如鏈表中包含的數據值和列表自身中的數據項的鏈)。
在某些情況中,您不必用同步來將數據從一個線程傳遞到另一個,因為 JVM 已經隱含地為您執行同步。這些情況包括:
- 由靜態初始化器(在靜態字段上或 static{} 塊中的初始化器)
- 初始化數據時
- 訪問 final 字段時
- 在創建線程之前創建對象時
- 線程可以看見它將要處理的對象時
鎖的原理:
- Java中每個對象都有一個內置鎖
- 當程序運行到非靜態的synchronized同步方法上時,自動獲得與正在執行代碼類的當前實例(this實例)有關的鎖。獲得一個對象的鎖也稱為獲取鎖、鎖定對象、在對象上鎖定或在對象上同步。
- 當程序運行到synchronized同步方法或代碼塊時才該對象鎖才起作用。
- 一個對象只有一個鎖。所以,如果一個線程獲得該鎖,就沒有其他線程可以獲得鎖,直到第一個線程釋放(或返回)鎖。這也意味著任何其他線程都不能進入該對象上的synchronized方法或代碼塊,直到該鎖被釋放。
- 釋放鎖是指持鎖線程退出了synchronized同步方法或代碼塊。
三、線程通信:wait()/notify()/notifyAll()
wait():導致當前線程等待並使其進入到等待阻塞狀態。直到其他線程調用該同步鎖對象的notify()或notifyAll()方法來喚醒此線程。
- void wait(long timeout) -- 導致當前線程等待,直到其他線程調用此對象的 notify() 方法或 notifyAll() 方法,或者超過指定的時間量。
- void wait(long timeout, int nanos) -- 導致當前線程等待,直到其他線程調用此對象的 notify() 方法或 notifyAll() 方法,或者其他某個線程中斷當前線程,或者已超過某個實際時間量。
notify():喚醒在此同步鎖對象上等待的單個線程,如果有多個線程都在此同步鎖對象上等待,則會任意選擇其中某個線程進行喚醒操作,只有當前線程放棄對同步鎖對象的鎖定,才可能執行被喚醒的線程。
notifyAll():喚醒在此同步鎖對象上等待的所有線程,只有當前線程放棄對同步鎖對象的鎖定,才可能執行被喚醒的線程。
這三個方法主要都是用於多線程中,但實際上都是Object類中的本地方法。因此,理論上,任何Object對象都可以作為這三個方法的主調,在實際的多線程編程中,只有同步鎖對象調這三個方法,才能完成對多線程間的線程通信。
註意點:
1.wait()方法執行後,當前線程立即進入到等待阻塞狀態,其後面的代碼不會執行;
2.notify()/notifyAll()方法執行後,將喚醒此同步鎖對象上的(任意一個-notify()/所有-notifyAll())線程對象,但是,此時還並沒有釋放同步鎖對象,也就是說,如果notify()/notifyAll()後面還有代碼,還會繼續進行,知道當前線程執行完畢才會釋放同步鎖對象;
3.notify()/notifyAll()執行後,如果右面有sleep()方法,則會使當前線程進入到阻塞狀態,但是同步對象鎖沒有釋放,依然自己保留,那麽一定時候後還是會繼續執行此線程,接下來同2;
4.wait()/notify()/nitifyAll()完成線程間的通信或協作都是基於不同對象鎖的,因此,如果是不同的同步對象鎖將失去意義,同時,同步對象鎖最好是與共享資源對象保持一一對應關系;
5.當wait線程喚醒後並執行時,是接著上次執行到的wait()方法代碼後面繼續往下執行的。
四、相關面試題
1. 線程和進程有什麽區別?
答:一個進程是一個獨立(self contained)的運行環境,它可以被看作一個程序或者一個應用。而線程是在進程中執行的一個任務。線程是進程的子集,一個進程可以有很多線程,每條線程並行執行不同的任務。不同的進程使用不同的內存空間,而所有的線程共享一片相同的內存空間。別把它和棧內存搞混,每個線程都擁有單獨的棧內存用來存儲本地數據。
2. 如何在Java中實現線程?比較這種種方式
答:創建線程有兩種方式:
(1)繼承 Thread 類,擴展線程。
(2)實現 Runnable 接口。
繼承Thread類的方式有它固有的弊端,因為Java中繼承的單一性,繼承了Thread類就不能繼承其他類了;同時也不符合繼承的語義,Dog跟Thread沒有直接的父子關系,繼承Thread只是為了能擁有一些功能特性。
而實現Runnable接口,①避免了單一繼承的局限性,②同時更符合面向對象的編程方式,即將線程對象進行單獨的封裝,③而且實現接口的方式降低了線程對象(Dog)和線程任務(run方法中的代碼)的耦合性,④如上面所述,可以使用同一個Dog類的實例來創建並開啟多個線程,非常方便的實現資源的共享。實際上Thread類也是實現了Runnable接口。實際開發中多是使用實現Runnable接口的方式。
3. 啟動一個線程是調用run()還是start()方法?
答:啟動一個線程是調用start()方法,使線程所代表的虛擬處理機處於可運行狀態,這意味著它可以由JVM 調度並執行,這並不意味著線程就會立即運行。run()方法是線程啟動後要進行回調(callback)的方法。
4. wai()t和sleep()比較
共同點:
1). 他們都是在多線程的環境下,sleep()方法和對象的wait()方法都可以讓線程暫停執行,都可以在程序的調用處阻塞指定的毫秒數,並返回。
2). wait()和sleep()都可以通過interrupt()方法打斷線程的暫停狀態 ,從而使線程立刻拋出InterruptedException。
如果線程A希望立即結束線程B,則可以對線程B對應的Thread實例調用interrupt方法。如果此刻線程B正在wait/sleep /join,則線程B會立刻拋出InterruptedException,在catch() {} 中直接return即可安全地結束線程。 需要註意的是,InterruptedException是線程自己從內部拋出的,並不是interrupt()方法拋出的。對某一線程調用 interrupt()時,如果該線程正在執行普通的代碼,那麽該線程根本就不會拋出InterruptedException。但是,一旦該線程進入到 wait()/sleep()/join()後,就會立刻拋出InterruptedException 。
不同點:
1). Thread類的方法:sleep(),yield()等
Object類的方法:wait()和notify()等
2). 每個對象都有一個鎖來控制同步訪問。Synchronized關鍵字可以和對象的鎖交互,來實現線程的同步。
sleep()方法讓當前線程暫停執行指定的時間,將執行機會(CPU)讓給其他線程,但是對象的鎖依然保持,休眠結束後線程會自動回到就緒狀態;
wait()方法導致當前線程放棄對象的鎖(線程暫停執行),進入對象的等待池(wait pool),只有調用對象的notify()方法(或notifyAll()方法)時才能喚醒等待池中的線程進入等鎖池(lock pool),如果線程重新獲得對象的鎖就可以進入就緒狀態。
3). wait,notify和notifyAll只能在同步控制方法或者同步控制塊裏面使用,而sleep可以在任何地方使用
4). sleep必須捕獲異常,而wait,notify和notifyAll不需要捕獲異常
所以sleep()和wait()方法的最大區別是:
sleep()睡眠時,保持對象鎖,仍然占有該鎖;
而wait()睡眠時,釋放對象鎖。
但是wait()和sleep()都可以通過interrupt()方法打斷線程的暫停狀態,從而使線程立刻拋出InterruptedException(但不建議使用該方法)。
5. sleep()方法和yield()方法有什麽區別?
① sleep()方法給其他線程運行機會時不考慮線程的優先級,因此會給低優先級的線程以運行的機會;yield()方法只會給相同優先級或更高優先級的線程以運行的機會;
② 線程執行sleep()方法後轉入阻塞(blocked)狀態,而執行yield()方法後轉入就緒(ready)狀態;
③ sleep()方法需要聲明拋出InterruptedException,而yield()方法沒有聲明任何異常;
④ sleep()方法比yield()方法(跟操作系統CPU調度相關)具有更好的可移植性。
6. 線程類的一些常用方法:
- sleep(): 強迫一個線程睡眠N毫秒,是一個靜態方法,調用此方法要處理InterruptedException異常;
- join(): 讓一個線程等待另一個線程完成才繼續執行;
- yeild(): 線程讓步,暫停當前正在執行的線程對象讓出CPU資源,將當前線程從運行狀態轉換到就緒狀態並執行其他優先級相同或更高的線程;
- isAlive(): 判斷一個線程是否存活。
- activeCount(): 程序中活躍的線程數。
- enumerate(): 枚舉程序中的線程。
- currentThread(): 得到當前線程。
- isDaemon(): 一個線程是否為守護線程。
- setDaemon(): 設置一個線程為守護線程。(用戶線程和守護線程的區別在於,是否等待主線程依賴於主線程結束而結束)
- setName(): 為線程設置一個名稱。
- setPriority(): 設置一個線程的優先級。
- wait():使一個線程處於等待(阻塞)狀態,並且釋放所持有的對象的鎖;
- notify():喚醒一個處於等待狀態的線程,當然在調用此方法的時候,並不能確切的喚醒某一個等待狀態的線程,而是由JVM確定喚醒哪個線程,而且與優先級無關;
- notityAll():喚醒所有處於等待狀態的線程,該方法並不是將對象的鎖給所有線程,而是讓它們競爭,只有獲得鎖的線程才能進入就緒狀態;
7. 同步代碼塊和同步方法的區別
兩者的區別主要體現在同步鎖上面。對於實例的同步方法,因為只能使用this來作為同步鎖,如果一個類中需要使用到多個鎖,為了避免鎖的沖突,必然需要使用不同的對象,這時候同步方法不能滿足需求,只能使用同步代碼塊(同步代碼塊可以傳入任意對象);或者多個類中需要使用到同一個鎖,這時候多個類的實例this顯然是不同的,也只能使用同步代碼塊,傳入同一個對象。
8. 對比synchronized和Lock
1)、synchronized是關鍵字,就和if...else...一樣,是語法層面的實現,因此synchronized獲取鎖以及釋放鎖都是Java虛擬機幫助用戶完成的;ReentrantLock是類層面的實現,因此鎖的獲取以及鎖的釋放都需要用戶自己去操作。特別再次提醒,ReentrantLock在lock()完了,一定要手動unlock(),一般放在finally語句塊中。
2)、synchronized簡單,簡單意味著不靈活,而ReentrantLock的鎖機制給用戶的使用提供了極大的靈活性。這點在Hashtable和ConcurrentHashMap中體現得淋漓盡致。synchronized一鎖就鎖整個Hash表,而ConcurrentHashMap則利用ReentrantLock實現了鎖分離,鎖的只是segment而不是整個Hash表
3)、synchronized是不公平鎖,而ReentrantLock可以指定鎖是公平的還是非公平的
4)、synchronized實現等待/通知機制通知的線程是隨機的,ReentrantLock實現等待/通知機制可以有選擇性地通知
5)、和synchronized相比,ReentrantLock提供給用戶多種方法用於鎖信息的獲取,比如可以知道lock是否被當前線程獲取、lock被同一個線程調用了幾次、lock是否被任意線程獲取等等
總結起來,我認為如果只需要鎖定簡單的方法、簡單的代碼塊,那麽考慮使用synchronized,復雜的多線程處理場景下可以考慮使用ReentrantLock。
參考鏈接:
http://www.importnew.com/21136.html
http://lavasoft.blog.51cto.com/62575/99155/
http://www.cnblogs.com/lwbqqyumidi/p/3821389.html
Java多線程(二) —— 線程安全、線程同步、線程間通信(含面試題集)