一、線程與進程
線程:一條線程指的是進程中一個單一順序的控制流,一個進程中可以並發多個線程,每條線程並行執行不同的任務。多線程是多任務的一種特別形式,但多線程使用了更小的資源開銷。
進程:一個進程包括有操作系統分配的內存空間,包含一個或多個線程。一個線程不能獨立的存在。它必須是進程的一部分。一個進程一直運行直到所有的非守護線程都結束運行后才能結束。
進程 | 線程 | |
定義 | 進程是指處於運行中的程序,並且具有一定的獨立功能。進程是系統進行資源分配和調度的一個單位。 當程序進入內存時,即為進程。 |
線程是進程的組成部分,一個進程可以擁有多個線程,而一個線程必須擁有一個父進程。 線程可以擁有自己的堆棧,自己的程序計數器和局部變量,但不能擁有系統資源。 它與父進程的其他線程共享該進城的所有資源。 |
特點 | 1)獨立性:進程是系統中獨立存在的實體,它可以獨立擁有資源,每一個進程都有自己獨立的地址空間, 沒有進程本身的運行,用戶進程不可以直接訪問其他進程的地址空間。 2)動態性:進程和程序的區別在於進程是動態的,進程中有時間的概念,進程具有自己的生命周期和各種 不同的狀態。 3)並發性:多個進程可以在單個處理器上並發執行,互不影響。 |
1)線程可以完成一定任務,可以和其他線程共享父進程的共享變量和部分環境,相互協作來完成任務。 2)線程是獨立運行的,其不知道進程中是否還有其他線程存在。 3)線程的執行是搶占式的,也就是說,當前執行的線程隨時可能被掛起,一邊運行另一個線程。 4)一個線程可以創建或撤銷另一個線程,一個進程中的多個線程可以並發執行。 |
二、線程的生命周期
新建狀態(New)
使用new關鍵字和Thread類或七子類建立一個線程對象后,該線程對象就處於新建狀態。此時僅由JVM為其分配內存,並初始化其成員變量的值。它保持這個狀態知道程序start()這個線程。
就緒狀態(runnable)
當線程對象調用了start()方法之后,該線程就進入就緒狀態。就緒狀態的線程處於就緒隊列中,Java虛擬機會為其創建方法調用棧和程序計數器,等待JVM里線程調度器的調度。
運行狀態(running)
如果就緒狀態的線程獲取CPU資源,就可以執行run方法,此時線程便處於運行狀態。處於運行狀態的線程最為復雜,它可以變為阻塞狀態、就緒狀態和死亡狀態。
阻塞狀態(blocked)
線程因為某種原因放棄了CPU使用權,暫時停止運行。直到線程進入可運行狀態,才有機會再次獲得CPU timeslice 轉到運行狀態。
如果一個線程執行了sleep(睡眠),suspend(掛起)等方法,失去所占用資源之后,該線程就從運行狀態進入阻塞狀態。在睡眠時間已到或獲得設備資源后可以重新進入就緒狀態。可以分為三種:
1)等待阻塞:運行狀態中的線程執行wait方法,使線程進入到等待阻塞狀態;
2)同步阻塞:線程在獲取synchronized同步鎖失敗(因為同步鎖被其他線程占用);
3)其他阻塞:通過調用線程的sleep()或join()發出了IO請求時,線程就會進入到阻塞狀態。當sleep()狀態超時,join()等待線程終止或超時,或IO請求處理完畢,線程重新轉入就緒狀態。
死亡狀態(dead)
一個運行狀態的線程完成任務或其他終止條件發生時,該線程就切換到終止狀態。
正常結束:run()或call()方法執行完成,線程正常結束。
異常結束:線程拋出一個未捕獲的Exception或Error。
調用stop:直接調用該線程的stop()方法結束該線程(該方法容易導致死鎖,不推薦使用)。
為了確定線程在當前是否存活着(就是要么是可運行的,要么是被阻塞了),需要使用isAlive方法。如果是可運行或被阻塞,這個方法返回true;如果線程仍舊是new狀態且不是可運行的,或線程死亡了,則返回false。
三、線程優先級
每個線程都有一個優先級,方便操作系統確定線程的調度順序。Java線程的優先級是一個整數,其取值范圍是1(Thread.MIN_PRIORITY)-10(Thread.MAX_PRIORITY)。
默認情況下,每一個線程都會分配一個優先級 NORM_PRIORITY(5)。具有較高優先級的線程在低優先級的線程之前分配處理器資源。但線程優先級不能保證線程執行的順序,而且非常依賴於平台。
四、創建線程的方式
1)繼承Thread類:Thread類本質上是實現了Runnable接口的一個實例。啟動線程的方法就是通過Thread類的start()方法。它是一個native方法,它將啟動一個新線程,並執行其中的run()方法。
A. 定義Thread類的子類,並重寫該類的run方法,該run方法的方法體就代表了線程要完成的任務。因此把run()方法稱為方法體。
B. 創建Thread子類的實例,即創建了線程對象。
C. 調用線程對象的start()方法來啟動該線程。
1 public class MyThread extends Thread { 2 public void run() { 3 System.out.println("MyThread.run()"); 4 } 5 } 6 MyThread myThread1 = new MyThread(); 7 myThread1.start();
2)實現Runnable接口
A. 定義Runnable接口的實現類,並重寫該接口的run()方法,該run()方法的方法體同樣是該線程的線程執行體。
B. 創建Runnable實現類的實例,並依此實例作為Thread的target來創建Thread對象,該Thread對象才是真正的線程對象。
C. 調用線程對象的start()方法來啟動該線程。
1 public class MyThread extends OtherClass implements Runnable { 2 public void run() { 3 System.out.println("MyThread.run()"); 4 } 5 } 6 //啟動 MyThread,需要首先實例化一個 Thread,並傳入自己的 MyThread 實例:
7 MyThread myThread = new MyThread(); 8 Thread thread = new Thread(myThread); 9 thread.start(); 10 target.run(); //當傳入一個 Runnable target 參數給 Thread 后,Thread 的 run()方法就會調用
11 public void run() { 12 if (target != null) { 13 target.run(); 14 } 15 }
3)通過Callable和Future創建線程
i. 創建Callable接口的實現類,並實現call()方法,該call()方法將作為線程執行體,並且有返回值。
ii. 創建Callable實現類的實例,使用FutureTask類包裝Callable對象,該FutureTask對象封裝了Callable對象的call()方法的返回值。
iii. 使用FutureTask對象作為Thread對象的target創建並啟動新線程。
iv. 調用FutureTask對象的get()方法來獲得子線程執行結束后的返回值。
1 //創建一個線程池
2 ExecutorService pool = Executors.newFixedThreadPool(taskSize); 3 // 創建多個有返回值的任務
4 List<Future> list = new ArrayList<Future>(); 5 for (int i = 0; i < taskSize; i++) { 6 Callable c = new MyCallable(i + " "); 7 // 執行任務並獲取 Future 對象
8 Future f = pool.submit(c); 9 list.add(f); 10 } 11 // 關閉線程池
12 pool.shutdown(); 13 // 獲取所有並發任務的運行結果
14 for (Future f : list) { 15 // 從 Future 對象上獲取任務的返回值,並輸出到控制台
16 System.out.println("res:" + f.get().toString()); 17 }
4)基於線程池的方式
利用線程池不用new就可以創建線程,線程可復用,利用Executors創建線程池。
1 ExecutorService threadPool = Executors.newFixedThreadPool(10); // 創建線程池 2 while(true) { 3 threadPool.execute(new Runnable() { // 提交多個線程任務,並執行 4 public void run() { 5 System.out.println(Thread.currentThread().getName() + " is running .."); 6 try { 7 Thread.sleep(3000); 8 } catch (InterruptedException e) { 9 e.printStackTrace(); 10 } 11 } 12 }); 13 }
五、終止線程的方式
1、正常運行結束:程序運行結束,線程自動結束。
2、使用退出標志退出線程
一般run()方法執行完,線程就會正常結束,然而,常常有些線程是伺服線程。它們需要長時間的運行,只有在外部某些條件滿足的情況下,才能關閉這些線程。使用一個變量來控制循環,例如:最直接的方法就是設一個boolean類型的標志,並通過設置這個標志為true或false來控制while循環是否退出,代碼示例:
1 public class ThreadSafe extends Thread { 2 public volatile boolean exit = false; 3 public void run() { 4 while (!exit){ 5 //do something
6 } 7 } 8 }
定義了一個退出標志exit,當exit為true時,while循環退出,exit的默認值為false。在定義exit時,使用了一個Java關鍵字volatile,這個關鍵字的目的是使exit同步,也就是說在同一時刻只能由一個線程來修改exit值。
3、Interrupt方法結束線程
使用interrupt()方法來中斷線程有兩種情況:
1)線程處於阻塞狀態:如使用了sleep,同步鎖的wait、socket中的receiver、accept等方法時,會使線程處於阻塞狀態。當調用線程的interrupt()方法時,會拋出InterruptException異常。阻塞中的那個方法拋出這個異常,通過代碼捕獲該異常,然后break跳出循環狀態,從而讓我們有機會結束這個線程的執行。通常很多人認為只要調用interrupt方法線程就會結束,實際上是錯的,一定要先捕獲InterruptedException異常之后通過break來跳出循環,才能正常結束run方法。
2)線程未處於阻塞狀態:使用isInterrupted()方法判斷線程的中斷標志來退出循環。當使用interrupt方法時,中斷標志就會置true,和使用自定義的標志來控制循環是一樣的道理。
1 public class ThreadSafe extends Thread { 2 public void run() { 3 while (!isInterrupted()){ //非阻塞過程中通過判斷中斷標志來退出
4 try{ 5 Thread.sleep(5*1000);//阻塞過程捕獲中斷異常來退出
6 }catch(InterruptedException e){ 7 e.printStackTrace(); 8 break;//捕獲到異常之后,執行 break 跳出循環
9 } 10 } 11 } 12 }
4、Stop方法終止線程(線程不安全)
程序中可以直接使用thread.stop()來強行終止線程,但是stop方法很危險,就像突然關閉計算機電源,而不是按正常程序關機一樣,可能會產生不可預料的結果,不安全主要是:
thread.stop()調用后,創建子線程的線程就會拋出ThreadDeatherror的錯誤,並且會釋放子線程所持有的所有鎖。一般任何進行加鎖的代碼塊,都是為了保護數據的一致性,如果在調用thread.stop()后導致了該線程所持有的所有鎖被突然釋放(不可控制),那么被保護數據就有可能呈現不一致性,其他線程在使用這些被破壞的數據時,有可能導致一些很奇怪的應用程序錯誤。因此,並不推薦使用stop方法來終止線程。
六、線程同步的方式
臨界區:通過對多線程的串行化來訪問公共資源或一段代碼,速度快,適應控制數據訪問。
互斥量:采用互斥對象機制,只有擁有互斥對象的線程才有訪問公共資源的權限,因為互斥對象只有一個,所以可以保證公共資源不會同時被多個線程訪問。
信號量:它允許多個線程統一時刻訪問同一資源,但是需要限制同一時刻訪問此資源的最大線程數目。信號量對象對線程的同步方式與前面幾種方法不同,信號允許多個線程同時使用共享資源,這與操作系統中PV操作相似。
事件(信號):通過通知操作的方式來保持對線程的同步,還可以方便的實現多線程的優先級比較的操作。
七、進程同步與互斥的區別
互斥:是指某一資源同時只允許一個訪問者對其進行訪問,具有唯一性和排他性。但互斥無法限制訪問者對資源的訪問順序,即訪問是無序的。
同步:是指在互斥的基礎上(大多數情況),通過其他機制實現訪問者對資源的有序訪問。在大多數情況下,同步已經實現了互斥,特別是所有寫入資源的情況必定是互斥的。少數情況是指可以允許多個訪問者同時訪問資源。
同步體現的是一種協作性,護持體現的是一種排他性。
八、Java后台線程(守護線程)
守護線程(Daemon):也稱服務線程,是后台線程。為用戶線程提供公共服務,在沒有用戶線程可服務是會自動離開。
1、優先級較低,用於為系統中的其他對象和線程提供服務。
2、設置:通過setDaemon(true)來設置線程為“守護線程”。
3、垃圾回收線程就是守護線程,它始終在低級別的狀態運行,用於監視和管理系統中的可回收資源。
4、生命周期:守護線程不依賴於終端但依賴於系統,與系統“同生共死”。
守護線程和用戶線程有什么區別?
當我們在Java程序中創建一個線程,它就被稱為用戶線程。將一個用戶線程設置為守護線程的方法就是在調用start()方法之前,調用對象得setDamon(true)方法。一個守護線程是在后台執行並且不會阻止JVM種植的線程,守護線程的作用是為其他線程的運行提供便利服務。當沒有用戶線程在運行的時候,JVM關閉程序並且退出。一個守護線程創建的子線程依然是守護線程。
守護線程的一個典型例子就是垃圾回收器。
九、如何在兩個線程之間共享數據?
Java里面進行多線程痛心的主要方式就是共享內存的方式,共享內存主要的關注點有兩個:可見性和有序性。Java內存模型(JMM)解決了可見性和有序性的問題,而鎖解決了原子性的問題,理想情況下我們希望做到“同步”和“互斥”。有以下常規實現方法:
1、將數據抽象成一個類,並將數據的操作作為這個類的方法,這么設計可以很容易做到同步,只要在方法上加“synchronized”
1 public class MyData { 2 private int j=0; 3 public synchronized void add(){ 4 j++; 5 System.out.println("線程"+Thread.currentThread().getName()+"j 為:"+j); 6 } 7 public synchronized void dec(){ 8 j--; 9 System.out.println("線程"+Thread.currentThread().getName()+"j 為:"+j); 10 } 11 public int getData(){ 12 return j; 13 } 14 } 15 public class AddRunnable implements Runnable{ 16 MyData data; 17 public AddRunnable(MyData data){ 18 this.data= data; 19 } 20 public void run() { 21 data.add(); 22 } 23 } 24 public class DecRunnable implements Runnable { 25 MyData data; 26 public DecRunnable(MyData data){ 27 this.data = data; 28 } 29 public void run() { 30 data.dec(); 31 } 32 } 33 public static void main(String[] args) { 34 MyData data = new MyData(); 35 Runnable add = new AddRunnable(data); 36 Runnable dec = new DecRunnable(data); 37 for(int i=0;i<2;i++){ 38 new Thread(add).start(); 39 new Thread(dec).start(); 40 } 41 }
2、將Runnable對象作為一個類的內部類,共享數據作為這個類的成員變量,每個線程對共享數據的操作方法也封裝在外部類,以便實現對數據的各個操作的同步和互斥,作為內部類的各個Runnable對象調用外部類的這些方法。
1 public class MyData { 2 private int j=0; 3 public synchronized void add(){ 4 j++; 5 System.out.println("線程"+Thread.currentThread().getName()+"j 為:"+j); 6 } 7 public synchronized void dec(){ 8 j--; 9 System.out.println("線程"+Thread.currentThread().getName()+"j 為:"+j); 10 } 11 public int getData(){ 12 return j; 13 } 14 } 15 public class TestThread { 16 public static void main(String[] args) { 17 final MyData data = new MyData(); 18 for(int i=0;i<2;i++){ 19 new Thread(new Runnable(){ 20 public void run() { 21 data.add(); 22 } 23 }).start(); 24 new Thread(new Runnable(){ 25 public void run() { 26 data.dec(); 27 } 28 }).start(); 29 } 30 } 31 }
十、多線程中的常見問題?
1、Java創建線程之后,直接調用run()方法和start()方法的區別?
start()方法來啟動線程,並在新線程中運行run()方法,真正實現了多線程運行。這時無需等待run方法體代碼執行完畢,可以直接繼續執行下面的代碼;通過調用Thread類的start方法來啟動一個線程,這時此線程是處於就緒狀態,並沒有運行,然后通過此Thread類調用run()方法來完成其運行操作,這里方法run稱為線程體,它包含了要執行的這個線程的內容,線程就進入了運行狀態,開始運行run函數中的代碼。run() 方法運行結束,此線程終止,然后CPU再調度其他線程。
只有調用了start方法,才會表現出多線程的特性,不同線程的run方法里面的代碼交替執行。如果直接調用run()方法的話,會把run()方法當作普通方法來調用,會在當前線程中執行run()方法,而不會啟動新線程來運行run()方法。程序還是要順序執行,必須等待一個線程的run方法里面的代碼全部執行完畢之后,另外一個線程才可以執行其run方法里面的代碼。
2、Java中 Runnable 接口和 Callable 接口的區別?
Runnable接口中的run()方法的返回值為void(即不能有返回值),它做的事情只是純粹地去執行run()方法中的代碼而已。Callable接口中的call()方法是有返回值的,是一個泛型,和Future、FutureTask配合可以用來獲取異步執行的結果。
Callable的call方法可拋出異常,而Runnable的run方法不能拋出異常。
Callable+Future/FutureTask可以獲取多線程運行的結果,可以在等待時間太長沒獲取需要的數據的情況下取消該線程的任務。
3、Sleep()方法和Wait()方法的區別?
sleep方法和wait方法都可以用來放棄CPU一定的時間,不同點在於如果線程持有某個對象的監視器(監視對象同步),sleep方法不會放棄這個對象的監視器,且可以在任何地方使用; wait方法會放棄這個對象的監視器,並且wait只能在同步控制方法或者同步控制塊中使用。
sleep方法屬於Thread類,是靜態方法;sleep方法導致了程序暫停執行指定的時間,讓出CPU給其他線程,但是它的監控狀態依然保持着,當指定的時間到了又會自動恢復運行狀態。在調用sleep方法過程中,線程不會釋放對象鎖。
wait方法屬於Object類,和notify()或notifyAll()方法配套使用,來實現線程間通信。當調用方法時,線程會放棄對象鎖,進入等待此對象的等待鎖定池,只有針對對象調用notify方法后本線程才進入對象鎖定池,准備獲取對象鎖進入運行狀態。
特別注意:sleep 和 wait 必須捕獲異常(Thread.sleep()和Object.wait()都會拋出InterruptedException),notify和notifyAll不需要捕獲異常。
4、線程讓步(yield)
yield會使當前線程讓出CPU執行時間片,與其他線程一起重新競爭CPU時間片。一般情況下,優先級高的線程有更大的可能性成功競爭得到CPU時間片,但這又不是絕對的,有的操作系統對線程優先級並不敏感。
5、join 等待其他線程終止
join方法,等待其他線程終止,在當前線程中調用一個線程的join方法,則當前線程轉為阻塞狀態,當另一個線程結束,當前線程再由阻塞狀態變為就緒狀態,等待CPU。
為什么要用join方法?
很多情況下,主線程生成並啟動子線程,需要用到子線程返回結果,也就是主線程需要在子線程結束后再結束,這時就要用到join方法。
System.out.println(Thread.currentThread().getName() + "線程運行開始!"); Thread6 thread1 = new Thread6(); thread1.setName("線程 B"); thread1.join(); System.out.println("這時 thread1 執行完畢之后才能執行主線程");
6、一個類是否可以同時繼承Thread和實現Runnable接口?
可以。比如下面的程序可以通過編譯。因為Test類從Thread類中繼承了run()方法,這個run()方法可以被當作對Runnable接口的實現。
public class Test extends Thread implements Runnable{ public static void main(String[] args){ Thread t = new Thread(new Test()); t.start(); } }