面試中線程相關問題(補充ing.....)

面試中線程相關問題(補充ing.....)




一、線程和進程的關係


1.基本概念

  在併發性程序中,有兩個基本的執行單元:進程和線程。在java編程語言中,併發編程大多數情況下都是和線程相關。然而,進程也是很重要的。 

  進程的含義:在操作系統中,獨立運行的應用程序或者可以說是一個服務

  線程的含義:是進程中的一個執行單元

  一個計算機系統中通常都有很多活動的進程和線程。這一點即使是在只有一個執行核心,並且在給定時刻只能執行一個線程的系統中都是存在的。單一核心的處理時間是由整個操作系統的“時間片”特性來在衆多的進程和線程中共享的。

  現在,計算機系統中有多個處理器或者是有多核處理器的情況越來越普遍。這就大大增強了系統執行多個進程和線程的併發性。


2.進程

  一個進程就是一個獨立的執行環境。進程有着完整的,私有的基本的運行時資源,尤其是每個進程都有自己的內存空間。 

  進程通常會被看作是程序或者是應用程序的同義詞。然而,用戶看到的應用程序實際上可能是多個相互協作的進程。爲了方便進程間通訊,絕大多數的操作系統都支持IPC(Inter Process Communication , 進程間通訊),諸如管道(pipe)和套接字(socket)。 IPC不僅可用於同一系統中進程間的相互通訊,還可以用於不同系統間的進程通訊。 

  大多數的java虛擬機的實現都是作爲一個單獨的進程的。

  通過使用ProcessBuilder,Java應用程序可以創建額外的進程。


3.線程

  線程有時被稱爲是輕型的進程。進程和線程都提供了一種運行環境。但是創建一個新的線程比創建一個新的進程需要更少的資源。 

  線程存在於進程之中——每個進程中至少有一個線程。同一進程的多個線程間共享進程的資源,包括內存和文件。這樣做是出於效率的考慮,但是可能帶來潛在的通信問題 

  多線程是Java平臺的一個重要特性。如果我們將進行諸如內存管理和信號處理的線程算上的“系統”線程計算上的話,那麼每一個應用程序至少都有一個線程,或者是多個線程。但是從應用程序的編程人員的角度來看,我們總是從一個叫做主線程的線程開始。該線程能夠創建其他的線程,比如我們最早寫的帶main方法的類,執行main方法,此時開啓一個進程,該進程包含一個主線程main,然後可以在主線程中創建其他線程。



二、創建線程的三種方式


在java中有3種方式來創建並執行線程


1.通過繼承java.lang.Thread類

(1)定義Thread類的子類,並重寫該類的run方法,該run方法的方法體就代表了線程要完成的任務。因此把run()方法稱爲執行體。

(2)創建Thread子類的實例,即創建了線程對象。

(3)調用線程對象的start()方法來啓動該線程。


2.通過Runnable接口創建線程類

(1)定義runnable接口的實現類,並重寫該接口的run()方法,該run()方法的方法體同樣是該線程的線程執行體。

(2)創建 Runnable實現類的實例,並依此實例作爲Thread的target來創建Thread對象,該Thread對象纔是真正的線程對象。

(3)調用線程對象的start()方法來啓動該線程。


3.通過Callable和Future創建線程

(1)創建Callable接口的實現類,並實現call()方法,該call()方法將作爲線程執行體,並且有返回值。

(2)創建Callable實現類的實例,使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的call()方法的返回值。

(3)使用FutureTask對象作爲Thread對象的target創建並啓動新線程。

(4)調用Thread對象的start方法來啓動線程

(5)調用FutureTask對象的get()方法來獲得子線程執行結束後的返回值



三、Runnable接口和Thread類的區別


  如果一個類繼承Thread,則不適合資源共享。但是如果實現了Runable接口的話,則很容易的實現資源共享。

  總結:

  實現Runnable接口比繼承Thread類所具有的優勢:

  1):適合多個相同的程序代碼的線程去處理同一個資源

  2):可以避免java中的單繼承的限制

  3):增加程序的健壯性,代碼可以被多個線程共享,代碼和數據獨立

  4):線程池只能放入實現Runable或callable類線程,不能直接放入繼承Thread的類

注意:main方法其實也是一個線程。在java中所有的線程都是同時啓動的,至於什麼時候,哪個先執行,完全看誰先得到CPU的資源。

在java中,每次程序運行至少啓動2個線程。一個是main線程,一個是垃圾收集線程。因爲每當使用java命令執行一個類的時候,實際上都會啓動一個jvm,每一個jvm實例就是在操作系統中啓動了一個進程。



四、Runnable接口和Callable接口的區別


Callable接口和Runnable接口相似,

區別就是Callable需要實現call方法,而Runnable需要實現run方法;並且,call方法還可以返回任何對象,無論是什麼對象,JVM都會當作Object來處理。

但是如果使用了泛型,我們就不用每次都對Object進行轉換了。

RunnableCallable都是接口

不同之處:

1.Callable可以返回一個類型V,而Runnable不可以
2.Callable能夠拋出checked exception,Runnable不可以。
3.Runnable是自從java1.1就有了,而Callable1.5之後才加上去的
4.CallableRunnable都可以應用於executors。而Thread類只支持Runnable.
上面只是簡單的不同,其實這兩個接口在用起來差別還是很大的。Callableexecutors聯合在一起,在任務完成時可立刻獲得一個更新了的Future。而Runable卻要自己處理

 

Future接口,一般都是取回Callable執行的狀態用的。其中的主要方法:

cancel   取消Callable的執行,當Callable還沒有完成時

get   獲得Callable的返回值

isCanceled   判斷是否取消了

isDone   判斷是否完成



五、join()方法以及爲什麼要使用


1.概念

  指等待某一線程終止。


2.使用方式。

  join是Thread類的一個方法,啓動線程後直接調用,即join()的作用是:“等待該線程終止”,這裏需要理解的就是該線程是指的主線程等待子線程的終止。也就是在子線程調用了join()方法後面的代碼,只有等到子線程結束了才能執行。


3.爲什麼要使用join()方法

  在很多情況下,主線程生成並起動了子線程,如果子線程裏要進行大量的耗時的運算,主線程往往將於子線程之前結束,但是如果主線程處理完其他的事務後,需要用到子線程的處理結果,也就是主線程需要等待子線程執行完成之後再結束,這個時候就要用到join()方法了。



六、yield方法和sleep方法的區別


1、概念

        1).sleep()

        使當前線程(即調用該方法的線程)暫停執行一段時間,讓其他線程有機會繼續執行,但它並不釋放對象鎖。也就是說如果有
        synchronized同步快,其他線程仍然不能訪問共享數據。注意該方法要捕捉異常。

        2).yield()

        該方法與sleep()類似,只是不能由用戶指定暫停多長時間,並且yield()方法只能讓同優先級的線程有執行的機會。


2、sleep()和yield()的區別

        1).sleep()使當前線程進入停滯狀態,所以執行sleep()的線程在指定的時間內肯定不會被執行;
yield()只是使當前線程重新回到可執行狀態,所以執行yield()的線程有可能在進入到可執行狀態後馬上又被執行。
        2).sleep 方法使當前運行中的線程睡眼一段時間,進入不可運行狀態,這段時間的長短是由程序設定的,yield 方法使當前線程讓出 CPU 佔有權,但讓出的時間是不可設定的。實際上,yield()方法對應瞭如下操作:先檢測當前是否有相同優先級的線程處於同可運行狀態,如有,則把 CPU  的佔有權交給此線程,否則,繼續運行原來的線程。所以yield()方法稱爲“退讓”,它把運行機會讓給了同等優先級的其他線程
        3).另外,sleep 方法允許較低優先級的線程獲得運行機會,但 yield()  方法執行時,當前線程仍處在可運行狀態,所以,不可能讓出較低優先級的線程些時獲得 CPU 佔有權。在一個運行系統中,如果較高優先級的線程沒有調用 sleep 方法,又沒有受到 I\O 阻塞,那麼,較低優先級線程只能等待所有較高優先級的線程運行結束,纔有機會運行。



七、wait()方法和sleep()方法的區別


1、概念


       1).wait()
 

        首先wait()是屬於Object類的方法,從源碼給出的解釋來看,wait()方法可以做到如下幾點:

        (1)首先,調用了wait()之後會引起當前線程處於等待狀狀態。

        (2)其次,每個線程必須持有該對象的monitor。如果在當前線程中調用wait()方法之後,該線程就會釋放monitor的持有對象並讓自己處於等待狀態。

        (3)如果想喚醒一個正在等待的線程,那麼需要開啓一個線程通過notify()或者notifyAll()方法去通知正在等待的線程獲取monitor對象。如此,該線程即可打破等待的狀態繼續執行代碼。


       2).sleep()

        sleep()方法來自於Thread類,從源碼給出的解釋來看,sleep()方法可以做到如下幾點:

        (1)首先,調用sleep()之後,會引起當前執行的線程進入暫時中斷狀態,也即睡眠狀態。

        (2)其次,雖然當前線程進入了睡眠狀態,但是依然持有monitor對象。

        (3)在中斷完成之後,自動進入喚醒狀態從而繼續執行代碼。


2、共同點: 

        1). 他們都是在多線程的環境下,都可以在程序的調用處阻塞指定的毫秒數,並返回。 
        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 。 


3、不同點: 

        1). Thread類的方法:sleep(),yield()等 
        Object的方法:wait()和notify()等 
        2). 每個對象都有一個鎖來控制同步訪問。Synchronized關鍵字可以和對象的鎖交互,來實現線程的同步。 
        sleep方法沒有釋放鎖,而wait方法釋放了鎖,使得其他線程可以使用同步控制塊或者方法。 
        3). wait,notify和notifyAll只能在同步控制方法或者同步控制塊裏面使用,而sleep可以在任何地方使用 


4、sleep()和wait()方法的最大區別是:

        sleep()睡眠時,保持對象鎖,仍然佔有該鎖;
        而wait()睡眠時,釋放對象鎖。
        但是wait()和sleep()都可以通過interrupt()方法打斷線程的暫停狀態,從而使線程立刻拋出InterruptedException(但不建議使用該方法)。

        sleep()方法:

        sleep()使當前線程進入停滯狀態(阻塞當前線程),讓出CUP的使用、目的是不讓當前線程獨自霸佔該進程所獲的CPU資源,以留一定時間給其他線程執行的機會;
        sleep()是Thread類的Static(靜態)的方法;因此他不能改變對象的機鎖,所以當在一個Synchronized塊中調用Sleep()方法是,線程雖然休眠了,但是對象的機鎖並木有
        被釋放,其他線程無法訪問這個對象(即使睡着也持有對象鎖)。
        在sleep()休眠時間期滿後,該線程不一定會立即執行,這是因爲其它線程可能正在運行而且沒有被調度爲放棄執行,除非此線程具有更高的優先級。 

        wait()方法:

        wait()方法是Object類裏的方法;當一個線程執行到wait()方法時,它就進入到一個和該對象相關的等待池中,同時失去(釋放)了對象的機鎖(暫時失去機
        鎖,wait(long timeout)超時時間到後還需要返還對象鎖);其他線程可以訪問;
        wait()使用notify或者notifyAlll或者指定睡眠時間來喚醒當前等待池中的線程。
        wiat()必須放在synchronized block中,否則會在program runtime時扔出”java.lang.IllegalMonitorStateException“異常。



八、什麼是線程安全


        經常用來描繪一段代碼。指在併發的情況之下,該代碼經過多線程使用,線程的調度順序不影響任何結果。這個時候使用多線程,我們只需要關注系統的內存,cpu是不是夠用即可。反過來,線程不安全就意味着線程的調度順序會影響最終結果。

        當多個線程訪問一個對象時,如果不用考慮這些線程在運行時環境的調度和交替執行,也不需要進行額外的同步,或者在調用方進行任何其他的協調操作,調用這個對象的行爲都可以獲得正確的結果,那這個對象就是線程安全的。代碼本身封裝了所有必要的正確性保障手段(互斥同步等),令調用者無需關心多線程的問題,更無需自己實現任何措施來保證多線程的正確調用。



九、如何實現線程安全?


1、互斥同步

       最常見的併發正確性保障手段,同步至多個線程併發訪問共享數據時,保證共享數據在同一個時刻只被一個線程使用。


2、非阻塞同步

       互斥同步的主要問題就是進行線程的阻塞和喚醒所帶來的性能問題,因此這個同步也被稱爲阻塞同步,阻塞同步屬於一種悲觀的併發策略,認爲只要不去做正確的同步措施,就肯定會出問題,無論共享的數據是否會出現競爭。隨着硬件指令的發展,有了另外一個選擇,基於衝突檢測的樂觀併發策略,通俗的講就是先進性操作,如果沒有其他線程爭用共享數據,那操作就成功了,如果共享數據有爭用,產生了衝突,那就再進行其他的補償措施(最常見的措施就是不斷的重試,直到成功爲止),這種策略不需要把線程掛起,所以這種同步也被稱爲非阻塞同步。


3、無同步方案

       簡單的理解就是沒有共享變量需要不同的線程去爭用,目前有兩種方案,一個是“可重入代碼”,這種代碼可以在執行的任何時刻中斷它,轉而去執行其他的另外一段代碼,當控制權返回時,程序繼續執行,不會出現任何錯誤。一個是“線程本地存儲”,如果變量要被多線程訪問,可以使用volatile關鍵字來聲明它爲“易變的“,以此來實現多線程之間的可見性。同時也可以通過ThreadLocal來實現線程本地存儲的功能,一個線程的Thread對象中都有一個ThreadLocalMap對象,來實現KV數據的存儲。



十、synchronized、Lock、ReentrantLock、ReadWriteLock


1、synchronized

1).synchronized關鍵字用法:修飾方法

       a.成員方法

       

       某個對象實例內,synchronized aMethod(){}可以防止多個線程同時訪問這個對象的synchronized方法(如果一個對象有多個synchronized方法,只要一個線程訪問了其中的一個synchronized方法,其它線程不能同時訪問這個對象中任何一個synchronized方法)。這時,不同的對象實例的synchronized方法是不相干擾的。也就是說,其它線程照樣可以同時訪問相同類的另一個對象實例中的synchronized方法; 

       b.靜態方法

       

       是某個類的範圍,synchronized static aStaticMethod{}防止多個線程同時訪問這個類中的synchronized static 方法。它可以對類的所有對象實例起作用。 簡單的可以理解爲將整個類都鎖住了,除非該鎖被釋放,該類的所有的關鍵詞synchronized修飾的靜態方法、語句塊均不可訪問。


2).修飾語句塊

       

       除了方法前用synchronized關鍵字,synchronized關鍵字還可以用於方法中的某個區塊中,表示只對這個區塊的資源實行互斥訪問。用法是: synchronized(this){/*區塊*/},它的作用域是當前對象; 


3).總結

       Java對多線程的支持與同步機制深受大家的喜愛,似乎看起來使用了synchronized關鍵字就可以輕鬆地解決多線程共享數據同步問題。到底如何?――還得對synchronized關鍵字的作用進行深入瞭解纔可定論。

總的說來,synchronized關鍵字可以作爲函數的修飾符,也可作爲函數內的語句,也就是平時說的同步方法和同步語句塊。如果再細的分類,synchronized可作用於instance變量、object reference(對象引用)、static函數和class literals(類名稱字面常量)身上。

在進一步闡述之前,我們需要明確幾點:

       A.無論synchronized關鍵字加在方法上還是對象上,它取得的鎖都是對象,而不是把一段代碼或函數當作鎖――而且同步方法很可能還會被其他線程的對象訪問。

       B.每個對象只有一個鎖(lock)與之相關聯。

       C.實現同步是要很大的系統開銷作爲代價的,甚至可能造成死鎖,所以儘量避免無謂的同步控制。

