Java之線程與進程


一、線程與進程

  線程:一條線程指的是進程中一個單一順序的控制流,一個進程中可以並發多個線程,每條線程並行執行不同的任務。多線程是多任務的一種特別形式,但多線程使用了更小的資源開銷。

  進程:一個進程包括有操作系統分配的內存空間,包含一個或多個線程。一個線程不能獨立的存在。它必須是進程的一部分。一個進程一直運行直到所有的非守護線程都結束運行后才能結束。

  進程 線程
定義

進程是指處於運行中的程序,並且具有一定的獨立功能。進程是系統進行資源分配和調度的一個單位。

當程序進入內存時,即為進程。

線程是進程的組成部分,一個進程可以擁有多個線程,而一個線程必須擁有一個父進程。

線程可以擁有自己的堆棧,自己的程序計數器和局部變量,但不能擁有系統資源。

它與父進程的其他線程共享該進城的所有資源。

特點

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(); } }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM