1. 程式人生 > >並發基礎(一) 線程介紹

並發基礎(一) 線程介紹

java 並發

一、線程的簡介

線程,有時被稱為輕量級進程(Lightweight Process,LWP),是程序執行流的最小單元。一個標準的線程由線程ID,當前指令指針(有一個程序計數器,它的作用是存放下一條指令所在單元的地址的地方),寄存器集合(寄存器是中央處理器內的組成部分。寄存器是有限存貯容量的高速存貯部件,它們可用來暫存指令、數據和地址。在中央處理器的控制部件中,包含的寄存器有指令寄存器(IR)和程序計數器(PC)。在中央處理器的算術及邏輯部件中,存器有累加器(ACC)。)和堆棧組成。

另外,線程是進程中的一個實體,是被系統獨立調度和分派的基本單位,線程自己不擁有系統資源,只擁有一點兒在運行中必不可少的資源,但它可與同屬一個進程的其它線程共享進程所擁有的全部資源。一個線程可以創建和撤消另一個線程,同一進程中的多個線程之間可以並發執行。由於線程之間的相互制約(共用資源造成的),致使線程在運行中呈現出間斷性。線程也有就緒、阻塞和運行三種基本狀態。就緒狀態是指線程具備運行的所有條件,邏輯上可以運行,在等待處理機;運行狀態是指線程占有處理機正在運行;阻塞狀態是指線程在等待一個事件(如某個信號量),邏輯上不可執行。每一個程序都至少有一個線程,若程序只有一個線程,那就是程序本身。

線程是程序中一個單一的順序控制流程。進程內一個相對獨立的、可調度的執行單元,是系統獨立調度和分派CPU的基本單位指運行中的程序的調度單位。在單個程序中同時運行多個線程完成不同的工作,稱為多線程。

特點

在多線程OS中,通常是在一個進程中包括多個線程,每個線程都是作為利用CPU的基本單位,是花費最小開銷的實體。線程具有以下屬性。
1)輕型實體

線程中的實體基本上不擁有系統資源,只是有一點必不可少的、能保證獨立運行的資源。

線程的實體包括程序、數據和TCB。線程是動態概念,它的動態特性由線程控制塊
TCB(Thread Control Block)描述。TCB包括以下信息:
(1)線程狀態。
(2)當線程不運行時,被保存的現場資源。

(3)一組執行堆棧。
(4)存放每個線程的局部變量主存區。
(5)訪問同一個進程中的主存和其它資源。

用於指示被執行指令序列的程序計數器、保留局部變量、少數狀態參數和返回地址等的一組寄存器和堆棧。
2)獨立調度和分派的基本單位。

在多線程OS中,線程是能獨立運行的基本單位,因而也是獨立調度和分派的基本單位。由於線程很“輕”,故線程的切換非常迅速且開銷小(在同一進程中的)。
3)可並發執行。

在一個進程中的多個線程之間,可以並發執行,甚至允許在一個進程中所有線程都能並發執行;同樣,不同進程中的線程也能並發執行,充分利用和發揮了處理機與外圍設備並行工作的能力。
4)共享進程資源。

在同一進程中的各個線程,都可以共享該進程所擁有的資源,這首先表現在:所有線程都具有相同的地址空間(進程的地址空間),這意味著,線程可以訪問該地址空間的每一個虛地址;此外,還可以訪問進程所擁有的已打開文件、定時器、信號量機構等。由於同一個進程內的線程共享內存和文件,所以線程之間互相通信不必調用內核。

與進程比較(容易混淆)

進程是資源分配的基本單位。所有與該進程有關的資源,都被記錄在進程控制塊PCB中。以表示該進程擁有這些資源或正在使用它們。

另外,進程也是搶占處理機的調度單位,(進程就像一個表演團隊一樣,而處理機可以形容為舞臺,線程可以形容為表演團隊的個人)它擁有一個完整的虛擬地址空間(進程可用的虛擬地址範圍稱為該進程的虛擬地址空間(當處理器讀或寫入內存位置時,它會使用虛擬地址。作為讀或寫操作的一部分,處理器將虛擬地址轉換為物理地址。))。當進程發生調度時,不同的進程擁有不同的虛擬地址空間,而同一進程內的不同線程共享同一地址空間。

與進程相對應,線程與資源分配無關,它屬於某一個進程,並與進程內的其他線程一起共享進程的資源。

線程只由相關堆棧(系統棧或用戶棧)寄存器和線程控制表TCB組成。寄存器可被用來存儲線程內的局部變量,但不能存儲其他線程的相關變量。

通常在一個進程中可以包含若幹個線程,它們可以利用進程所擁有的資源。在引入線程的操作系統中,通常都是把進程作為分配資源的基本單位,而把線程作為獨立運行和獨立調度的基本單位。由於線程比進程更小,基本上不擁有系統資源,故對它的調度所付出的開銷就會小得多,能更高效的提高系統內多個程序間並發執行的程度,從而顯著提高系統資源的利用率和吞吐量。因而近年來推出的通用操作系統都引入了線程,以便進一步提高系統的並發性,並把它視為現代操作系統的一個重要指標。

線程與進程的區別可以歸納為以下4點:
1)地址空間和其它資源(如打開文件):進程間相互獨立,同一進程的各線程間共享。某進程內的線程在其它進程不可見。
2)通信:進程間通信IPC,線程間可以直接讀寫進程數據段(如全局變量)來進行通信——需要進程同步和互斥手段的輔助,以保證數據的一致性。
3)調度和切換:線程上下文切換比進程上下文切換要快得多。
4)在多線程OS中,進程不是一個可執行的實體。

線程基礎知識點可以總結為幾點:

  • 線程是程序中的執行線程。java虛擬機允許應用程序並發地運行多個執行線程。
  • 每個線程都有一個優先級,高優先級線程的執行優先於低級優先級線程。但不應該通過設置線程優先級的方式來安排線程的執行順序,後續將會細說。
  • 每個線程都可以或者不標誌為一個守護線程。即在java中,線程分為兩類:用戶線程 和 守護線程。
  • 當java虛擬機啟動時,都會有一個非守護線程(即用戶線程)啟動運行。這個線程通常是調用指定類的main方法。簡單來說,當你執行一個類的main方法時,其實是作為一個線程在運行,即為main線程。
  • 線程在操作系統中是不擁有資源, 線程是共享進程的資源。即屬於同一個進程的多個線程之間是對資源可能要進行互斥訪問。這就要涉及到鎖的概念。同時在JVM中,除了用鎖來解決並發的問題外,還可以讓每個線程擁有私有資源---線程副本(ThreadLocal),這樣,線程就不需要競爭資源。

二、用戶線程與守護線程

在Java中有兩類線程:User Thread(用戶線程)、Daemon Thread(守護線程)

用個比較通俗的比如,任何一個守護線程都是整個JVM中所有非守護線程的保姆:

只要當前JVM實例中尚存在任何一個非守護線程沒有結束,守護線程就全部工作;只有當最後一個非守護線程結束時,守護線程隨著JVM一同結束工作。
Daemon的作用是為其他線程的運行提供便利服務,守護線程最典型的應用就是 GC (垃圾回收器),它就是一個很稱職的守護者。

User和Daemon兩者幾乎沒有區別,唯一的不同之處就在於虛擬機的離開:如果 User Thread已經全部退出運行了,只剩下Daemon Thread存在了,虛擬機也就退出了。 因為沒有了被守護者,Daemon也就沒有工作可做了,也就沒有繼續運行程序的必要了。

值得一提的是,守護線程並非只有虛擬機內部提供,用戶在編寫程序時也可以自己設置守護線程。下面的方法就是用來設置守護線程的。

    Thread daemonTread = new Thread();  

      // 設定 daemonThread 為 守護線程,default false(非守護線程)  
     daemonThread.setDaemon(true);  

     // 驗證當前線程是否為守護線程,返回 true 則為守護線程  
     daemonThread.isDaemon();  

這裏有幾點需要註意:

(1) thread.setDaemon(true)必須在thread.start()之前設置,否則會跑出一個IllegalThreadStateException異常。你不能把正在運行的常規線程設置為守護線程。
(2) 在Daemon線程中產生的新線程也是Daemon的。
(3) 不要認為所有的應用都可以分配給Daemon來進行服務,比如讀寫操作或者計算邏輯。