總的來說

       1、線程同步的目的是爲了保護多個線程反問一個資源時對資源的破壞。

       2、線程同步方法是通過鎖來實現,每個對象都有切僅有一個鎖,這個鎖與一個特定的對象關聯,線程一旦獲取了對象鎖,其他訪問該對象的線程就無法再訪問該對象的其他非同步方法

       3、對於靜態同步方法,鎖是針對這個類的,鎖對象是該類的Class對象。靜態和非靜態方法的鎖互不干預。一個線程獲得鎖,當在一個同步方法中訪問另外對象上的同步方法時,會獲取這兩個對象鎖。

       4、對於同步,要時刻清醒在哪個對象上同步,這是關鍵。

       5、編寫線程安全的類,需要時刻注意對多個線程競爭訪問資源的邏輯和安全做出正確的判斷,對“原子”操作做出分析,並保證原子操作期間別的線程無法訪問競爭資源。

       6、當多個線程等待一個對象鎖時,沒有獲取到鎖的線程將發生阻塞。

       7、死鎖是線程間相互等待鎖鎖造成的,在實際中發生的概率非常的小。真讓你寫個死鎖程序,不一定好使,呵呵。但是,一旦程序發生死鎖,程序將死掉


2、Lock

       Lock比傳統線程模型中的synchronized方式更加面向對象,與生活中的鎖類似,鎖本身也應該是一個對象。兩個線程執行的代碼片段要實現同步互斥的效果,它們必須用同一個Lock對象。

       讀寫鎖:分爲讀鎖和寫鎖,多個讀鎖不互斥,讀鎖與寫鎖互斥,這是由jvm自己控制的,你只要上好相應的鎖即可。如果你的代碼只讀數據,可以很多人同時讀,但不能同時寫,那就上讀鎖;如果你的代碼修改數據,只能有一個人在寫,且不能同時讀取,那就上寫鎖。總之,讀的時候上讀鎖,寫的時候上寫鎖!

 

Lock接口提供的主要方法:

lock()  等待獲取鎖

lockInterruptibly()  可中斷等待獲取鎖,synchronized無法實現可中斷等待

tryLock() 嘗試獲取鎖,立即返回true或false

tryLock(long time, TimeUnit unit)    指定時間內等待獲取鎖

unlock()      釋放鎖

newCondition()   返回一個綁定到此 Lock 實例上的 Condition 實例

關於Lock接口的實現,主要關注以下兩個類

ReentrantLock

ReentrantReadWriteLock


3、ReentrantLock

       可重入鎖,所謂的可重入鎖,也叫遞歸鎖,是指一個線程獲取鎖後,再次獲取該鎖時,不需要重新等待獲取。ReentrantLock分爲公平鎖和非公平鎖,公平鎖指的是嚴格按照先來先得的順序排隊等待去獲取鎖,而非公平鎖每次獲取鎖時,是先直接嘗試獲取鎖,獲取不到,再按照先來先得的順序排隊等待。

注意:ReentrantLock和synchronized都是可重入鎖。


4、ReentrantReadWriteLock

可重入讀寫鎖,指的是沒有線程進行寫操作時,多個線程可同時進行讀操作,當有線程進行寫操作時,其它讀寫操作只能等待。即“讀-讀能共存,讀-寫不能共存,寫-寫不能共存”。

在讀多於寫的情況下,讀寫鎖能夠提供比排它鎖更好的併發性和吞吐量。

ReentrantReadWriteLock會使用兩把鎖來解決問題,一個讀鎖,一個寫鎖
線程進入讀鎖的前提條件:
    沒有其他線程的寫鎖,
    沒有寫請求或者有寫請求,但調用線程和持有鎖的線程是同一個

線程進入寫鎖的前提條件:
    沒有其他線程的讀鎖
    沒有其他線程的寫鎖

到ReentrantReadWriteLock,首先要做的是與ReentrantLock劃清界限。它和後者都是單獨的實現,彼此之間沒有繼承或實現的關係。然後就是總結這個鎖機制的特性了: 
     (a).重入方面其內部的WriteLock可以獲取ReadLock,但是反過來ReadLock想要獲得WriteLock則永遠都不要想。 
     (b).WriteLock可以降級爲ReadLock,順序是:先獲得WriteLock再獲得ReadLock,然後釋放WriteLock,這時候線程將保持Readlock的持有。反過來ReadLock想要升級爲WriteLock則不可能,爲什麼?請參考A 
     (c).ReadLock可以被多個線程持有並且在作用時排斥任何的WriteLock,而WriteLock則是完全的互斥。這一特性最爲重要,因爲對於高讀取頻率而相對較低寫入的數據結構,使用此類鎖同步機制則可以提高併發量。 
     (d).不管是ReadLock還是WriteLock都支持Interrupt,語義與ReentrantLock一致。 
     (e).WriteLock支持Condition並且與ReentrantLock語義一致,而ReadLock則不能使用Condition,否則拋出UnsupportedOperationException異常。



十一、什麼是ThreadLocal


詳細介紹(轉載):http://blog.csdn.net/sonny543/article/details/51336457


十二、線程池及創建線程池的4種方式


1、什麼是線程池

       線程池是指在初始化一個多線程應用程序過程中創建一個線程集合,然後在需要執行新的任務時重用這些線程而不是新建一個線程。線程池中線程的數量通常完全取決於可用內存數量和應用程序的需求。然而,增加可用線程數量是可能的。線程池中的每個線程都有被分配一個任務,一旦任務已經完成了,線程回到池子中並等待下一次分配任務。


2、爲什麼需要線程池

         基於以下幾個原因在多線程應用程序中使用線程是必須的:

         1). 線程池改進了一個應用程序的響應時間。由於線程池中的線程已經準備好且等待被分配任務,應用程序可以直接拿來使用而不用新建一個線程。

         2). 線程池節省了CLR 爲每個短生存週期任務創建一個完整的線程的開銷並可以在任務完成後回收資源。

         3). 線程池根據當前在系統中運行的進程來優化線程時間片。

         4). 線程池允許我們開啓多個任務而不用爲每個線程設置屬性。

         5). 線程池允許我們爲正在執行的任務的程序參數傳遞一個包含狀態信息的對象引用。

         6). 線程池可以用來解決處理一個特定請求最大線程數量限制問題。


3、線程池的概念

       影響一個多線程應用程序的相應時間的幾個主要因素之一是爲每個任務生成一個線程的時間。

       例如,一個Web Server 是一個多線程應用程序,它可以同時對多個客戶端請求提供服務。讓我們假設有十個客戶端同時訪問Web Server:

       a. 如果服務執行一個客戶端對應一個線程的策略,它將爲這些客戶端生成十個新線程,從創建第一個線程開始到在線程的整個生命週期管理它們都會增加系統開銷。也有可能在某個時間計算機的資源耗盡。

       b. 相反的,如果服務端使用一個線程池來處理這些請求,那麼當每次客戶端請求來到後都創建一個線程的時間會節省下來。它可以管理已經創建的線程,如果線程池太忙的話也可以拒絕客戶端請求。這是線程池背後的概念。

       現在回顧一下,影響設計一個多線程應用程序的因素有:

         1. 一個應用程序的響應時間。

         2. 線程管理資源的分配。

         3. 資源共享。

         4. 線程同步。


4、創建線程池的4種方式

       通過Executors工具類可以創建各種類型的線程池,如下爲常見的四種:

       1). newCachedThreadPool :大小不受限,當線程釋放時,可重用該線程;

       2). newFixedThreadPool :大小固定,無可用線程時,任務需等待,直到有可用線程;

       3). newSingleThreadExecutor :創建一個單線程,任務會按順序依次執行;

       4). newScheduledThreadPool:創建一個定長線程池,支持定時及週期性任務執行


十三、ThreadPoolExecutor(線程池)的內部工作原理


詳細介紹(轉載):http://www.jb51.net/article/116521.htm





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