Java多線程(二) —— 線程安全、線程同步、線程間通訊(含面試題集)

上一篇博文:Java多線程(一) —— 線程的狀態詳解中詳細介紹了線程的五種狀態及狀態間的轉換。本文着重介紹了線程安全的相關知識點,包括線程同步和鎖機制、線程間通訊以及相關面試題的總結html

1、線程安全

多個線程在執行同一段代碼的時候,每次的執行結果和單線程執行的結果都是同樣的,不存在執行結果的二義性,就能夠稱做是線程安全的。面試

講到線程安全問題,實際上是指多線程環境下對共享資源的訪問可能會引發此共享資源的不一致性。所以,爲避免線程安全問題,應該避免多線程環境下對此共享資源的併發訪問。編程

線程安全問題可能是由全局變量和靜態變量引發的,當多個線程對共享數據只執行讀操做,不執行寫操做時,通常是線程安全的;當多個線程都執行寫操做時,須要考慮線程同步來解決線程安全問題。安全

 

2、線程同步(synchronized/Lock)

線程同步:將操做共享數據的代碼行做爲一個總體,同一時間只容許一個線程執行,執行過程當中其餘線程不能參與執行。目的是爲了防止多個線程訪問一個數據對象時,對數據形成的破壞。多線程

(1)同步方法(synchronized)併發

對共享資源進行訪問的方法定義中加上synchronized關鍵字修飾,使得此方法稱爲同步方法。能夠簡單理解成對此方法進行了加鎖,其鎖對象爲當前方法所在的對象自身。多線程環境下,當執行此方法時,首先都要得到此同步鎖(且同時最多隻有一個線程可以得到),只有當線程執行完此同步方法後,纔會釋放鎖對象,其餘的線程纔有可能獲取此同步鎖,以此類推...格式以下:post

public synchronized void run() {        
     // ....
}

 

(2)同步代碼塊(synchronized)this

使用同步方法時,使得整個方法體都成爲了同步執行狀態,會使得可能出現同步範圍過大的狀況,因而,針對須要同步的代碼能夠直接另外一種同步方式——同步代碼塊來解決。格式以下:spa

synchronized (obj) {        
     // ....
}

其中,obj爲鎖對象,所以,選擇哪個對象做爲鎖是相當重要的。通常狀況下,都是選擇此共享資源對象做爲鎖對象。操作系統

 

(3)同步鎖(Lock)

 使用Lock對象同步鎖能夠方便地解決選擇鎖對象的問題,惟一須要注意的一點是Lock對象須要與資源對象一樣具備一對一的關係。Lock對象同步鎖通常格式爲:

class X {
    // 顯示定義Lock同步鎖對象,此對象與共享資源具備一對一關係
    private final Lock lock = new ReentrantLock();
    
    public void m(){
        // 加鎖
        lock.lock();
        
        //...  須要進行線程安全同步的代碼
        
        // 釋放Lock鎖
        lock.unlock();
    }
}

 

何時須要同步:

(1)可見性同步:在如下狀況中必須同步: 1)讀取上一次多是由另外一個線程寫入的變量 ;2)寫入下一次可能由另外一個線程讀取的變量

(2)一致性同步:當修改多個相關值時,您想要其它線程原子地看到這組更改—— 要麼看到所有更改,要麼什麼也看不到。

這適用於相關數據項(如粒子的位置和速率)和元數據項(如鏈表中包含的數據值和列表自身中的數據項的鏈)。

在某些狀況中,您沒必要用同步來將數據從一個線程傳遞到另外一個,由於 JVM 已經隱含地爲您執行同步。這些狀況包括:

  1. 由靜態初始化器(在靜態字段上或 static{} 塊中的初始化器)
  2. 初始化數據時 
  3. 訪問 final 字段時
  4. 在建立線程以前建立對象時 
  5. 線程能夠看見它將要處理的對象時

 

鎖的原理:

  • Java中每一個對象都有一個內置鎖
  • 當程序運行到非靜態的synchronized同步方法上時,自動得到與正在執行代碼類的當前實例(this實例)有關的鎖。得到一個對象的鎖也稱爲獲取鎖、鎖定對象、在對象上鎖定或在對象上同步。
  • 當程序運行到synchronized同步方法或代碼塊時才該對象鎖才起做用。
  • 一個對象只有一個鎖。因此,若是一個線程得到該鎖,就沒有其餘線程能夠得到鎖,直到第一個線程釋放(或返回)鎖。這也意味着任何其餘線程都不能進入該對象上的synchronized方法或代碼塊,直到該鎖被釋放。
  • 釋放鎖是指持鎖線程退出了synchronized同步方法或代碼塊。
鎖與同步要點:
1)、只能同步方法,而不能同步變量和類;
2)、每一個對象只有一個鎖;當提到同步時,應該清楚在什麼上同步?也就是說,在哪一個對象上同步?
3)、沒必要同步類中全部的方法,類能夠同時擁有同步和非同步方法。
4)、若是兩個線程要執行一個類中的synchronized方法,而且兩個線程使用相同的實例來調用方法,那麼一次只能有一個線程可以執行方法,另外一個須要等待,直到鎖被釋放。也就是說:若是一個線程在對象上得到一個鎖,就沒有任何其餘線程能夠進入(該對象的)類中的任何一個同步方法。
5)、若是線程擁有同步和非同步方法,則非同步方法能夠被多個線程自由訪問而不受鎖的限制。
6)、線程睡眠時,它所持的任何鎖都不會釋放。
7)、線程能夠得到多個鎖。好比,在一個對象的同步方法裏面調用另一個對象的同步方法,則獲取了兩個對象的同步鎖。
8)、同步損害併發性,應該儘量縮小同步範圍。同步不但能夠同步整個方法,還能夠同步方法中一部分代碼塊。
9)、在使用同步代碼塊時候,應該指定在哪一個對象上同步,也就是說要獲取哪一個對象的鎖。
10)、同步靜態方法,須要一個用於整個類對象的鎖,這個對象是就是這個類(XXX.class)。
 
線程不能得到鎖會怎麼樣:若是線程試圖進入同步方法,而其鎖已經被佔用,則線程在該對象上被阻塞。實質上,線程進入該對象的的一種池中,必須在哪裏等待,直到其鎖被釋放,該線程再次變爲可運行或運行爲止。
 
線程死鎖:當兩個線程被阻塞,每一個線程都在等待另外一個線程時就發生死鎖。有一些設計方法能幫助避免死鎖,如始終按照預約義的順序獲取鎖這一策略。
 
線程同步小結
 
一、線程同步的目的是爲了保護多個線程反問一個資源時對資源的破壞。
二、線程同步方法是經過 來實現,每一個對象都有且僅有一個鎖,這個鎖與一個特定的對象關聯,線程一旦獲取了對象鎖,其餘訪問該對象的線程就沒法再訪問該對象的其餘同步方法。
三、對於靜態同步方法,鎖是針對這個類的,鎖對象是該類的Class對象。靜態和非靜態方法的鎖互不干預。一個線程得到鎖,當在一個同步方法中訪問另外對象上的同步方法時,會獲取這兩個對象鎖。
四、對於同步,要時刻清醒在哪一個對象上同步,這是關鍵。
五、編寫線程安全的類,須要時刻注意對多個線程競爭訪問資源的邏輯和安全作出正確的判斷,對「原子」操做作出分析,並保證原子操做期間別的線程沒法訪問競爭資源。
六、當多個線程等待一個對象鎖時,沒有獲取到鎖的線程將發生阻塞。
七、死鎖是線程間相互等待鎖鎖形成的,在實際中發生的機率很是的小。真讓你寫個死鎖程序,不必定好使,呵呵。可是,一旦程序發生死鎖,程序將死掉。
 

3、線程通訊: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()方法代碼後面繼續往下執行的。

 

4、相關面試題

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. wait()和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

 

Voiatile關鍵字:

volatile關鍵字是Java併發的最輕量級實現,本質上有兩個功能,在生成的彙編語句中加入LOCK關鍵字和內存屏障

做用就是保證每一次線程load和write兩個操做,都會直接從主內存中進行讀取和覆蓋,而非普通變量從線程內的工做空間(默認各位已經熟悉Java多線程內存模型)

但它有一個很致命的缺點,致使它的使用範圍很少,就是他只保證在讀取和寫入這兩個過程是線程安全的。若是咱們對一個volatile修飾的變量進行多線程 下的自增操做,仍是會出現線程安全問題。根本緣由在於volatile關鍵字沒法對自增進行安全性修飾,由於自增分爲三步,讀取-》+1-》寫入。中間多 個線程同時執行+1操做,仍是會出現線程安全性問題。

 

參考連接:

http://www.importnew.com/21136.html

http://lavasoft.blog.51cto.com/62575/99155/

http://www.cnblogs.com/lwbqqyumidi/p/3821389.html

相關文章
相關標籤/搜索