因為你不可能知道在所有的User完成之前,Daemon是否已經完成了預期的服務任務。一旦User退出了,可能大量數據還沒有來得及讀入或寫出,計算任務也可能多次運行結果不一樣。這對程序是毀滅性的。造成這個結果理由已經說過了:一旦所有User Thread離開了,虛擬機也就退出運行 //完成文件輸出的守護線程任務

    import java.io.*;     

    class TestRunnable implements Runnable{     
        public void run(){     
                   try{     
                      Thread.sleep(1000);//守護線程阻塞1秒後運行     
                      File f=new File("daemon.txt");     
                      FileOutputStream os=new FileOutputStream(f,true);     
                      os.write("daemon".getBytes());     
               }     
                   catch(IOException e1){     
              e1.printStackTrace();     
                   }     
                   catch(InterruptedException e2){     
                      e2.printStackTrace();     
               }     
        }     
    }     
    public class TestDemo2{     
        public static void main(String[] args) throws InterruptedException     
        {     
            Runnable tr=new TestRunnable();     
            Thread thread=new Thread(tr);     
                    thread.setDaemon(true); //設置守護線程     
            thread.start(); //開始執行分進程     
        }     
    }     
    //運行結果:文件daemon.txt中沒有"daemon"字符串。

看到了吧,把輸入輸出邏輯包裝進守護線程多麽的可怕,字符串並沒有寫入指定文件。原因也很簡單,直到主線程完成,守護線程仍處於1秒的阻塞狀態。這個時候主線程很快就運行完了,虛擬機退出,Daemon停止服務,輸出操作自然失敗了。

    public class Test {  
      public static void main(String args) {  
      Thread t1 = new MyCommon();  
      Thread t2 = new Thread(new MyDaemon());  
      t2.setDaemon(true); //設置為守護線程  
      t2.start();  
      t1.start();  
      }  
      }  
      class MyCommon extends Thread {  
      public void run() {  
      for (int i = 0; i < 5; i++) {  
      System.out.println("線程1第" + i + "次執行!");  
      try {  
      Thread.sleep(7);  
      } catch (InterruptedException e) {  
      e.printStackTrace();  
      }  
      }  
      }  
      }  

    class MyDaemon implements Runnable {  
      public void run() {  
      for (long i = 0; i < 9999999L; i++) {  
      System.out.println("後臺線程第" + i + "次執行!");  
      try {  
      Thread.sleep(7);  
      } catch (InterruptedException e) {  
      e.printStackTrace();  
      }  
      }  
      }  
      }  

輸出結果:

後臺線程第0次執行!
線程1第0次執行!
線程1第1次執行!
後臺線程第1次執行!
後臺線程第2次執行!
線程1第2次執行!
線程1第3次執行!
後臺線程第3次執行!
線程1第4次執行!
後臺線程第4次執行!
後臺線程第5次執行!
後臺線程第6次執行!
後臺線程第7次執行!
Process finished with exit code 0
 
 
從上面的執行結果可以看出:
前臺線程是保證執行完畢的,後臺線程還沒有執行完畢就退出了。

實際上:JRE判斷程序是否執行結束的標準是所有的前臺執線程行完畢了,而不管後臺線程的狀態,因此,在使用後臺縣城時候一定要註意這個問題。

補充說明:

定義:守護線程--也稱“服務線程”,在沒有用戶線程可服務時會自動離開。

優先級:守護線程的優先級比較低,用於為系統中的其它對象和線程提供服務。

設置:通過setDaemon(true)來設置線程為“守護線程”;將一個用戶線程設置為守護線程的方式是在 線程對象創建 之前 用線程對象的setDaemon方法。

example: 垃圾回收線程就是一個經典的守護線程,當我們的程序中不再有任何運行的Thread,程序就不會再產生垃圾,垃圾回收器也就無事可做,所以當垃圾回收線程是JVM上僅剩的線程時,垃圾回收線程會自動離開。它始終在低級別的狀態中運行,用於實時監控和管理系統中的可回收資源。

生命周期:守護進程(Daemon)是運行在後臺的一種特殊進程。它獨立於控制終端並且周期性地執行某種任務或等待處理某些發生的事件。也就是說守護線程不依賴於終端,但是依賴於系統,與系統“同生共死”。那Java的守護線程是什麽樣子的呢。當JVM中所有的線程都是守護線程的時候,JVM就可以退出了;如果還有一個
或以上的非守護線程則JVM不會退出。

實際應用例子:

在使用長連接的comet服務端推送技術中,消息推送線程設置為守護線程,服務於ChatServlet的servlet用戶線程,在servlet的init啟動消息線程,servlet一旦初始化後,一直存在服務器,servlet摧毀後,消息線程自動退出

容器收到一個Servlet請求,調度線程從線程池中選出一個工作者線程,將請求傳遞給該工作者線程,然後由該線程來執行Servlet的 service方法。當這個線程正在執行的時候,容器收到另外一個請求,調度線程同樣從線程池中選出另一個工作者線程來服務新的請求,容器並不關心這個請求是否訪問的是同一個Servlet.當容器同時收到對同一個Servlet的多個請求的時候,那麽這個Servlet的service()方法將在多線程中並發執行。
Servlet容器默認采用單實例多線程的方式來處理請求,這樣減少產生Servlet實例的開銷,提升了對請求的響應時間,對於Tomcat可以在server.xml中通過<Connector>元素設置線程池中線程的數目。
如圖:

技術分享圖片

為什麽要用守護線程?

我們知道靜態變量是ClassLoader級別的,如果Web應用程序停止,這些靜態變量也會從JVM中清除。但是線程則是JVM級別的,如果你在Web 應用中啟動一個線程,這個線程的生命周期並不會和Web應用程序保持同步。也就是說,即使你停止了Web應用,這個線程依舊是活躍的。正是因為這個很隱晦 的問題,所以很多有經驗的開發者不太贊成在Web應用中私自啟動線程。

如果我們手工使用JDK Timer(Quartz的Scheduler),在Web容器啟動時啟動Timer,當Web容器關閉時,除非你手工關閉這個Timer,否則Timer中的任務還會繼續運行!

下面通過一個小例子來演示這個“詭異”的現象,我們通過ServletContextListener在Web容器啟動時創建一個Timer並周期性地運行一個任務:

    //代碼清單StartCycleRunTask:容器監聽器  
    package com.baobaotao.web;  
    import java.util.Date;  
    import java.util.Timer;  
    import java.util.TimerTask;  
    import javax.servlet.ServletContextEvent;  
    import javax.servlet.ServletContextListener;  
    public class StartCycleRunTask implements ServletContextListener ...{  
        private Timer timer;  
        public void contextDestroyed(ServletContextEvent arg0) ...{  
            // ②該方法在Web容器關閉時執行  
            System.out.println("Web應用程序啟動關閉...");  
        }  
        public void contextInitialized(ServletContextEvent arg0) ...{  
             //②在Web容器啟動時自動執行該方法  
            System.out.println("Web應用程序啟動...");  
            timer = new Timer();//②-1:創建一個Timer,Timer內部自動創建一個背景線程  
            TimerTask task = new SimpleTimerTask();  
            timer.schedule(task, 1000L, 5000L); //②-2:註冊一個5秒鐘運行一次的任務  
        }  
    }  
    class SimpleTimerTask extends TimerTask ...{//③任務  
        private int count;  
        public void run() ...{  
            System.out.println((++count)+"execute task..."+(new Date()));  
        }  
    }  

web.xml中聲明這個Web容器監聽器:

<?xml version="1.0" encoding="UTF-8"?>
<web-app>
…
<listener>
<listener-class>com.baobaotao.web.StartCycleRunTask</listener-class>
</listener>
</web-app>

在Tomcat中部署這個Web應用並啟動後,你將看到任務每隔5秒鐘執行一次。
運行一段時間後,登錄Tomcat管理後臺,將對應的Web應用(chapter13)關閉。

轉到Tomcat控制臺,你將看到雖然Web應用已經關閉,但Timer任務還在我行我素地執行如故——舞臺已經拆除,戲子繼續表演:

我們可以通過改變清單StartCycleRunTask的代碼,在contextDestroyed(ServletContextEvent arg0)中添加timer.cancel()代碼,在Web容器關閉後手工停止Timer來結束任務。

Spring為JDK TimerQuartz Scheduler所提供的TimerFactoryBeanSchedulerFactoryBean能夠和Spring容器的生命周期關聯,在 Spring容器啟動時啟動調度器,而在Spring容器關閉時,停止調度器。所以在Spring中通過這兩個FactoryBean配置調度器,再從 Spring IoC中獲取調度器引用進行任務調度將不會出現這種Web容器關閉而任務依然運行的問題。而如果你在程序中直接使用Timer或Scheduler,如不 進行額外的處理,將會出現這一問題。

#####參考資料

  • https://blog.csdn.net/shimiso/article/details/8964414
  • http://www.cnblogs.com/jinggod/p/8484674.html

文章有不當之處,歡迎指正,你也可以關註我的微信公眾號:好好學java,獲取優質學習資源,也可以加入QQ技術交流群:766946816,咋們來聊聊java。

並發基礎(一) 線程介紹