一、基本概念:html
1.1定義java
進程是具備必定獨立功能的程序關於某個數據集合的一次運行活動,進程是操做系統進行資源分配和調度的一個獨立單位;它能夠申請和擁有系統資源,是一個動態的概念,是一個活動的實體,它不僅是程序的代碼,還包括當前的活動,經過程序計數器的值和處理寄存器的內容來表示。面試
線程是進程的一個實體,是cpu調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位,線程本身基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(程序計數器,一組寄存器和棧),可是它可與同屬一個進程的其餘的線程共享進程所擁有的所有資源。算法
1.2關係及區別sql
進程和線程的關係:數據庫
1 一個程序至少有一個進程,一個進程至少有一個線程;線程是指進程內的一個執行單元,也是進程內的可調度實體。資源分配給進程,同一進程的全部線程共享該進程的全部資源;編程
2 一個線程能夠建立和撤銷另外一個線程,同一個進程中的多個線程之間能夠併發執行,相對於進程而言,線程是一個更加接近於執行體的概念,它能夠與同進程中的其餘線程共享數據,但擁有本身的棧空間,擁有獨立的執行序列。瀏覽器
3 線程在執行過程當中,須要協做同步。不一樣進程的線程間要利用消息通訊的辦法實現同步。緩存
進程和線程的區別:安全
1) 資源佔用:進程是擁有資源的一個獨立單位,進程間相互獨立,某進程內的線程在其它進程內不可見;線程不擁有資源,但能夠訪問隸屬於進程的資源。
2) 通訊:進程間通訊IPC,線程間能夠直接讀寫進程數據段(全局變量)來進行通訊—須要進程同步和互斥手段的輔助,以保證數據的一致性。
3) 調度和切換:線程上下文切換比進程上下文切換要快的多。線程做爲調度和分配的基本單位,進程做爲擁有資源的基本單位;
4) 線程在執行過程當中與進程仍是有區別的。每一個獨立的線程有一個程序運行的入口、順序執行序列和程序的出口。但多線程不可以獨立執行,必須依存在應用程序中,有應用程序提供多個線程執行控制;
5) 從邏輯角度來看,多線程的意義在於一個應用程序中,有多個執行部分能夠同時執行,但操做系統並無將多個線程看作多個獨立的應用,來實現進程的調度和管理以及資源分配。
進程和線程的主要差異在於它們是不一樣的操做系統資源管理的方式。進程有獨立的地址空間,一個進程崩潰後,在保護模式下不會對其它進程產生影響,而線程只是一個進程中的不一樣執行路徑,線程有本身的堆棧和局部變量,但線程之間沒有單獨的地址空間,一個線程死掉等於整個進程死掉,因此多進程的程序要比多線程的程序健壯,但在進程切換時,耗費資源較大,效率要差一些。但對於一些要求同時進行而且要共享某些變量的併發操做,只能用線程,不能用進程。
優缺點:
線程執行開銷小,但不利於資源的管理和保護,進程正好相反。同時,線程適合於在SMP機器上運行,而進程則能夠跨機器遷移。
1.3另類解釋
進程和線程是操做系統的基本概念:
1 計算機的核心是CPU,它承擔了全部的計算任務,時刻在運行。CPU相似工廠,假定工廠電力有限,一次只能供給一個車間使用,也就是說,一個車間開工的時候,其餘車間都必須停工。含義是:單個cpu一次只能運行一個任務;
2 進程比如工廠的車間,它表明cpu所能處理的單個任務,任一時刻,cpu老是運行一個進程,其餘進程處於非運行狀態;
3 一個車間裏,能夠有不少工人,它們協同完成一個任務;線程就比如車間裏的工人。一個進程能夠包括多個線程。
4 車間的空間是工人們共享的,好比許多房間是每一個工人均可以進出的。這象徵一個進程的內存空間是共享的,每一個線程均可以使用這些共享內存。
5 但是,每間房間的大小不一樣,有些房間最多隻能容納一我的,好比廁所。裏面有人的時候,其餘人就不能進去了。這表明一個線程使用某些共享內存時,其餘線程必須等它結束,才能使用這一塊內存。
一個防止他人進入的簡單方法,就是門口加一把鎖。先到的人鎖上門,後到的人看到上鎖,就在門口排隊,等鎖打開再進去。這就叫"互斥鎖"(Mutual exclusion,縮寫 Mutex),防止多個線程同時讀寫某一塊內存區域。
6 還有些房間,能夠同時容納n我的,好比廚房。也就是說,若是人數大於n,多出來的人只能在外面等着。這比如某些內存區域,只能供給固定數目的線程使用。
這時的解決方法,就是在門口掛n把鑰匙。進去的人就取一把鑰匙,出來時再把鑰匙掛回原處。後到的人發現鑰匙架空了,就知道必須在門口排隊等着了。這種作法叫作"信號量"(Semaphore),用來保證多個線程不會互相沖突。
不難看出,mutex是semaphore的一種特殊狀況(n=1時)。也就是說,徹底能夠用後者替代前者。可是,由於mutex較爲簡單,且效率高,因此在必須保證資源獨佔的狀況下,仍是採用這種設計。
操做系統的設計,所以能夠歸結爲三點:
(1)以多進程形式,容許多個任務同時運行;
(2)以多線程形式,容許單個任務分紅不一樣的部分運行;
(3)提供協調機制,一方面防止進程之間和線程之間產生衝突,另外一方面容許進程之間和線程之間共享資源。
二、 線程的生命週期:
2.1概述:
當線程被建立並啓動之後,它既不是一啓動就進入了執行狀態,也不是一直處於執行狀態。在線程的生命週期中,它要通過新建(New)、就緒(Runnable)、運行(Running)、阻塞(Blocked)和死亡(Dead)5種狀態。尤爲是當線程啓動之後,它不可能一直"霸佔"着CPU獨自運行,CPU須要在多條線程之間切換,因而線程狀態也會屢次在運行、阻塞之間切換
1. 新建狀態,當程序使用new關鍵字建立了一個線程以後,該線程就處於新建狀態,此時僅由JVM爲其分配內存,並初始化其成員變量的值。
2. 就緒狀態,當線程對象調用了start()方法以後,該線程處於就緒狀態。Java虛擬機會爲其建立方法調用棧和程序計數器,等待調度運行。
3. 運行狀態,若是處於就緒狀態的線程得到了CPU,開始執行run()方法的線程執行體,則該線程處於運行狀態。
4. 阻塞狀態,當處於運行狀態的線程失去所佔用資源以後,便進入阻塞狀態。
5. 在線程的生命週期當中,線程的各類狀態的轉換過程
當程序使用new關鍵字建立了一個線程以後,該線程就處於新建狀態,此時它和其餘的Java對象同樣,僅僅由Java虛擬機爲其分配內存,並初始化其成員變量的值。此時的線程對象沒有表現出任何線程的動態特徵,程序也不會執行線程的線程執行體。
當線程對象調用了start()方法以後,該線程處於就緒狀態。Java虛擬機會爲其建立方法調用棧和程序計數器,處於這個狀態中的線程並無開始運行,只是表示該線程能夠運行了。至於該線程什麼時候開始運行,取決於JVM裏線程調度器的調度。
注意:啓動線程使用start()方法,而不是run()方法。永遠不要調用線程對象的run()方法。調用start0方法來啓動線程,系統會把該run()方法當成線程執行體來處理;但若是直按調用線程對象的run()方法,則run()方法當即就會被執行,並且在run()方法返回以前其餘線程沒法併發執行。也就是說,系統把線程對象當成一個普通對象,而run()方法也是一個普通方法,而不是線程執行體。須要指出的是,調用了線程的run()方法以後,該線程已經再也不處於新建狀態,不要再次調用線程對象的start()方法。只能對處於新建狀態的線程調用start()方法,不然將引起IllegaIThreadStateExccption異常。
調用線程對象的start()方法以後,該線程當即進入就緒狀態——就緒狀態至關於"等待執行",但該線程並未真正進入運行狀態。若是但願調用子線程的start()方法後子線程當即開始執行,程序能夠使用Thread.sleep(1) 來讓當前運行的線程(主線程)睡眠1毫秒,1毫秒就夠了,由於在這1毫秒內CPU不會空閒,它會去執行另外一個處於就緒狀態的線程,這樣就可讓子線程當即開始執行。
若是處於就緒狀態的線程得到了CPU,開始執行run()方法的線程執行體,則該線程處於運行狀態,若是計算機只有一個CPU。那麼在任什麼時候刻只有一個線程處於運行狀態,固然在一個多處理器的機器上,將會有多個線程並行執行;當線程數大於處理器數時,依然會存在多個線程在同一個CPU上輪換的現象。
當一個線程開始運行後,它不可能一直處於運行狀態(除非它的線程執行體足夠短,瞬間就執行結束了)。線程在運行過程當中須要被中斷,目的是使其餘線程得到執行的機會,線程調度的細節取決於底層平臺所採用的策略。對於採用搶佔式策略的系統而言,系統會給每一個可執行的線程一個小時間段來處理任務;當該時間段用完後,系統就會剝奪該線程所佔用的資源,讓其餘線程得到執行的機會。在選擇下一個線程時,系統會考慮線程的優先級。
全部現代的桌面和服務器操做系統都採用搶佔式調度策略,但一些小型設備如手機則可能採用協做式調度策略,在這樣的系統中,只有當一個線程調用了它的sleep()或yield()方法後纔會放棄所佔用的資源——也就是必須由該線程主動放棄所佔用的資源。
當發生以下狀況時,線程將會進入阻塞狀態
① 線程調用sleep()方法主動放棄所佔用的處理器資源
② 線程調用了一個阻塞式IO方法,在該方法返回以前,該線程被阻塞
③ 線程試圖得到一個同步監視器,但該同步監視器正被其餘線程所持有。
④ 線程在等待某個通知(notify)
⑤ 程序調用了線程的suspend()方法將該線程掛起。但這個方法容易致使死鎖,因此應該儘可能避免使用該方法
當前正在執行的線程被阻塞以後,其餘線程就能夠得到執行的機會。被阻塞的線程會在合適的時候從新進入就緒狀態,注意是就緒狀態而不是運行狀態。也就是說,被阻塞線程的阻塞解除後,必須從新等待線程調度器再次調度它。
針對上面幾種狀況,當發生以下特定的狀況時能夠解除上面的阻塞,讓該線程從新進入就緒狀態:
① 調用sleep()方法的線程通過了指定時間。
② 線程調用的阻塞式IO方法已經返回。
③ 線程成功地得到了試圖取得的同步監視器。
④ 線程正在等待某個通知時,其餘線程發出了個通知。
⑤ 處於掛起狀態的線程被調甩了resdme()恢復方法。
從上圖能夠看出,線程從阻塞狀態只能進入就緒狀態,沒法直接進入運行狀態。而就緒和運行狀態之間的轉換一般不受程序控制,而是由系統線程調度所決定。當處於就緒狀態的線程得到處理器資源時,該線程進入運行狀態;當處於運行狀態的線程失去處理器資源時,該線程進入就緒狀態。但有一個方法例外,調用yield()方法可讓運行狀態的線程轉入就緒狀態。關於yield()方法後面有更詳細的介紐。
線程會以以下3種方式結束,結束後就處於死亡狀態:
① run()或call()方法執行完成,線程正常結束。
② 線程拋出一個未捕獲的Exception或Error。
③ 直接調用該線程stop()方法來結束該線程——該方法容易致使死鎖,一般不推薦使用。
三、建立多線程
1. 經過繼承Thread類來建立並啓動多線程的方式
2. 經過實現Runnable接口來建立並啓動線程的方式
3. 經過實現Callable接口來建立並啓動線程的方式
java:
建立Thread子類的一個實例並重寫run方法,run方法會在調用start()方法以後被執行。
public class Demo extends Thread { int i = 0; public void run(){ for(;i< 10;i++){ System.out.println(getName() + " " + i); } } public static void main(String[] args) { for(int k = 0;k < 20;k++){ System.out.println(Thread.currentThread().getName() + " : " + k); if(k == 5){ new Demo().start(); } } } }
編寫線程執行代碼的方式是新建一個實現了java.lang.Runnable接口的類的實例,實例中的方法能夠被線程調用。
public class Demo implements Runnable{ public void run(){ for(int i = 0;i< 10;i++){ System.out.println(Thread.currentThread().getName() + " : " + i); } } public static void main(String[] args){ for(int k=0;k< 20;k++){ System.out.println(Thread.currentThread().getName() + " : " + k); if(k == 5){ Runnable oneRunnable = new Demo(); Thread oneThread = new Thread(oneRunnable); oneThread.start(); } } } }
public class Demo implements Callable<Integer>{ @Override public Integer call() throws Exception{ int i = 0; for(;i< 10;i++){ System.out.println(Thread.currentThread().getName() + " : " + i); } return i; } public static void main(String[] args){ Demo demo = new Demo(); FutureTask<Integer> ft = new FutureTask<>(demo); for(int k=0;k< 20;k++){ System.out.println(Thread.currentThread().getName() + " : " + k); if(k == 5){ Thread oneThread = new Thread(ft,"有返回值的線程"); oneThread.start(); } } try{ System.out.println("子線程的返回值:" + ft.get()); }catch(InterruptedException e){ e.printStackTrace(); }catch(ExecutionException e){ e.printStackTrace(); } } }
採用實現Runnable、Callable接口的方式創見多線程時,
優點是:線程類只是實現了Runnable接口或Callable接口,還能夠繼承其餘類。在這種方式下,多個線程能夠共享同一個target對象,因此很是適合多個相同線程來處理同一份資源的狀況,從而能夠將CPU、代碼和數據分開,造成清晰的模型,較好地體現了面向對象的思想。
劣勢是:編程稍微複雜,若是要訪問當前線程,則必須使用Thread.currentThread()方法。
使用繼承Thread類的方式建立多線程時優點是:編寫簡單,若是須要訪問當前線程,則無需使用Thread.currentThread()方法,直接使用this便可得到當前線程。
劣勢是:線程類已經繼承了Thread類,因此不能再繼承其餘父類。
4.一、併發
4.1.1 併發與並行
首先介紹一下併發與並行,二者雖然只有一字之差,但實際上卻有着本質的區別,其概念以下:
並行性(parallel):指在同一時刻,有多條指令在多個處理器上同時執行;
併發性(concurrency):指在同一時刻只能有一條指令執行,但多個進程指令被快速輪換執行,使得在宏觀上具備多個進程同時執行的效果。
併發的關鍵是你有處理多個任務的能力,不必定要同時;並行的關鍵是你有同時處理多個任務的能力;
4.1.2 順序編程與併發編程
在解決編程問題時,一般使用順序編程來解決,即程序中的全部事物在任意時刻都只能執行一個步驟。然而對於某些問題,但願可以並行地執行程序中的多個部分,來達到咱們想要的效果。在單處理器機器中,能夠將程序劃分爲多個部分,而後每一個部分由該處理器併發執行。在多處理器機器中,咱們能夠將程序劃分多個部分,而後每一個部分分別在多個處理器上並行執行。固然爲了更加充分利用CPU資源,咱們也能夠在多個處理器上併發執行,那麼在這咱們就涉及到了另外一種編程模式了併發編程。
併發編程又叫多線程編程。併發編程使咱們能夠將程序劃分爲多個分離的、獨立運行的任務。經過使用多線程機制,每一個獨立任務都將由線程來驅動。一個線程就是在進程中的一個單一的順序控制流,單個進程能夠擁有多個"併發執行"的任務。這樣使程序的每一個任務,都好像擁有一個本身的CPU同樣。但其底層機制仍是是切分CPU時間,CPU都有個時鐘頻率,表示每秒中能執行CPU指令的次數。在每一個時鐘週期內,CPU實際上只能去執行一條也有可能多條指令。操做系統將進程進行管理,輪流分配每一個進程很短的一段是時間但不必定是均分,而後在每一個進程內部,程序代碼本身處理該進程內部線程的時間分配,多個線程之間相互的切換去執行,這個切換時間也是很是短的因此一般咱們不須要考慮它。
併發是指"發",不是處理,最多見的狀況就是許多人在一小段時間內都點擊了你的網站,發出了處理請求。併發編程是對併發情況的應對,在單處理器和多處理器機器上均可對其進行應對,可這個處理方案和架構以及算法有關。CPU通常是分時的,會在極短的時間內不停地切換給不一樣的線程使用,不管多少併發都會處理下去,只是時間問題,如何提升處理效率就看採用的技術了。
4.1.3 併發編程的優點
併發編程能夠使咱們的程序執行速度獲得提升,例如,若是你有一臺多處理器的機器,那麼就能夠在這些處理器之間分佈多個任務,從而能夠極大地提升吞吐量。這是Web服務器的常見狀況,通常Web服務器是一個多處理器機器,將爲每一個請求分配到一個線程中,那麼就能夠將大量的用戶請求分佈到多個CPU上進行併發處理。
可是,併發一般是提升運行在單處理器上的程序的性能。雖然,在單處理器上運行的併發程序開銷確實應該比該程序的全部部分都順序執行的開銷大,由於其中增長了所謂上"下文切換"的代價,即從一個任務切換到另外一個任務。表面上看,將程序的全部部分看成單個的任務運行好像是開銷更小一點,而且能夠節省上下文切換的代價。可是咱們的程序並不會按咱們設想的那樣一直正常運行,它會發生阻塞。若是程序中的某個任務由於某些緣由發生了阻塞,那麼該任務將不能繼續執行。若是沒有併發,則整個程序都將中止下來,直至外部條件發生變化。可是,若是使用併發來編寫程序,那麼當一個任務阻塞時,程序中的其餘任務還能夠繼續執行,所以這個程序能夠保持繼續向前執行,這樣就提升程序的執行效率和運行性能。
併發須要付出代價,包含複雜性代價,可是這些代價與在程序設計、資源負載均衡以及用戶方便使用方面的改進相比,就顯得微不足道了。一般,線程使你可以建立更加鬆散耦合的設計則,你的代碼中各個部分都必須顯式地關注那些一般能夠由線程來處理的任務。
4.二、多任務、多進程、多線程
幾乎全部的操做系統都支持同時運行多個任務,一個任務一般就是一個程序,每一個運行中的程序就是一個進程。當一個程序運行時,內部可能包含了多個順序執行流,每一個順序執行流就是一個線程。
4.2.1 多進程
實現併發最直接的方式是在操做系統級別使用進程,進程是運行在它本身的地址空間內的自包容的程序。多任務操做系統能夠經過週期性地將CPU從一個進程切換到另外一個進程,來實現同時運行多個進程。 儘管對於一個CPU而言,它在某個時間點只能運行一個進程,但CPU能夠在多個進程之間進行輪換執行,而且CPU的切換速度極高,使咱們沒法感知其切換的過程,就好像有多個進程在同時執行。
幾乎全部的操做系統都支持進程的概念,全部運行中的任務一般對應一個進程(Process)。當一個程序進入內存運行時,即變成一個進程。進程是處於運行過程當中的程序,而且具備必定的獨立功能,進程是系統進行資源分配和調度的一個獨立單位。通常而言,進程包含以下3個特徵。
■ 獨立性:進程是系統中獨立存在的實體,它能夠擁有本身獨立的資源,每個進程都擁有本身私有的地址空間。在沒有通過進程自己容許的狀況下,一個用戶進程不能夠直接訪問其餘進程的地址空間。
■ 動態性:進程與程序的區別在於,程序只是一個靜態的指令集合,而進程是一個正在系統中活動的指令集合。在進程中加入了時間的概念,進程具備本身的生命週期和各類不一樣的狀態,這些概念在程序中部是不具有的。
■ 併發性:多個進程能夠在單個處理器上併發執行,多個進程之間不會互相影響。
現代的操做系統都支持多進程的併發,但在具體的實現細節上可能由於硬件和操做系統的不一樣而採用不一樣的策略。比較經常使用的方式有:共用式的多任務操做策略,例如Windows 3.1和Mac OS 9。目前操做系統大多采用效率更高的搶佔式多任務操做策略,例如 VVindows NT、Windows 2000以及UNIX/Linux等操做系統。但對進程的併發一般會有數量和開銷的限制,以免它們在不一樣的併發系統之間的可應用性。爲了應對該問題,因此在多進程的基礎上提出了多線程的概念,下面將詳細介紹。
4.2.2 多線程
4.2.2.1 多線程概述
多線程則擴展了多進程的概念。使得同一個進程中也能夠同時併發處理多個任務。線程(Thread)也被稱做輕量級進程(Lightweight Process)。線程是進程的執行單元,就像進程在操做系統中的地位同樣,線程在程序中是獨立的、併發的執行流。當進程被初始化後,主線程就被建立了。對於絕大多數的應用程序來講,一般僅要求有一個主線程,但也能夠在該進程內建立多條順序執行流,這些順序執行流就是線程,每一個線程也是互相獨立的。
線程是進程的組成部分,一個進程能夠擁有多個線程,一個線程必須有一個父進程。線程能夠擁有本身的堆棧、本身的程序計數器和本身的局部變量,但不擁有系統資源,它與父進程的其餘線程共享該進程所擁有的所有資源。由於多個線程共享父進程裏的所有資源,所以編程更加方便;但必須更加當心,咱們必須確保線程不會妨礙同一進程裏的其餘線程。
4.2.2.2 多線程機制
線程模型爲編程帶來了便利,它簡化了在單一程序中同時交織在一塊兒的多個操做的處理。在使用線程時,CPU將輪流給每一個任務分配其佔用時間。每一個任務都以爲本身在一直佔用CPU,但事實上CPU時間是劃分紅片斷分配給了全部的任務。線程的一大好處是能夠使你從這個層次抽身出來,即代碼沒必要知道它是運行在具備一個仍是多個CPU的機器上。因此,使用線程機制是一種創建透明的、可擴展的程序的方法,若是程序行得太慢,爲機器增添一個CPU就能很容易地加快程序的運行速度。多任務和多線程每每是使用多處理器系統的最合理方式。
4.2.2.3 多線程調度
線程能夠完成必定的任務,能夠與其餘線程共享父進程中的共享變量及部分環境,相互之間協同來完成進程所要完成的任務。線程是獨立運行的,它並不知道進程中是否還有其餘線程存在,線程的執行是搶佔式的,也就是說,當前運行的線程在任什麼時候候均可能被掛起,以便另一個線程能夠運行。
一個線程能夠建立和撤銷另外一個線程,同一個進程中的多個線程之間能夠併發執行。從邏輯角度來看,多線程存在於一個應用程序中,讓一個應用程序中能夠有多個執行部分同時執行,但操做系統無須將多個線程看做多個獨立的應用,對多線程實現調度和管理以及資源分配。線程的調度和管理由進程自己負責完成。
概括起採能夠這樣說:操做系統能夠同時執行多個任務,每一個任務就是進程;進程能夠同時執行多個任務,每一個任務就是線程。簡而言之,一個程序運行後至少有一個進程,一個進程裏能夠包含多個線程,但至少要包含一個線程。
4.2.2.4 多線程的優點
線程在程序中是獨立的、併發的執行流,與分隔的進程相比,進程中線程之間的隔離程度要小:
01. 它們共享內存、文件句柄和其餘每一個進程應有的狀態。由於線程的劃分尺度小於進程,使得多線程程序的併發性高。進程在執行過程當中擁有獨立的內存單元,而多個線程共享內存,從而極大地提升了程序的運行效率。
02. 線程比進程具備更高的性能,這是因爲同一個進程中的線程都有共性----多個線程共享同一個進程虛擬空間。線程共享的環境包括進程代碼段、進程的公有數據等。利用這些共享的數據,線程很容易實現相互之間的通訊。
03. 當操做系統建立一個進程時,必須爲該進程分配獨立的內存空間,並分配大量的相關資源;但建立一個線程則簡單得多,所以使用多線程來實現併發比使用多進程實現併發的性能要高得多。
總結起來,使用多線程編程具備以下幾個優勢:
01. 進程之間不能共享內存,但線程之間共享內存很是容易
02. 系統建立進程時須要爲該進程從新分配系統資源,但建立線程則代價小得多,所以使用多線程來實現多任務併發比多進程的效率高
03. Java語言內置了多線程功能支持,而不是單純地做爲底層操做系統的調度方式,從而簡化了Java的多線程編程
在實際應用中,多線程是很是有用的,一個瀏覽器必須能同時下載多個圖片;一個Web服務器必須能同時響應多個用戶請求;Java虛擬機自己就在後臺提供了一個超級線程來進行垃圾回收;圖形用戶界面(GUI)應用也須要啓動單獨的線程從主機環境收集用戶界面事件……總之,多線程在實際編程中的應用是很是普遍的。
五、多線程和異步操做:
多線程和異步操做二者均可以達到避免調用線程阻塞的目的,從而提升軟件的可響應性。甚至有些時候咱們就認爲多線程和異步操做是等同的概念。可是,多線程和異步操做仍是有一些區別的。而這些區別形成了使用多線程和異步操做的時機的區別。
異步操做的本質
全部的程序最終都會由計算機硬件來執行,因此爲了更好的理解異步操做的本質,咱們有必要了解一下它的硬件基礎。 熟悉電腦硬件的朋友確定對DMA這個詞不陌生,硬盤、光驅的技術規格中都有明確DMA的模式指標,其實網卡、聲卡、顯卡也是有DMA功能的。DMA就是直接內存訪問的意思,也就是說,擁有DMA功能的硬件在和內存進行數據交換的時候能夠不消耗CPU資源。只要CPU在發起數據傳輸時發送一個指令,硬件就開始本身和內存交換數據,在傳輸完成以後硬件會觸發一箇中斷來通知操做完成。這些無須消耗CPU時間的I/O操做正是異步操做的硬件基礎。因此即便在DOS這樣的單進程(並且無線程概念)系統中也一樣能夠發起異步的DMA操做。
異步操做的優缺點:
由於異步操做無須額外的線程負擔,而且使用回調的方式進行處理,在設計良好的狀況下,處理函數能夠沒必要使用共享變量(即便沒法徹底不用,最起碼能夠減小共享變量的數量),減小了死鎖的可能。固然異步操做也並不是完美無暇。編寫異步操做的複雜程度較高,程序主要使用回調方式進行處理,與普通人的思惟方式有些初入,並且難以調試。
線程的本質:線程不是一個計算機硬件的功能,而是操做系統提供的一種邏輯功能,線程本質上是進程中一段併發運行的代碼,因此線程須要操做系統投入CPU資源來運行和調度。
多線程的優缺點:多線程的優勢很明顯,線程中的處理程序依然是順序執行,符合普通人的思惟習慣,因此編程簡單。可是多線程的缺點也一樣明顯,線程的使用(濫用)會給系統帶來上下文切換的額外負擔。而且線程間的共享變量可能形成死鎖的出現。
適用範圍:
當須要執行I/O操做時,使用異步操做比使用線程+同步I/O操做更合適。I/O操做不只包括了直接的文件、網絡的讀寫,還包括數據庫操做、Web Service、HttpRequest以及.Net Remoting等跨進程的調用。
而線程的適用範圍則是那種須要長時間CPU運算的場合,例如耗時較長的圖形處理和算法執行。可是每每因爲使用線程編程的簡單和符合習慣,因此不少朋友每每會使用線程來執行耗時較長的I/O操做。這樣在只有少數幾個併發操做的時候還無傷大雅,若是須要處理大量的併發操做時就不合適了。
六、多線程提問:
一、在Java中Lock接口比synchronized塊的優點是什麼?你須要實現一個高效的緩存,它容許多個用戶讀,但只容許一個用戶寫,以此來保持它的完整性,你會怎樣去實現它?
lock接口在多線程和併發編程中最大的優點是它們爲讀和寫分別提供了鎖,它能知足你寫像ConcurrentHashMap這樣的高性能數據結構和有條件的阻塞。Java的讀寫鎖能夠實現上述請求。
通常用lock或者 readwritelock時,須要把unlock方法放在一個 fianlly 塊中,由於程序運行的時候可能會出現一些咱們人爲控制不了的因素,致使鎖一直沒釋放,那其餘線程就進不來了。
二、join實現:
join() method suspends the execution of the calling thread until the object called finishes its execution.
好比在線程B中調用了線程A的Join()方法,直到線程A執行完畢後,纔會繼續執行線程B;
三、在java中wait和sleep方法的不一樣?
最大的不一樣是在等待時wait會釋放鎖,而sleep一直持有鎖。Wait一般被用於線程間交互,sleep一般被用於暫停執行
阻塞隊列與普通隊列的區別在於,當隊列是空的時,從隊列中獲取元素的操做將會被阻塞,或者當隊列是滿時,往隊列裏添加元素的操做會被阻塞。試圖從空的阻塞隊列中獲取元素的線程將會被阻塞,直到其餘的線程往空的隊列插入新的元素。一樣,試圖往已滿的阻塞隊列中添加新元素的線程一樣也會被阻塞,直到其餘的線程使隊列從新變得空閒起來。
爲何要使用生產者和消費者模式:
在線程世界裏,生產者就是生產數據的線程,消費者就是消費數據的線程。在多線程開發當中,若是生產者處理速度很快,而消費者處理速度很慢,那麼生產者就必須等待消費者處理完,才能繼續生產數據。一樣的道理,若是消費者的處理能力大於生產者,那麼消費者就必須等待生產者。爲了解決這種生產消費能力不均衡的問題,因此便有了生產者和消費者模式。
什麼是生產者消費者模式:
生產者消費者模式是經過一個容器來解決生產者和消費者的強耦合問題。生產者和消費者彼此之間不直接通信,而經過阻塞隊列來進行通信,因此生產者生產完數據以後不用等待消費者處理,直接扔給阻塞隊列,消費者不找生產者要數據,而是直接從阻塞隊列裏取,阻塞隊列就至關於一個緩衝區,平衡了生產者和消費者的處理能力。
這個阻塞隊列就是用來給生產者和消費者解耦的。
四、什麼是原子操做,Java中的原子操做是什麼?
原子操做是指不會被線程調度機制打斷的操做;這種操做一旦開始,就一直運行到結束,中間不會有任何 context switch (切換到另外一個線程)
JDK1.5的原子包:java.util.concurrent.atomic
這個包裏面提供了一組原子類。其基本的特性就是在多線程環境下,當有多個線程同時執行這些類的實例包含的方法時,具備排他性,即當某個線程進入方法,執行其中的指令時,不會被其餘線程打斷,而別的線程就像自旋鎖同樣,一直等到該方法執行完成,才由JVM從等待隊列中選擇一個另外一個線程進入,這只是一種邏輯上的理解。其實是藉助硬件的相關指令來實現的,不會阻塞線程(synchronized 會把別的等待的線程掛起)(或者說只是在硬件級別上阻塞了)。
五、什麼是競爭條件?你怎樣發現和解決競爭?
多個線程或者進程在讀寫一個共享數據時結果依賴於它們執行的相對時間,這種情形叫作競爭。競爭條件發生在當多個進程或者線程在讀寫數據時,其最終的的結果依賴於多個進程的指令執行順序。
當因爲事件次序異常而形成對同一資源的競爭,從而致使程序沒法正常運行時,就會出現「競爭條件」。
競爭條件的典型解決方案是,確保程序在使用某個資源(好比文件、設備、對象或者變量) 時,擁有本身的專有權。得到某個資源的專有權的過程稱爲加鎖。鎖不太容易處理。死鎖(「抱死,deadly embrace」)是常見的問題,在這種情形下,程序會因等待對方釋放被加鎖的資源而沒法繼續運行。 要求全部線程都必須按照相同的順序(好比,按字母排序,或者從「largest grain」到「smallest grain」的順序) 得到鎖,這樣能夠避免大部分死鎖。另外一個常見問題是活鎖(livelock),在這種狀況下,程序至少 成功地得到和釋放了一個鎖,可是以這種方式沒法將程序再繼續運行下去。若是一個鎖被掛起,順利地釋放它會很難。簡言之,編譯在任何狀況下均可以按須要正確地加鎖和釋放的程序一般很困難。
有時,能夠一次執行一個單獨操做來完成一些特殊的操做,從而使您不須要顯式地對某個資源 進行加鎖然後再解鎖。這類操做稱爲「原子」操做,只要可以使用這類操做,它們一般是最好的解決方案。
六、你將如何使用thread dump?你將如何分析Thread dump?
在故障定位(尤爲是out of memory)和性能分析的時候,常常會用到一些文件來幫助咱們排除代碼問題。這些文件記錄了JVM運行期間的內存佔用、線程執行等狀況,這就是咱們常說的dump文件。經常使用的有heap dump和thread dump(也叫javacore,或java dump)。咱們能夠這麼理解:heap dump記錄內存信息的,thread dump是記錄CPU信息的。
heap dump:
heap dump文件是一個二進制文件,它保存了某一時刻JVM堆中對象使用狀況。HeapDump文件是指定時刻的Java堆棧的快照,是一種鏡像文件。Heap Analyzer工具經過分析HeapDump文件,哪些對象佔用了太多的堆棧空間,來發現致使內存泄露或者可能引發內存泄露的對象。
thread dump:
thread dump文件主要保存的是java應用中各線程在某一時刻的運行的位置,即執行到哪個類的哪個方法哪個行上。thread dump是一個文本文件,打開後能夠看到每個線程的執行棧,以stacktrace的方式顯示。經過對thread dump的分析能夠獲得應用是否「卡」在某一點上,即在某一點運行的時間太長,如數據庫查詢,長期得不到響應,最終致使系統崩潰。單個的thread dump文件通常來講是沒有什麼用處的,由於它只是記錄了某一個絕對時間點的狀況。比較有用的是,線程在一個時間段內的執行狀況。
兩個thread dump文件在分析時特別有效,困爲它能夠看出在前後兩個時間點上,線程執行的位置,若是發現前後兩組數據中同一線程都執行在同一位置,則說明此處可能有問題,由於程序運行是極快的,若是兩次均在某一點上,說明這一點的耗時是很大的。經過對這兩個文件進行分析,查出緣由,進而解決問題。
http://blog.csdn.net/rachel_luo/article/details/8920596
https://www.cnblogs.com/zhengyun_ustc/archive/2013/01/06/dumpanalysis.html
七、爲何咱們調用start()方法時會執行run()方法,爲何咱們不能直接調用run()方法?
當你調用start()方法時你將建立新的線程,而且執行在run()方法裏的代碼。可是若是你直接調用run()方法,它不會建立新的線程也不會執行調用線程的代碼。
start()方法來啓動線程,真正實現了多線程運行,這時無需等待run方法體代碼執行完畢而直接繼續執行下面的代碼:
經過調用Thread類的start()方法來啓動一個線程,這時此線程是處於就緒狀態,並無運行。而後經過此Thread類調用方法run()來完成其運行操做的,這裏方法run()稱爲線程體,它包含了要執行的這個線程的內容,Run方法運行結束,此線程終止,而CPU再運行其它線程。
run()方法看成普通方法的方式調用,程序仍是要順序執行,仍是要等待run方法體執行完畢後纔可繼續執行下面的代碼:
而若是直接用Run方法,這只是調用一個方法而已,程序中依然只有主線程--這一個線程,其程序執行路徑仍是隻有一條,這樣就沒有達到寫線程的目的。
Thread類中run()和start()方法的區別以下:
run()方法:在本線程內調用該Runnable對象的run()方法,能夠重複屢次調用;
start()方法:啓動一個線程,調用該Runnable對象的run()方法,不能屢次啓動一個線程;
八、Java中你怎樣喚醒一個阻塞的線程?
1. sleep() 方法:sleep() 容許 指定以毫秒爲單位的一段時間做爲參數,它使得線程在指定的時間內進入阻塞狀態,不能獲得CPU 時間,指定的時間一過,線程從新進入可執行狀態。
典型地,sleep() 被用在等待某個資源就緒的情形:測試發現條件不知足後,讓線程阻塞一段時間後從新測試,直到條件知足爲止。不會釋放佔用鎖。
2. suspend() 和 resume() 方法:兩個方法配套使用,suspend()使得線程進入阻塞狀態,而且不會自動恢復,必須其對應的resume() 被調用,才能使得線程從新進入可執行狀態。典型地,suspend() 和 resume() 被用在等待另外一個線程產生的結果的情形:測試發現結果尚未產生後,讓線程阻塞,另外一個線程產生告終果後,調用 resume() 使其恢復。
3. yield() 方法:yield() 使得線程放棄當前分得的 CPU 時間,可是不使線程阻塞,即線程仍處於可執行狀態,隨時可能再次分得 CPU 時間。調用 yield() 的效果等價於調度程序認爲該線程已執行了足夠的時間從而轉到另外一個線程。
4. wait() 和 notify() 方法:兩個方法配套使用,wait() 使得線程進入阻塞狀態,它有兩種形式,一種容許指定以毫秒爲單位的一段時間做爲參數,另外一種沒有參數,前者當對應的 notify() 被調用或者超出指定時間時線程從新進入可執行狀態,後者則必須對應的 notify() 被調用。
初看起來它們與 suspend() 和 resume() 方法對沒有什麼分別,可是事實上它們是大相徑庭的。區別的核心在於,前面敘述的全部方法,阻塞時都不會釋放佔用的鎖(若是佔用了的話),而這一對方法則相反。
上述的核心區別致使了一系列的細節上的區別。
首先,前面敘述的全部方法都隸屬於 Thread 類,可是這一對卻直接隸屬於 Object 類,也就是說,全部對象都擁有這一對方法。初看起來這十分難以想象,可是實際上倒是很天然的,由於這一對方法阻塞時要釋放佔用的鎖,而鎖是任何對象都具備的,調用任意對象的 wait() 方法致使線程阻塞,而且該對象上的鎖被釋放。而調用 任意對象的notify()方法則致使因調用該對象的 wait() 方法而阻塞的線程中隨機選擇的一個解除阻塞(但要等到得到鎖後才真正可執行)。
其次,前面敘述的全部方法均可在任何位置調用,可是這一對方法卻必須在 synchronized 方法或塊中調用,理由也很簡單,只有在synchronized 方法或塊中當前線程才佔有鎖,纔有鎖能夠釋放。一樣的道理,調用這一對方法的對象上的鎖必須爲當前線程所擁有,這樣纔有鎖能夠釋放。所以,這一對方法調用必須放置在這樣的 synchronized 方法或塊中,該方法或塊的上鎖對象就是調用這一對方法的對象。若不知足這一條件,則程序雖然仍能編譯,但在運行時會出現 IllegalMonitorStateException 異常。
wait() 和 notify() 方法的上述特性決定了它們常常和synchronized 方法或塊一塊兒使用,將它們和操做系統的進程間通訊機制做一個比較就會發現它們的類似性:synchronized方法或塊提供了相似於操做系統原語的功能,它們的執行不會受到多線程機制的干擾,而這一對方法則至關於 block 和wakeup 原語(這一對方法均聲明爲 synchronized)。它們的結合使得咱們能夠實現操做系統上一系列精妙的進程間通訊的算法(如信號量算法),並用於解決各類複雜的線程間通訊問題。
關於 wait() 和 notify() 方法最後再說明兩點:
第一:調用 notify() 方法致使解除阻塞的線程是從因調用該對象的 wait() 方法而阻塞的線程中隨機選取的,咱們沒法預料哪個線程將會被選擇,因此編程時要特別當心,避免因這種不肯定性而產生問題。
第二:除了 notify(),還有一個方法 notifyAll() 也可起到相似做用,惟一的區別在於,調用 notifyAll() 方法將把因調用該對象的 wait() 方法而阻塞的全部線程一次性所有解除阻塞。固然,只有得到鎖的那一個線程才能進入可執行狀態。
談到阻塞,就不能不談一談死鎖,略一分析就能發現,suspend() 方法和不指定超時期限的 wait() 方法的調用均可能產生死鎖。遺憾的是,Java 並不在語言級別上支持死鎖的避免,咱們在編程中必須當心地避免死鎖。
九、在Java中CycliBarriar和CountdownLatch有什麼區別?
CyclicBarrier和CountdownLatch是java 1.5中提供的一些很是有用的輔助類來幫助咱們進行併發編程。這兩個的區別是CyclicBarrier能夠重複使用已經經過的障礙,而CountdownLatch不能重複使用。
CountdownLatch:
一個線程(或者多個),等待另外N個線程完成某個事情以後才能執行。是併發包中提供的一個可用於控制多個線程同時開始某個動做的類,其採用的方法爲減小計數的方式,當計數減至零時位於latch.Await()後的代碼纔會被執行,CountDownLatch是減計數方式,計數==0時釋放全部等待的線程;CountDownLatch當計數到0時,計數沒法被重置;
CyclicBarrier:
字面意思迴環柵欄,經過它能夠實現讓一組線程等待至某個狀態以後再所有同時執行。叫作迴環是由於當全部等待線程都被釋放之後,CyclicBarrier能夠被重用。 即:N個線程相互等待,任何一個線程完成以前,全部的線程都必須等待。CyclicBarrier是當await的數量到達了設置的數量的時候,纔會繼續往下面執行,CyclicBarrier計數達到指定值時,計數置爲0從新開始。
對於CountDownLatch來講,重點是那個「一個線程」,是它在等待,而另外那N的線程在把「某個事情」作完以後能夠繼續等待,能夠終止。而對於CyclicBarrier來講,重點是那N個線程,他們之間任何一個沒有完成,全部的線程都必須等待。
十、什麼是不可變對象,它對寫併發應用有什麼幫助?
不可變對象是指一個對象的狀態在對象被建立以後就再也不變化。不可變對象對於緩存是很是好的選擇,由於你不須要擔憂它的值會被更改。
建立一個不可變類:
將類聲明爲final,因此它不能被繼承;
將全部的成員聲明爲私有的,這樣就不容許直接訪問這些成員;
對變量不要提供setter方法;
將全部可變的成員聲明爲final,這樣只能對它們賦值一次;
經過構造器初始化全部成員,進行深拷貝(deep copy);
在getter方法中,不要直接返回對象自己,而是克隆對象,並返回對象的拷貝;
在Java中, String類是不可變的。那麼到底什麼是不可變的對象呢? 能夠這樣認爲:若是一個對象,在它建立完成以後,不能再改變它的狀態,那麼這個對象就是不可變的。不能改變狀態的意思是,不能改變對象內的成員變量,包括基本數據類型的值不能改變,引用類型的變量不能指向其餘的對象,引用類型指向的對象的狀態也不能改變。
區分對象和對象的引用
對於Java初學者, 對於String是不可變對象老是存有疑惑。看下面代碼:
String s = "ABCabc";
System.out.println("s = " + s);
s = "123456";
System.out.println("s = " + s);
打印結果爲:
s = ABCabc
s = 123456
首先建立一個String對象s,而後讓s的值爲「ABCabc」, 而後又讓s的值爲「123456」。 從打印結果能夠看出,s的值確實改變了。那麼怎麼還說String對象是不可變的呢? 其實這裏存在一個誤區: s只是一個String對象的引用,並非對象自己。對象在內存中是一塊內存區,成員變量越多,這塊內存區佔的空間越大。引用只是一個4字節的數據,裏面存放了它所指向的對象的地址,經過這個地址能夠訪問對象。
也就是說,s只是一個引用,它指向了一個具體的對象,當s=「123456」; 這句代碼執行過以後,又建立了一個新的對象「123456」, 而引用s從新指向了這個心的對象,原來的對象「ABCabc」還在內存中存在,並無改變。
String類不可變性的好處
String是全部語言中最經常使用的一個類。咱們知道在Java中,String是不可變的、final的。Java在運行時也保存了一個字符串池(String pool),這使得String成爲了一個特別的類。
String類不可變性的好處
1.只有當字符串是不可變的,字符串池纔有可能實現。字符串池的實現能夠在運行時節約不少heap空間,由於不一樣的字符串變量都指向池中的同一個字符串。但若是字符串是可變的,那麼String interning將不能實現(譯者注:String interning是指對不一樣的字符串僅僅只保存一個,即不會保存多個相同的字符串。),由於這樣的話,若是變量改變了它的值,那麼其它指向這個值的變量的值也會一塊兒改變。
2.若是字符串是可變的,那麼會引發很嚴重的安全問題。譬如,數據庫的用戶名、密碼都是以字符串的形式傳入來得到數據庫的鏈接,或者在socket編程中,主機名和端口都是以字符串的形式傳入。由於字符串是不可變的,因此它的值是不可改變的,不然黑客們能夠鑽到空子,改變字符串指向的對象的值,形成安全漏洞。
3.由於字符串是不可變的,因此是多線程安全的,同一個字符串實例能夠被多個線程共享。這樣便不用由於線程安全問題而使用同步。字符串本身即是線程安全的。
4.類加載器要用到字符串,不可變性提供了安全性,以便正確的類被加載。譬如你想加載java.sql.Connection類,而這個值被改爲了myhacked.Connection,那麼會對你的數據庫形成不可知的破壞。
5.由於字符串是不可變的,因此在它建立的時候hashcode就被緩存了,不須要從新計算。這就使得字符串很適合做爲Map中的鍵,字符串的處理速度要快過其它的鍵對象。這就是HashMap中的鍵每每都使用字符串。
引用: