多線程並發詳解


一、Java 線程實現/創建方式

  注意:

  • 新建的線程不會自動開始運行,必須通過start( )方法啟動

  • 不能直接調用run()來啟動線程,這樣run()將作為一個普通方法立即執行,執行完畢前其他線程無法並發執行

  • Java程序啟動時,會立刻創建主線程,main就是在這個線程上運行。當不再產生新線程時,程序是單線程的

 1.1 繼承Thread 類

   Thread 類本質上是實現了 Runnable 接口的一個實例,代表一個線程的實例。啟動線程的唯一方法就是通過 Thread 類的 start()實例方法。start()方法是一個 native 方法,它將啟動一個新線程,並執行 run()方法。

    • 優勢:編寫簡單

    • 劣勢:無法繼承其它父類

  1.1.1 創建:繼承Thread+重寫run

  1.1.2 啟動:創建子類對象+調用start

public class StartThread extends Thread{

	//線程入口點
	@Override
	public void run() {
		for(int i=0;i<10;i++) {
			System.out.println("listen music");
		}
	}
	public static void main(String[] args) {
		//創建子類對象
		StartThread st=new StartThread();
		//調用start方法
		st.start();//開啟新線程交於cpu決定執行順序
		for(int i=0;i<10;i++) {
			System.out.println("coding");
		}
	}
}

 1.2 實現runnable接口

   如果自己的類已經 extends 另一個類,就無法直接 extends Thread,此時,可以實現一個Runnable 接口。

    • 優勢:可以繼承其它類,多線程可共享同一個Runnable對象

    • 劣勢:編程方式稍微復雜,如果需要訪問當前線程,需要調用Thread.currentThread()方法

   1.2.1 創建:實現runnable接口+重寫run

   1.2.2 啟動:創建實現類對象+Thread類對象+調用start

public class StartRun implements Runnable{

	//線程入口點
	@Override
	public void run() {
		for(int i=0;i<10;i++) {
			System.out.println("listen music");
		}
	}
	public static void main(String[] args) {
		//創建實現類對象
		StartRun st=new StartRun();
		//創建代理類對象
      //啟動 MyThread,需要首先實例化一個 Thread,並傳入自己的 MyThread 實例:

		Thread t=new Thread(st);           
     //事實上,當傳入一個 Runnable target 參數給 Thread 后,Thread 的 run()方法就會調用target.run()
		//調用start方法
		t.start();//開啟新線程交於cpu決定執行順序

		//匿名法
//		new Thread(new StartRun()).start();
		for(int i=0;i<10;i++) {
			System.out.println("coding");
		}
	}
}

  

 1.3 實現Callable接口

   1.3.1 創建:實現callable接口+重寫call

   1.3.2 啟動:創建Callable實現類的實現,使用FutureTask類包裝Callable對象,該FutureTask對象封裝了Callable對象的Call方法的返回值

   1.3.3 使用FutureTask對象作為Thread對象的target創建並啟動線程

   1.3.4 調用FutureTask對象的get()來獲取子線程執行結束的返回值

   有返回值的任務必須實現 Callable 接口,類似的,無返回值的任務必須 Runnable 接口。執行Callable 任務后,可以獲取一個 Future 的對象,在該對象上調用 get 就可以獲取到 Callable 任務返回的 Object 了,再結合線程池接口 ExecutorService 就可以實現傳說中有返回結果的多線程了。

    • 與實行Runnable相比, Callable功能更強大些

    • 方法不同

    • 可以有返回值,支持泛型的返回值

    • 可以拋出異常

    • 需要借助FutureTask,比如獲取返回結果

  Future接口

    • 可以對具體Runnable、Callable任務的執行結果進行取消、查詢是否完成、獲取結果等。

    • FutrueTask是Futrue接口的唯一的實現類

    • FutureTask 同時實現了Runnable, Future接口。它既可以作為Runnable被線程執行,又可以作為Future得到Callable的返回值
 簡單實現Callable接口
public class ThreadTest {
  public static void main(String[] args) {
    Callable<Integer> myCallable = new MyCallable(); // 創建MyCallable對象
    FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask來包裝MyCallable對象

    for (int i = 0; i < 100; i++) {
      System.out.println(Thread.currentThread().getName() + " " + i);
      if (i == 30) {
        Thread thread = new Thread(ft); //FutureTask對象作為Thread對象的target創建新的線程
        thread.start(); //線程進入到就緒狀態
      }
    }

    System.out.println("主線程for循環執行完畢..");
    try {       int sum = ft.get(); //取得新創建的新線程中的call()方法返回的結果       System.out.println("sum = " + sum);     } catch (InterruptedException e) {       e.printStackTrace();     } catch (ExecutionException e) {       e.printStackTrace();     }   } } class MyCallable implements Callable<Integer> {   private int i = 0;   // 與run()方法不同的是,call()方法具有返回值   @Override   public Integer call() {     int sum = 0;     for (; i < 100; i++) {       System.out.println(Thread.currentThread().getName() + " " + i);       sum += i;     }     return sum;   } }
 
ExecutorService、Callable<Class>、Future 有返回值線程
//創建一個線程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
//創建多個有返回值的任務
List<Future> list = new ArrayList<Future>(); 
for (int i = 0; i < taskSize; i++) { 
	Callable c = new MyCallable(i + " "); 
	//執行任務並獲取 Future 對象
	Future f = pool.submit(c); 
	list.add(f); 
} 
//關閉線程池
pool.shutdown(); 
//獲取所有並發任務的運行結果
for (Future f : list) { 
	//從 Future 對象上獲取任務的返回值,並輸出到控制台
	System.out.println("res:" + f.get().toString()); 
}

 1.4 基於線程池的方式

   線程和數據庫連接這些資源都是非常寶貴的資源。那么每次需要的時候創建,不需要的時候銷毀,是非常浪費資源的。那么我們就可以使用緩存的策略,也就是使用線程池

// 創建線程池
 ExecutorService threadPool = Executors.newFixedThreadPool(10);
 while(true) {
     threadPool.execute(new Runnable() { // 提交多個線程任務,並執行
         @Override
         public void run() {
             System.out.println(Thread.currentThread().getName() + " is running ..");
             try {
                 Thread.sleep(3000);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
      });
   } 
}                

  

二、四種線程池

 線程組

  • 線程組表示一個線程的集合。

  • 線程組也可以包含其他線程組。線程組構成一棵樹。在樹中,除了初始線程組外,每個線程組都有一個父線程組。

  • 頂級線程組名system,線程的默認線程組名稱是main

  • 在創建之初,線程被限制到一個組里,而且不能改變到一個不同的組

 線程組的作用

  • 統一管理:便於對一組線程進行批量管理線程或線程組對象

  • 安全隔離:允許線程訪問有關自己的線程組的信息,但是不允許它訪問有關其線程組的父線程組或其他任何線程組的信息

  • 查看ThreadGroup、Thread構造方法代碼,觀察默認線程組的情況

 線程池(JDK1.5起,提供了內置線程池)

  • 創建和銷毀對象是非常耗費時間的

  • 創建對象:需要分配內存等資源

  • 銷毀對象:雖然不需要程序員操心,但是垃圾回收器會在后台一直跟蹤並銷毀

  • 對於經常創建和銷毀、使用量特別大的資源,比如並發情況下的線程,對性能影響很大。

  • 思路:創建好多個線程,放入線程池中,使用時直接獲取引用,不使用時放回池中。可以避免頻繁創建銷毀、實現重復利用

 線程池作用:
  • 提高響應速度(減少了創建新線程的時間)

  • 降低資源消耗(重復利用線程池中線程,不需要每次都創建)

  • 提高線程的可管理性:避免線程無限制創建、從而銷耗系統資源,降低系統穩定性,甚至內存溢出或者CPU耗盡

 線程池應用場合

  • 需要大量線程,並且完成任務的時間端

  • 對性能要求苛刻

  • 接受突發性的大量請求

 線程池的組成

  一般的線程池主要分為以下 4 個組成部分:

     1. 線程池管理器:用於創建並管理線程池
     2. 工作線程:線程池中的線程
     3. 任務接口:每個任務必須實現的接口,用於工作線程調度其運行
     4. 任務隊列:用於存放待處理的任務,提供一種緩沖機制
 

 Java 里面線程池的頂級接口是 Executor,但是嚴格意義上講 Executor 並不是一個線程池,而只是一個執行線程的工具。真正的線程池接口是 ExecutorService

 

 圖解:

  • Executor:線程池頂級接口,只有一個方法

  • ExecutorService:真正的線程池接口
    • void execute(Runnable command) :執行任務/命令,沒有返回值,一般用來執行Runnable 
    • <T> Future<T> submit(Callable<T> task):執行任務,有返回值,一般又來執行Callable
    • void shutdown() :關閉連接池

  • AbstractExecutorService:基本實現了ExecutorService的所有方法

  • ThreadPoolExecutor:默認的線程池實現類

  • ScheduledThreadPoolExecutor:實現周期性任務調度的線程池

  • Executors:工具類、線程池的工廠類,用於 創建並返回不同類型的線程池

 2.1 Executors.newCachedThreadPool

  創建一個可 根據需要創建新線程的線程池,但是在以前構造的線程可用時將重用它們。對於執行很多短期異步任務的程序而言,這些線程池通常可提高程序性能。 調用 execute 將重用以前構造的線程(如果線程可用)。如果現有線程沒有可用的,則創建一個新線程並添加到池中。終止並從緩存中移除那些已有 60 秒鍾未被使用的線程。因此, 長時間保持空閑的線程池不會使用任何資源

 2.2 Executors.newFixedThreadPool

  創建一個 可重用固定線程數的線程池,以共享的無界隊列方式來運行這些線程。在任意點,在大多數 Threads 線程會處於處理任務的活動狀態。 如果在所有線程處於活動狀態時提交附加任務,則在有可用線程之前,附加任務將在隊列中等待。如果在關閉前的執行期間由於失敗而導致任何線程終止,那么一個新線程將代替它執行后續的任務(如果需要)。在某個線程被 顯式地關閉之前,池中的線程將一直存在

 2.3 Executors.newScheduledThreadPool

  創建一個線程池,它可安排在給定延遲后運行命令或者定期地執行。

 ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3); 
 scheduledThreadPool.schedule(new Runnable(){ 
     @Override 
     public void run() {
         System.out.println("延遲三秒");
     }
 }, 3, TimeUnit.SECONDS);
scheduledThreadPool.scheduleAtFixedRate(new Runnable(){ 
     @Override 
     public void run() {
         System.out.println("延遲 1 秒后每三秒執行一次");
     }
 },1,3,TimeUnit.SECONDS);

 2.4 Executors.newSingleThreadExecutor

  Executors.newSingleThreadExecutor()返回一個線程池(這個線程池只有一個線程),這個線程池可以在線程死后(或發生異常時)重新啟動一個線程來替代原來的線程繼續執行下去!

 

 線程池參數:

  • corePoolSize:核心池的大小
    • 默認情況下,創建了線程池后,線程數為0,當有任務來之后,就會創建一個線程去執行任務。
    • 但是當線程池中線程數量達到corePoolSize,就會把到達的任務放到隊列中等待。

  • maximumPoolSize:最大線程數
    • corePoolSize和maximumPoolSize之間的線程數會自動釋放,小於等於corePoolSize的不會釋放。當大於了這個值就會將任務由一個丟棄處理機制來處理。

  • keepAliveTime:線程沒有任務時最多保持多長時間后會終止 
    
• 默認只限於corePoolSize和maximumPoolSize之間的線程

  • TimeUnit: keepAliveTime的時間單位

  • BlockingQueue:存儲等待執行的任務的阻塞隊列,有多中選擇,可以是順序隊列、鏈式隊列等。

   • workQueue:任務隊列,被提交但尚未被執行的任務。

  • ThreadFactory:線程工廠,默認是DefaultThreadFactory,Executors的靜態內部類

  • RejectedExecutionHandler: 
    拒絕處理任務時的策略。如果線程池的線程已經飽和,並且任務隊列也已滿,對新的任務應該采取什么策略。
    • 比如拋出異常、直接舍棄、丟棄隊列中最舊任務等,默認是直接拋出異常。
      • 1、CallerRunsPolicy:只要線程池未關閉,該策略直接在調用者線程中,運行當前被丟棄的任務。顯然這樣做不會真的丟棄任務,但是,任務提交線程的性能極有可能會急劇下降。
      • 2、DiscardOldestPolicy:丟棄最老的一個請求,也就是即將被執行的一個任務,並嘗試再次提交當前任務。
      • 3、DiscardPolicy:該策略默默地丟棄無法處理的任務,不予任何處理。如果允許任務丟失,這是最好的一種方案。
      • 4、AbortPolicy:java默認,拋出一個異常
    以上內置拒絕策略均實現了 RejectedExecutionHandler 接口,若以上策略仍無法滿足實際需要,完全可以 自己擴展 RejectedExecutionHandler 接口

三、線程的生命周期(狀態)

   

  當線程被創建並啟動以后,它既不是一啟動就進入了執行狀態,也不是一直處於執行狀態。在線程的生命周期中,它要經過 新生(New)、就緒(Runnable)、運行(Running)、阻塞(Blocked)和死亡(Dead)5 種狀態。尤其是當線程啟動以后,它不可能一直"霸占"着 CPU 獨自運行,所以 CPU 需要在多條線程之間切換,於是 線程狀態也會多次在運行、阻塞之間切換

 3.1 新生狀態(NEW) 

  • 用new關鍵字建立一個線程對象后,該線程對象就處於新生狀態。

  • 處於新生狀態的線程有 自己的內存空間,,此時僅由 JVM 為其分配內存,並初始化其成員變量的值,通過調用start進入就緒狀態 。

 3.2 就緒狀態(RUNNABLE)

  • 當線程對象調用了 start()方法之后,該線程處於就緒狀態。Java 虛擬機會為其創建方法調用棧和程序計數器,等待調度運行。

  • 處於就緒狀態線程具備了運行條件,但還沒分配到CPU,處於線程就緒隊列,等待系統為其分配CPU

  • 當系統選定一個等待執行的線程后,它就會從就緒狀態進入執行狀態,該動作稱之為“cpu調度”。

 3.3 運行狀態(RUNNING)

  • 如果處於就緒狀態的線程獲得了 CPU,開始執行 run()方法的線程執行體,則該線程處於運行狀態。 

  • 在運行狀態的線程執行自己的run方法中代碼,直到等待某資源而阻塞或完成任務而死亡。

  • 如果在給定的時間片內沒有執行結束,就會被系統給換下來回到等待執行狀態。

 3.4 阻塞狀態(BLOCKED)

  阻塞狀態是指線程因為某種原因放棄了 cpu 使用權,也即讓出了 cpu timeslice,暫時停止運行進入阻塞狀態。在阻塞狀態的線程不能進入就緒隊列。只有當引起阻塞的原因消除時,如睡眠時間已到,或等待的I/O設備空閑下來,線程便轉入就緒狀態,重新到就緒隊列中排隊等待,才有機會再次獲得 cpu timeslice 轉到運行(running)狀態。被系統選中后從原來停止的位置開始繼續運行。
  阻塞的情況分三種:

   • 等待阻塞(o.wait->等待對列):運行(running)的線程執行 o.wait()方法,JVM 會把該線程放入等待隊列(waitting queue)中。

   • 同步阻塞(lock->鎖池) :運行(running)的線程在獲取對象的同步鎖時,若該同步鎖被別的線程占用,則 JVM 會把該線程放入鎖池(lock pool)中。

   •  其他阻塞(sleep/join) :運行(running)的線程 執行 Thread.sleep(long ms)或 t.join()方法,或者發出了 I/O 請求時,JVM 會把該線程置為阻塞狀態。當 sleep()狀態超時、join()等待線程終止或者超時、或者 I/O處理完畢時,線程重新轉入可運行(runnable)狀態。
 

 3.5 死亡狀態(DEAD)

   死亡狀態是線程生命周期中的最后一個階段。線程會以下面三種方式結束,結束后就是死亡狀態。

    •  正常結束 : run()或 call()方法執行完成,線程正常運行結束。

    •  異常結束 :線程拋出一個未捕獲的 Exception 或 Error。

    •   調用 stop強制終止 :直接調用該線程的 stop()方法來結束該線程—該方法通常 容易導致死鎖不推薦使用
    

四、終止線程的四種方式

 4.1 正常運行結束

  程序運行結束,線程自動結束。

 4.2 使用退出標志退出線程

  一般 run()方法執行完,線程就會正常結束,然而,常常有些線程是 伺服線程。它們需要長時間的運行,只有在外部某些條件滿足的情況下,才能關閉這些線程。使用一個變量來控制循環,例如:最直接的方法就是 設一個 boolean 類型的標志,並通過設置這個標志為 true 或 false 來 控制 while循環是否退出,代碼示例:
public class ThreadSafe extends Thread {
    public volatile boolean exit = false; 
    public void run() { 
        while (!exit){
         //do something
        }
    } 
}

  定義了一個退出標志 exit,當 exit 為 true 時,while 循環退出,exit 的默認值為 false.在定義 exit時,使用了一個 Java 關鍵字 volatile(保證可見性但是不保證原子性,線程不安全),這個關鍵字的目的是使 exit 同步,也就是說在同一時刻只能由一個線程來修改 exit 的值。

 4.3 Interrupt 方法結束線程

  使用 interrupt()方法來中斷線程有兩種情況:

   4.3.1 線程處於阻塞狀態:如使用了 sleep,同步鎖的 wait,socket 中的 receiver,accept 等方法時,會使線程處於阻塞狀態。當調用線程的 interrupt()方法時,會拋出 InterruptException 異常。阻塞中的那個方法拋出這個異常,通過代碼捕獲該異常,然后 break 跳出循環狀態,從而讓我們有機會結束這個線程的執行。 通常很多人認為只要調用 interrupt 方法線程就會結束,實際上是錯的, 一定要先捕獲 InterruptedException 異常之后通過 break 來跳出循環,才能正常結束 run 方法。

  4.3.2 線程未處於阻塞狀態:使用 isInterrupted()判斷線程的中斷標志來退出循環。當使用interrupt()方法時,中斷標志就會置 true,和使用自定義的標志來控制循環是一樣的道理。

  
 public class ThreadSafe extends Thread {
     public void run() { 
         while (!isInterrupted()){ //非阻塞過程中通過判斷中斷標志來退出
             try{
                 Thread.sleep(5*1000);//阻塞過程捕獲中斷異常來退出
             }catch(InterruptedException e){
                 e.printStackTrace();
                 break;//捕獲到異常之后,執行 break 跳出循環
             }
         }
     } 
}

 4.4 stop 方法終止線程(線程不安全)

  程序中可以直接使用 thread.stop()來強行終止線程,但是 stop 方法是很危險的,就象突然關閉計算機電源,而不是按正常程序關機一樣,可能會產生不可預料的結果,不安全主要是:thread.stop()調用之后, 創建子線程的線程就會拋出 ThreadDeatherror 的錯誤,並且會 釋放子線程所持有的所有鎖。一般任何進行加鎖的代碼塊,都是為了保護數據的一致性,如果在 調用thread.stop()后導致了該線程所持有的所有鎖的突然釋放(不可控制),那么被保護數據就有可能呈現不一致性,其他線程在使用這些被破壞的數據時,有可能導致一些很奇怪的應用程序錯誤。因此,並不推薦使用 stop 方法來終止線程。

 五、線程控制方法

  5.1 優先級控制

    Java提供一個線程調度器來監控程序中啟動后進入就緒狀態的所有線程。線程調度器按照線程的優先級決定應調度哪個線程來執行。

   線程的優先級用數字表示,范圍從1到10:

    • Thread.MIN_PRIORITY = 1

    • Thread.MAX_PRIORITY = 10

    • Thread.NORM_PRIORITY = 5

   使用下述方法獲得或設置線程對象的優先級。

    • int getPriority();

    • void setPriority(int newPriority);

    注意:優先級低只是意味着獲得調度的概率低。並不是絕對先調用優先級高后調用優先級低的線程。

 5.2 線程啟動start

   線程由新生態進入就緒態,等待cpu調度運行。

 5.3 線程等待(wait)

   調用該方法的線程進入 WAITING 狀態,只有等待另外線程的通知或被中斷才會返回,需要注意的是調用 wait()方法后,會釋放對象的鎖。因此,wait 方法一般用在同步方法或同步代碼塊中

 5.4  線程睡眠(sleep)

   sleep 導致當前線程休眠,與 wait 方法不同的是 sleep 不會釋放當前占有的鎖,sleep(long)會導致線程進入 TIMED-WATING 狀態,而 wait()方法會導致當前線程進入 WATING 狀態。

 5.5 線程讓步(yield)

    yield 會使 當前線程讓出 CPU 執行時間片(進入就緒態),與其他線程一起重新競爭 CPU 時間片。一般情況下,優先級高的線程有更大的可能性成功競爭得到 CPU 時間片,但這又不是絕對的,有的操作系統對線程優先級並不敏感。
 

 5.6 線程中斷(interrupt)

   中斷一個線程,其 本意是給這個線程一個通知信號,會影響這個線程內部的一個中斷標識位。這個線程本身並不會因此而改變狀態(如阻塞,終止等)。

   1. 調用 interrupt()方法並不會中斷一個正在運行的線程。也就是說處於 Running 狀態的線程並不會因為被中斷而被終止,僅僅改變了內部維護的中斷標識位而已。

   2. 若調用 sleep()而使線程處於 TIMED-WATING 狀態,這時調用 interrupt()方法,會拋出InterruptedException,從而使線程提前結束 TIMED-WATING 狀態。

   3. 許多聲明拋出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),拋出異常前,都會清除中斷標識位,所以拋出異常后,調用 isInterrupted()方法將會返回 false。

   4. 中斷狀態是線程 固有的一個標識位,可以通過此標識位安全的終止線程。比如,你想終止一個線程 thread 的時候,可以調用 thread.interrupt()方法,在線程的 run 方法內部可以根據 thread.isInterrupted()的值來優雅的終止線程。
 

 5.7 插隊線程(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 執行完畢之后才能執行主線程");

 5.8 設置為守護線程(setDaemon)

   • 可以將指定的線程設置成后台線程

   • 創建后台線程的線程結束時,后台線程也隨之消亡

   • 只能在線程啟動之前把它設為后台線程

 5.9 線程喚醒(notify)

   Object 類中的 notify() 方法,喚醒在此對象監視器上等待的單個線程,如果所有線程都在此對象上等待,則會選擇喚醒其中一個線程,選擇是任意的,並在對實現做出決定時發生,線程通過調用其中一個 wait() 方法,在對象的監視器上等待,直到當前的線程放棄此對象上的鎖定,才能繼續執行被喚醒的線程,被喚醒的線程將以常規方式與在該對象上主動同步的其他所有線程進行競爭。類似的方法還有 notifyAll() ,喚醒再次監視器上等待的所有線程。

 5.10 終止線程(stop)

   結束線程,不推薦使用

 5.11 其他方法

   5.11.1 isAlive(): 判斷一個線程是否存活。

  5.11.2 activeCount(): 程序中活躍的線程數。

   5.11.3 enumerate(): 枚舉程序中的線程。

  5.11.4 currentThread(): 得到當前線程。

   5.11.5 sDaemon(): 一個線程是否為守護線程。

  5.11.6 setName(): 為線程設置一個名稱。

   5.11.7 setPriority(): 設置一個線程的優先級。

  5.11.8 getPriority():獲得一個線程的優先級。

 六、線程上下文切換

  巧妙地利用了時間片輪轉的方式, CPU 給每個任務都服務一定的時間,然后把當前任務的狀態保存下來,在加載下一任務的狀態后,繼續服務下一任務, 任務的狀態保存及再加載, 這段過程就叫做上下文切換。時間片輪轉的方式使多個任務在同一顆 CPU 上執行變成了可能。

 6.1 進程

  (有時候也稱做任務)是指一個程序運行的實例。在 Linux 系統中,線程就是能並行運行並且與他們的父進程(創建他們的進程)共享同一地址空間(一段內存區域)和其他資源的輕量級的進程。

 6.2 上下文

  是指某一時間點 CPU 寄存器和程序計數器的內容。

 6.3 寄存器

  是 CPU 內部的數量較少但是速度很快的內存(與之對應的是 CPU 外部相對較慢的 RAM 主內存)。寄存器通過對常用值(通常是運算的中間值)的快速訪問來提高計算機程序運行的速度。

 6.4 程序計數器

  是 一個專用的寄存器,用於表明指令序列中 CPU 正在執行的位置,存的值為正在執行的指令的位置或者下一個將要被執行的指令的位置,具體依賴於特定的系統。
 

 6.5 PCB-“切換楨”

  上下文切換可以認為是內核(操作系統的核心)在 CPU 上對於進程(包括線程)進行切換, 上下文切換過程中的信息是保存在進程控制塊(PCB, process control block)中的。PCB 還經常被稱作“切換楨”(switchframe)。信息會一直保存到 CPU 的內存中,直到他們被再次使用。
 

 6.6 上下文切換的活動:

  1. 掛起一個進程,將這個進程在 CPU 中的狀態(上下文)存儲於內存中的某處。
  2. 在內存中檢索下一個進程的上下文並將其在 CPU 的寄存器中恢復。
  3. 跳轉到程序計數器所指向的位置(即跳轉到進程被中斷時的代碼行),以恢復該進程在程序中。

 6.7 引起線程上下文切換的原因

  1. 當前執行任務的時間片用完之后,系統 CPU 正常調度下一個任務;
  2. 當前執行任務碰到 IO 阻塞,調度器將此任務掛起,繼續下一任務;
  3. 多個任務搶占鎖資源,當前任務沒有搶到鎖資源,被調度器掛起,繼續下一任務;
  4. 用戶代碼掛起當前任務,讓出 CPU 時間;
  5. 硬件中斷;

七、Java后台線程

   1. 定義:守護線程--也稱“服務線程”,他是后台線程,它有一個特性,即為用戶線程 提供 公共服務,在沒有用戶線程可服務時會自動離開。

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

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

  4. 在 Daemon 線程中產生的新線程也是 Daemon 的。

  5. 線程則是 JVM 級別的,以 Tomcat 為例,如果你在 Web 應用中啟動一個線程,這個線程的生命周期並不會和 Web 應用程序保持同步。也就是說,即使你停止了 Web 應用,這個線程依舊是活躍的。

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

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

八、同步鎖與死鎖

  同步鎖:當多個線程同時訪問同一個數據時,很容易出現問題。為了避免這種情況出現,我們要保證線程同步互斥,就是指並發執行的多個線程,在同一時間內只允許一個線程訪問共享數據。 Java 中可以使用 synchronized 關鍵字來取得一個對象的同步鎖。

   死鎖:何為死鎖,就是多個線程同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。

 8.1 線程同步

  當多個線程訪問同一個數據時,容易出現線程安全問題。需要讓線程同步,保證數據安全。

  當兩個或兩個以上線程訪問同一資源時,需要某種方式來確保資源在某一時刻只被一個線程使用---線程同步。

  線程同步的實現方案:
   • 同步代碼塊
    • synchronized (obj){ }
   • 同步方法
    • private synchronized void makeWithdrawal(int amt) {}

 8.2 Synchronized 同步鎖

   synchronized 它可以把任意一個非 NULL 的對象當作鎖。他屬於獨占式的悲觀鎖,同時屬於可重入鎖。

  Synchronized 作用范圍:

   1. 作用於方法時,鎖住的是對象的實例(this);
   2. 當作用於靜態方法時,鎖住的是Class實例,又因為Class的相關數據存儲在永久帶PermGen(jdk1.8 則是 metaspace),永久帶是全局共享的,因此靜態方法鎖相當於類的一個全局鎖,會鎖所有調用該方法的線程;
   3. synchronized 作用於一個對象實例時,鎖住的是所有以該對象為鎖的代碼塊。它有多個隊列,當多個線程一起訪問某個對象監視器的時候,對象監視器會將這些線程存儲在不同的容器中。

  Synchronized 核心組件:

    1) Wait Set:哪些調用 wait 方法被阻塞的線程被放置在這里;
    2) Contention List競爭隊列,所有請求鎖的線程首先被放在這個競爭隊列中;
    3) Entry List:Contention List 中那些 有資格成為候選資源的線程被移動到 Entry List 中
    4) OnDeck:任意時刻, 最多只有一個線程正在競爭鎖資源,該線程被成為 OnDeck;
    5) Owner:當前已經獲取到所資源的線程被稱為 Owner;
    6) !Owner:當前釋放鎖的線程。

  Synchronized 實現:

  

 

   1. JVM 每次從隊列的尾部取出一個數據用於鎖競爭候選者(OnDeck),但是並發情況下,ContentionList 會被大量的並發線程進行 CAS 訪問,為了降低對尾部元素的競爭,JVM 會將一部分線程移動到 EntryList 中作為候選競爭線程。

   2. Owner 線程會在 unlock 時,將 ContentionList 中的部分線程遷移到 EntryList 中,並指定EntryList 中的某個線程為 OnDeck 線程(一般是最先進去的那個線程)。

   3. Owner 線程並不直接把鎖傳遞給 OnDeck 線程,而是把鎖競爭的權利交給 OnDeck,OnDeck 需要重新競爭鎖。這樣雖然犧牲了一些公平性,但是能極大的提升系統的吞吐量,在JVM 中,也把這種選擇行為稱之為“競爭切換”。

   4. OnDeck 線程獲取到鎖資源后會變為 Owner 線程,而沒有得到鎖資源的仍然停留在 EntryList中。如果 Owner 線程被 wait 方法阻塞,則轉移到 WaitSet 隊列中,直到某個時刻通過 notify或者 notifyAll 喚醒,會重新進去 EntryList 中。

   5. 處於 ContentionList、EntryList、WaitSet 中的線程都處於阻塞狀態,該阻塞是由操作系統來完成的(Linux 內核下采用 pthread_mutex_lock 內核函數實現的)。

   6. Synchronized 是非公平鎖。 Synchronized 在線程進入 ContentionList 時, 等待的線程會先嘗試自旋獲取鎖,如果獲取不到就進入 ContentionList,這明顯對於已經進入隊列的線程是不公平的,還有一個不公平的事情就是自旋獲取鎖的線程還可能直接搶占 OnDeck 線程的鎖資源。

   7. 每個對象都有個 monitor 對象,加鎖就是在競爭 monitor 對象,代碼塊加鎖是在前后分別加上 monitorenter 和 monitorexit 指令來實現的,方法加鎖是通過一個標記位來判斷的

   8. synchronized 是一個重量級操作,需要調用操作系統相關接口,性能是低效的,有可能給線程加鎖消耗的時間比有用操作消耗的時間更多。

   9. Java1.6,synchronized 進行了很多的優化,有 適應自旋、鎖消除、鎖粗化、輕量級鎖及偏向鎖等,效率有了本質上的提高。在之后推出的 Java1.7 與 1.8 中,均對該關鍵字的實現機理做了優化。引入了 偏向鎖和輕量級鎖。都是在對象頭中有標記位,不需要經過操作系統加鎖。

   10. 鎖可以從偏向鎖升級到輕量級鎖,再升級到重量級鎖。這種升級過程叫做鎖膨脹;

   11. JDK 1.6 中默認是開啟偏向鎖和輕量級鎖,可以通過-XX:-UseBiasedLocking 來禁用偏向鎖。

 8.3 同步監視器

    • synchronized (obj){ }中的obj稱為同步監視器

    • 同步代碼塊中同步監視器可以是任何對象,但是推薦使用共享資源作為同步監視器

    • 同步方法中無需指定同步監視器,因為同步方法的同步監視器是this,也就是該對象本事

  同步監視器的執行過程

    • 第一個線程訪問,鎖定同步監視器,執行其中代碼
    • 第二個線程訪問,發現同步監視器被鎖定,無法訪問
    • 第一個線程訪問完畢,解鎖同步監視器
    • 第二個線程訪問,發現同步監視器未鎖,鎖定並訪問

 8.4 Lock鎖

  • JDK1.5后新增功能,與采用synchronized相比,lock可提供多種鎖方案,更靈活

  • java.util.concurrent.lock 中的 Lock 框架是鎖定的一個抽象,它允許把鎖定的實現作為 Java 類,而不是作為語言的特性來實現。這就為 Lock 的多種實現留下了空間,各種實現可能有不同的調度算法、性能特性或者鎖定語義。

  • ReentrantLock 類實現了 Lock ,它擁有與 synchronized 相同的並發性和內存語義, 但是添加了類似鎖投票、定時鎖等候和可中斷鎖等候的一些特性。此外,它還提供了在激烈爭用情況下更佳的性能。

  • 注意:如果同步代碼有異常,要將unlock()寫入finally語句塊

 8.5 Lock和synchronized的區別

  •  Lock是顯式鎖(手動開啟和關閉鎖,別忘記關閉鎖),synchronized是隱式鎖

  •  Lock只有代碼塊鎖,synchronized有代碼塊鎖和方法鎖

  •  使用Lock鎖,JVM將花費較少的時間來調度線程,性能更好。並且具有更好的擴展性(提供更多的子類)
 
  • 優先使用順序:
    • Lock----同步代碼塊(已經進入了方法體,分配了相應資源)----同步方法(在方法體之外)

 8.6 線程同步的優缺點

  線程同步的好處:

    • 解決了線程安全問題

  線程同步的缺點

    • 性能下降
    • 會帶來死鎖

  死鎖

    • 當兩個線程相互等待對方釋放“鎖”時就會發生死鎖
    • 出現死鎖后, 不會出現異常,不會出現提示,只是所有的線程都處於阻塞狀態,無法繼續
    • 多線程編程時應該注意避免死鎖的發生

 九、volatile 關鍵字的作用(變量可見性、禁止重排序)

  Java 語言提供了一種稍弱的同步機制,即 volatile 變量,用來確保將 變量的更新操作通知到其他線程。volatile 變量具備兩種特性,volatile 變量不會被緩存在寄存器或者對其他處理器不可見的地方,因此在 讀取 volatile 類型的變量時總會返回最新寫入的值。

變量可見性:其一是保證該變量對所有線程可見,這里的可見性指的是當一個線程修改了變量的值,那么新的值對於其他線程是可以立即獲取的。

禁止重排序:volatile 禁止了指令重排。

 9.1 是比 sychronized 更輕量級的同步鎖

  在訪問 volatile 變量時 不會執行加鎖操作,因此也就不會使執行線程阻塞,因此 volatile 變量是一種比 sychronized 關鍵字更輕量級的同步機制。 volatile 適合這種場景:一個變量被多個線程共
享,線程直接給這個變量賦值。

  當對非 volatile 變量進行讀寫的時候,每個線程先從內存拷貝變量到 CPU 緩存中。如果計算機有多個 CPU,每個線程可能在不同的 CPU 上被處理,這意味着每個線程可以拷貝到不同的 CPUcache 中。而聲明變量是 volatile 的,JVM 保證了每次讀變量都從內存中讀,跳過 CPU cache這一步。

 9.2 適用場景

  值得說明的是 對 volatile 變量的單次讀/寫操作可以保證原子性的,如 long 和 double 類型變量, 但是並不能保證 i++這種操作的原子性,因為本質上 i++是讀、寫兩次操作。在某些場景下可以代替 Synchronized。但是,volatile 的不能完全取代 Synchronized 的位置,只有在一些特殊的場景下,才能適用 volatile。總的來說,必須同時滿足下面兩個條件才能保證在並發環境的線程安全:

  (1)對變量的寫操作不依賴於當前值(比如 i++),或者說是單純的變量賦值(boolean flag = true)。

  (2)該變量沒有包含在具有其他變量的不變式中,也就是說,不同的 volatile 變量之間,不能互相依賴。只有在狀態真正獨立於程序內其他內容時才能使用 volatile。

十、線程通信

 10.1 Java提供了3個方法解決線程之間的通信問題

 

   均是java.lang.Object類的方法都只能在同步方法或者同步代碼塊中使用,否則會拋出異常

 10.2 兩個線程之間共享數據

  Java 里面進行多線程通信的主要方式就是 共享內存的方式,共享內存主要的關注點有兩個: 可見性和有序性原子性。Java 內存模型(JMM)解決了可見性和有序性的問題,而鎖解決了原子性的
問題,理想情況下我們希望做到“同步”和“互斥”。有以下常規實現方法:
  將數據抽象成一個類,並將數據的操作作為這個類的方法:將數據抽象成一個類,並將對這個數據的操作作為這個類的方法,這么設計可以和容易做到同步,只要在方法上加”synchronized“
public class MyData {
    private int j=0;
    public synchronized void add(){
        j++;
        System.out.println("線程"+Thread.currentThread().getName()+"j 為:"+j);
    }
    public synchronized void dec(){
        j--;
        System.out.println("線程"+Thread.currentThread().getName()+"j 為:"+j);
    }
    public int getData(){
        return j;
    }     
}
public class AddRunnable implements Runnable{
    MyData data;
    public AddRunnable(MyData data){
        this.data= data;
    }
    public void run() {
        data.add();
    } 
}
public class DecRunnable implements Runnable {
    MyData data;
    public DecRunnable(MyData data){
        this.data = data;
    }
    public void run() {
        data.dec();
    } 
}
public static void main(String[] args) {
    MyData data = new MyData();
    Runnable add = new AddRunnable(data);
    Runnable dec = new DecRunnable(data);
    for(int i=0;i<2;i++){
        new Thread(add).start();
        new Thread(dec).start();
    }    
}

  Runnable 對象作為一個類的內部類:將 Runnable 對象作為一個類的內部類,共享數據作為這個類的成員變量,每個線程對共享數據的操作方法也封裝在外部類,以便實現對數據的各個操作的同步和互斥,作為內部類的各個 Runnable 對象調用外部類的這些方法。

public class MyData {
    private int j=0;
    public synchronized void add(){
        j++;
        System.out.println("線程"+Thread.currentThread().getName()+"j 為:"+j);
    }
    public synchronized void dec(){
        j--;
        System.out.println("線程"+Thread.currentThread().getName()+"j 為:"+j);
    }
    public int getData(){
        return j;
    } 
}
public class TestThread {
    public static void main(String[] args) {
        final MyData data = new MyData();
        for(int i=0;i<2;i++){
            new Thread(new Runnable(){
                public void run() {
                    data.add();
                }
            }).start();
            new Thread(new Runnable(){
                public void run() {
                    data.dec(); 
                }
            }).start();
        }
    } 
}

十一、ThreadLocal 作用(線程本地存儲)

  ThreadLocal,很多地方叫做線程本地變量,也有些地方叫做線程本地存儲,ThreadLocal 的作用是提供線程內的局部變量,這種變量在線程的生命周期內起作用,減少同一個線程內多個函數或者組件之間一些公共變量的傳遞的復雜度。

ThreadLocalMap(線程的一個屬性):

  1. 每個線程中都有一個自己的 ThreadLocalMap 類對象,可以將線程自己的對象保持到其中,各管各的,線程可以正確的訪問到自己的對象。

  2. 將一個共用的 ThreadLocal 靜態實例作為 key,將不同對象的引用保存到不同線程的ThreadLocalMap 中,然后在線程執行的各處通過這個靜態 ThreadLocal 實例的 get()方法取得自己線程保存的那個對象,避免了將這個對象作為參數傳遞的麻煩。

  3. ThreadLocalMap 其實就是線程里面的一個屬性,它在 Thread 類中定義ThreadLocal.ThreadLocalMap threadLocals = null;

 使用場景:最常見的 ThreadLocal 使用場景為 用來解決 數據庫連接、Session 管理等。

private static final ThreadLocal threadSession = new ThreadLocal(); 
public static Session getSession() throws InfrastructureException { 
    Session s = (Session) threadSession.get(); 
    try { 
        if (s == null) { 
            s = getSessionFactory().openSession(); 
            threadSession.set(s); 
        } 
    } catch (HibernateException ex) { 
        throw new InfrastructureException(ex); 
    } 
    return s; 
}

十二、Java 中用到的線程調度 

 12.1 搶占式調度:

  搶占式調度指的是每條線程執行的時間、線程的切換都由系統控制,系統控制指的是在系統某種運行機制下,可能每條線程都分同樣的執行時間片,也可能是某些線程執行的時間片較長,甚至某些線程得不到執行的時間片。在這種機制下,一個線程的堵塞不會導致整個進程堵塞。 

 12.2 協同式調度

  協同式調度指某一線程執行完后主動通知系統切換到另一線程上執行,這種模式就像接力賽一樣,一個人跑完自己的路程就把接力棒交接給下一個人,下個人繼續往下跑。線程的執行時間由線程本身控制,線程切換可以預知,不存在多線程同步問題,但它有一個致命弱點:如果一個線程編寫有問題,運行到一半就一直堵塞,那么可能導致整個系統崩潰。

 12.3 JVM 的線程調度實現(搶占式調度) 

  java 線程調度使用搶占式調度,Java 中線程會按優先級分配 CPU 時間片運行,且優先級越高越優先執行,但優先級高並不代表能獨自占用執行時間片,可能是優先級高得到越多的執行時間片,反之,優先級低的分到的執行時間少但不會分配不到執行時間。 

 12.4 線程讓出 cpu 的情況

  1. 當前運行線程主動放棄 CPU,JVM 暫時放棄 CPU 操作(基於時間片輪轉調度的 JVM 操作系統不會讓線程永久放棄 CPU,或者說放棄本次時間片的執行權),例如調用 yield()方法。
  2. 當前運行線程因為某些原因進入阻塞狀態,例如阻塞在 I/O 上。
  3. 當前運行線程結束,即運行完 run()方法里面的任務。 

十三、進程調度算法

 13.1 優先調度算法

  1. 先來先服務調度算法(FCFS):當在作業調度中采用該算法時,每次調度都是從后備作業隊列中選擇一個或多個最先進入該隊列的作業,將它們調入內存,為它們分配資源、創建進程,然后放入就緒隊列。在進程調度中采用 FCFS 算法時,則每次調度是從就緒隊列中選擇一個最先進入該隊列的進程,為之分配處理機,使之投入運行。該進程一直運行到完成或發生某事件而阻塞后才放棄處理機,特點是:算法比較簡單,可以實現基本上的公平。

   2. 短作業(進程)優先調度算法:短作業優先(SJF)的調度算法是從后備隊列中選擇一個或若干個估計運行時間最短的作業,將它們調入內存運行。而短進程優先(SPF)調度算法則是從就緒隊列中選出一個估計運行時間最短的進程,將處理機分配給它,使它立即執行並一直執行到完成,或發生某事件而被阻塞放棄處理機時再重新調度。該 算法未照顧緊迫型作業

 13.2 高優先權優先調度算法 

  為了照顧緊迫型作業,使之在進入系統后便獲得優先處理,引入了最高優先權優先(FPF)調度算法。當把該算法用於作業調度時,系統將從后備隊列中選擇若干個優先權最高的作業裝入內存。當用於進程調度時,該 算法是把處理機分配給就緒隊列中優先權最高的進程。 

  1. 非搶占式優先權算法:在這種方式下,系統一旦把處理機分配給就緒隊列中優先權最高的進程后,該進程便一直執行下去,直至完成;或因發生某事件使該進程放棄處理機時。這種調度算法主要用於批處理系統中;也可用於某些對實時性要求不嚴的實時系統中。 

   2. 搶占式優先權調度算法:在這種方式下,系統同樣是把處理機分配給優先權最高的進程,使之執行。但在其執行期間,只要又出現了另一個其優先權更高的進程,進程調度程序就立即停止當前進程(原優先權最高的進程)的執行,重新將處理機分配給新到的優先權最高的進程。顯然,這種搶占式的優先權調度算法能更好地滿足緊迫作業的要求,故而常用於要求比較嚴格的實時系統中,以及對性能要求較高的批
處理和分時系統中。

 13.3.高響應比優先調度算法

  在批處理系統中,短作業優先算法是一種比較好的算法,其主 要的不足之處是長作業的運行得不到保證。如果我們能為每個作業引入前面所述的動態優先權,並使作業的優先級隨着等待時間的增加而以速率 a 提高,則長作業在等待一定的時間后,必然有機會分配到處理機。該優先權的變化規律可描述為: 

  (1) 如果作業的等待時間相同,則要求服務的時間愈短,其優先權愈高,因而該算法有利於短作業。

  (2) 當要求服務的時間相同時,作業的優先權決定於其等待時間,等待時間愈長,其優先權愈高,因而它實現的是先來先服務。

  (3) 對於長作業,作業的優先級可以隨等待時間的增加而提高,當其等待時間足夠長時,其優先級便可升到很高,從而也可獲得處理機。簡言之,該算法既照顧了短作業,又考慮了作業到達的先后次序,不會使長作業長期得不到服務。因此,該算法實現了一種較好的折衷。當然,在利用該算法時,每要進行調度之前,都須先做響應比的計算,這會增加系統開銷。

 13.4 基於時間片的輪轉調度算法

   1. 時間片輪轉法:在早期的時間片輪轉法中,系統將所有的就緒進程按先來先服務的原則排成一個隊列,每次調度時,把 CPU 分配給隊首進程,並令其執行一個時間片。時間片的大小從幾 ms 到幾百 ms。當執行的時間片用完時,由一個計時器發出時鍾中斷請求,調度程序便據此信號來停止該進程的執行,並將它送往就緒隊列的末尾;然后,再把處理機分配給就緒隊列中新的隊首進程,同時也讓它執行一個時間片。這樣就可以保證就緒隊列中的所有進程在一給定的時間內均能獲得一時間片的處理機執行時間。 

  2. 多級反饋隊列調度算法:

   (1) 應設置多個就緒隊列,並為各個隊列賦予不同的優先級。第一個隊列的優先級最高,第二個隊列次之,其余各隊列的優先權逐個降低。該算法賦予各個隊列中進程執行時間片的大小也各不相同,在優先權愈高的隊列中,為每個進程所規定的執行時間片就愈小。例如,第二個隊列的時間片要比第一個隊列的時間片長一倍,……,第 i+1 個隊列的時間片要比第 i 個隊列的時間片長一倍。

   (2) 當一個新進程進入內存后,首先將它放入第一隊列的末尾,按 FCFS 原則排隊等待調度。當輪到該進程執行時,如它能在該時間片內完成,便可准備撤離系統;如果它在一個時間片結束時尚未完成,調度程序便將該進程轉入第二隊列的末尾,再同樣地按 FCFS 原則等待調度執行;如果它在第二隊列中運行一個時間片后仍未完成,再依次將它放入第三隊列,……,如此下去,當一個長作業(進程)從第一隊列依次降到第 n 隊列后,在第 n 隊列便采取按時間片輪轉的方式運行。

   (3) 僅當第一隊列空閑時,調度程序才調度第二隊列中的進程運行;僅當第 1~(i-1)隊列均空時,才會調度第 i 隊列中的進程運行。如果處理機正在第 i 隊列中為某進程服務時,又有新進程進入優先權較高的隊列(第 1~(i-1)中的任何一個隊列),則此時新進程將搶占正在運行進程的處理機,即由調度程序把正在運行的進程放回到第 i 隊列的末尾,把處理機分配給新到的高優先權進程。在多級反饋隊列調度算法中,如果規定第一個隊列的時間片略大於多數人機交互所需之處理時間時,便能夠較好的滿足各種類型用戶的需要。 

 


免責聲明!

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



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