10.執行緒

可乐爱兑姜汁發表於2024-04-14

第十章【執行緒】

一、程序和執行緒

1、程序:

代表了記憶體中正在執行的應用程式,計算機中的資源(cpu 記憶體 磁碟 網路等),會按照需求分配給每個程序,從而這個程序對應的應用程式就可以使用這些資源了。程序就是在系統中執行一個應用程式的基本單位。

2、執行緒:

是程序中的一個程式碼執行單元,負責當前程序中程式碼程式的執行,一個程序中有一個或多個執行緒。當一個程序中啟動了多個執行緒去分別執行程式碼的時候,這個程式就是多執行緒程式。

image-20210303143648761

​ 透過JDK自帶的jconsole工具,可以檢測到當前執行Hello這個類的時候,JVM的執行情況,包含記憶體的使用、執行緒的執行狀態、類的載入等資訊

二、併發和並行

1、併發:

是指在一個時間段內,倆個或多個執行緒,使用一個CPU,進行交替執行。

2、並行:

是指在同一時刻,倆個或多個執行緒,各自使用一個CPU,同時進行執行。

三、時間片

1、概念

時間片,當前一個執行緒要使用CPU的時候,CPU會分配給這個執行緒一小段時間(毫秒級別),這段時間就叫做時間片,也就是該執行緒允許使用CPU執行的時間,在這個期間,執行緒擁有CPU的使用權。

2、排程

當倆個或多個執行緒使用一個CPU來執行程式碼的時候,在作業系統的核心中,就會有相應的演算法來控制執行緒獲取CPU時間片的方式,從而使得這些執行緒可以按照某種順序來使用cpu執行程式碼,這種情況被稱為執行緒呼叫。

常見的排程方式:

  • 時間片輪轉

    所有執行緒輪流使用 CPU 的使用權,平均分配每個執行緒佔用 CPU 的時間。

  • 搶佔式排程

    系統會讓優先順序高的執行緒優先使用 CPU(提高搶佔到的機率),但是如果執行緒的優先順序相同,那麼會隨機選擇一個執行緒獲取當前CPU的時間片。 【JVM中的執行緒,使用的為搶佔式排程】

四、執行緒的相關操作

1、main執行緒

使用java命令來執行一個類的時候,首先會啟動JVM(程序),JVM會在建立一個名字叫做main的執行緒,來執行類中的程式入口(main方法),我們寫在main方法中的程式碼,其實都是由名字叫做main的執行緒去執行的。

Thread.currentThread(); 可以寫在任意方法中,返回就是執行這個方法的執行緒物件

image-20210303144713963

2、執行緒的建立和啟動
  1. Java中透過繼承Thread類來建立並啟動一個新的執行緒
  • 定義 Thread 類的子類(可以是匿名內部類),並重寫 Thread 類中的 run 方法, run 方法中的程式碼就是執行緒的執行任務

  • 建立 Thread 子類的物件,這個物件就代表了一個要獨立執行的新執行緒

  • 呼叫執行緒物件的 start 方法來啟動該執行緒

    class MyThread extends Thread{
        @Override 
        public void run() {
            for (int i = 0; i < 10; i++) { 
                System.out.println("hello world"); 
                try {
                    Thread.sleep(1000); 
                } catch (InterruptedException e) { 
                    e.printStackTrace(); 
                } 
            } 
        } 
    }
    
    public static void main(String[] args) { 
    	Thread t = new MyThread(); 
        t.start();
    }
    
    匿名內部類方法
    public static void main(String[] args) { 
        Thread t = new MyThread(){ 
            @Override 
            public void run() {
                for (int i = 0; i < 10; i++) { 
                    System.out.println("hello world"); 
                    try {
                        Thread.sleep(1000); 
                    } catch (InterruptedException e) { 
                        e.printStackTrace(); 
                    } 
                } 
            } 
        };
        t.start();
    }
    
  1. 利用Runnable介面來完成執行緒建立和啟動【推薦】

    public class Thread implements Runnable { 
        private Runnable target;
        public Thread() {
            //... 
        }
        public Thread(Runnable target) { 
            this.target = target; 
            //.. 
        }
        @Override 
        public void run() { 
            if (target != null) { 
                target.run(); 
            } 
        } 
    }
    
    使用 Runnable 介面的匿名內部類,來指定執行緒的執行任務(重寫介面中的run方法)
    public static void main(String[] args) { 
        Runnable run = new Runnable() { 
            @Override 
            public void run() {
    			for (int i = 0; i < 10; i++) { 
                    System.out.println("hello world"); 
                    try {
                        Thread.sleep(1000); 
                    } catch (InterruptedException e) { 
                        e.printStackTrace(); 
                    } 
                } 
            } 
        };
    	Thread t = new Thread(run); 
        t.start(); 
    }
    
3、執行緒的名字

​ 透過Thread類中的currentThread方法,可以獲取當前執行緒的物件,然後呼叫執行緒物件的getName方法,可以獲取當前執行緒的名字。String name = Thread.currentThread().getName();

​ 主執行緒中,建立出的執行緒,它們的都會有一個預設的名字:Thread-" + nextThreadNum()

​ 可以建立執行緒物件的時候,給它設定一個指定的名字:

//方法一:
Thread t = new Thread("t執行緒"); 
//方法二:
Thread t = new Thread(new Runnable(){ 
    @Override
    public void run(){ 
        //執行任務 
    } 
},"t執行緒"); 
//方法三:
Thread t = new Thread(); 
t.setName("t執行緒");
4、執行緒的分類
  • 前臺執行緒,又叫做執行執行緒、使用者執行緒
  • 後臺執行緒,又叫做守護執行緒、精靈執行緒

前臺執行緒:

​ 這種執行緒專門用來執行使用者編寫的程式碼,地位比較高,JVM是否會停止執行,就是要看當前是否還有前臺執行緒沒有執行完,如果還剩下任意一個前臺執行緒沒有“死亡”,那麼JVM就不能停止!

後臺執行緒:

​ 這種執行緒是用來給前臺執行緒服務的,給前臺執行緒提供一個良好的執行環境,地位比較低,JVM是否停止執行,根本不關心後臺執行緒的執行情況和狀態。

在主執行緒中,建立出來的執行緒物件,預設就是前臺執行緒,在它啟動之前,我們還可以給它設定為後臺執行緒:

public static void main(String[] args) { 
    Thread t = new Thread("t執行緒"){ 
        @Override 
        public void run() { 
            String name = Thread.currentThread().getName();
				for (int i = 0; i < 10; i++) { 
                    System.out.println(name+": hello "+i); 
                } 
        } 
    };
    //在啟動執行緒之前,可以將其設定為後臺執行緒,否則預設是前臺執行緒 
    t.setDaemon(true); 
    t.start(); 
}
5、執行緒的優先順序

執行緒的優先順序使用int型別數字表示,最大是10,最小是1,預設的優先順序是5。

最終設定執行緒優先順序的方法,是一個native方法,並不是java語言實現的。

當倆個執行緒爭奪CPU時間片的時候:

  • 優先順序相同,獲得CPU使用權的機率相同
  • 優先順序不同,那麼高優先順序的執行緒有更高的機率獲取到CPU的使用權
6、執行緒組

Java中使用java.lang.ThreadGroup類來表示執行緒組,它可以對一批執行緒進行管理,對執行緒組進行操作,同時也會對執行緒組裡面的這一批執行緒操作。

	@Test
	public void testThreadGroup1() {
		/*
		 * JVM會構建一個預設的執行緒組,管理JVM構建的main執行緒組
		 */
		Thread currentThread = Thread.currentThread();
		ThreadGroup threadGroup = currentThread.getThreadGroup();
		System.out.println(threadGroup);
		//建立一個新執行緒,沒有放到執行緒組
		Thread th = new Thread("myThread");
		System.out.println(th.getThreadGroup());
		
		//自己構建一個執行緒組
		ThreadGroup tg = new ThreadGroup("myThreadGroup");
		tg.setMaxPriority(9);
		Thread th1 = new Thread(tg,"執行緒2");
		System.out.println(th1.getThreadGroup());
	}	
@Test
	public void testThreadGroup2() {
		/*
		 * 執行緒放線上程組,可以檢視和管理執行緒組中的執行緒
		 */
		Runnable run = new Runnable() {
			@Override
			public void run() {
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		};
		
		ThreadGroup group = new ThreadGroup("myThreadGroup");
		
		Thread t1 = new Thread(group,run,"執行緒1");
		Thread t2 = new Thread(group,run,"執行緒2");
		Thread t3 = new Thread(group,run,"執行緒3");
		
		t1.start();
		t2.start();
		t3.start();
		
		System.out.println("執行緒組中還在存活的執行緒個數為:"+group.activeCount());
		
		Thread[] arr = new Thread[group.activeCount()];
		System.out.println("arr陣列中存放的執行緒個數為:"+group.enumerate(arr));
		System.out.println(Arrays.toString(arr));
	}
7、執行緒狀態

java.lang.Thread.State列舉型別中(內部類形式),定義了執行緒的幾種狀態:

執行緒狀態 名稱 描述
NEW 新建 執行緒剛被建立,還沒呼叫start方法,或者剛剛呼叫了start方法,呼叫start方法不一定"立即"改變執行緒狀態,中間可能需要一些步驟才完成一個執行緒的啟動。
RUNNABLE 可執行 start方法呼叫結束,執行緒由NEW變成RUNNABLE,執行緒存活著,並嘗試搶佔CPU資源,或者已經搶佔到CPU資源正在執行,這倆種情況的狀態都顯示為RUNNABLE
BLOCKED 鎖阻塞 執行緒A和執行緒B都要執行方法test,而且方法test被加了鎖,執行緒A先拿到了鎖去執行test方法,執行緒B這時候需要等待執行緒A把鎖釋放。這時候執行緒B就是處理BLOCKED
WAITING 無限期等待 一個執行緒在等待另一個執行緒執行一個(喚醒)動作時,該執行緒進入Waiting狀態。進入這個狀態後是不能自動喚醒的,必須等待另一個執行緒呼叫notify或者notifyAll方法才能夠喚醒。
TIMED_WAITING 有限期等待 和WAITING狀態類似,但是有一個時間期限,時間到了,自己也會主動醒來
TERMINATED 終止(死亡) run方法執行結束的執行緒處於這種狀態。

執行緒在BLOCKED,WAITING,TIMED_WAITING這三種情況的阻塞下,都具備相同的特點:

  • 執行緒不執行程式碼

  • 執行緒也不參與CPU時間片的爭奪

image-20210304094955171

注意:

  1. 剛建立好的執行緒物件,就是出於NEW的狀態

  2. 執行緒啟動後,會出於RUNNABLE狀態,從就緒狀態到執行狀態,之間會經過多次反覆的CPU執行權的爭奪

    RUNNABLE狀態包含倆種情況:

    • 就緒狀態:此時這個執行緒沒有執行,因為沒有搶到CPU的執行權
    • 執行狀態:此時這個執行緒正在執行,因為搶到CPU的執行權
  3. 執行緒執行了sleep(long million)方法後,會從RUNNABLE狀態進入到TIMED_WAITING狀態

    TIMED_WAITING狀態阻塞結束後,執行緒會自動回到RUNNABLE狀態

  4. 執行緒執行了join()方法後,會從RUNNABLE狀態進入到WAITING狀態,呼叫執行緒的interrupt()方法,可以從WAITING狀態進入到RUNNABLE狀態

    執行緒執行了join(long million)方法後,會從RUNNABLE狀態進入到TIMED_WAITING狀態

  5. interrupt()方法是透過改變執行緒物件中的一個標識的值(true|false),來達到打斷阻塞狀態的效果

    檢視就緒/執行狀態下的執行緒物件中“打斷標識”值的倆個方法:

    1. isInterrupted()方法

      呼叫了interrupt()方法後,執行緒中的“打斷標識”值設定為true,可以透過執行緒物件中的isInterrupted方法返回這個標識的值,並且不會修改這個值,所以輸出顯示的一直是ture。

    2. interrupted()方法

      呼叫了interrupt()方法後,執行緒中的“打斷標識”值設定為true,可以透過執行緒物件中的interrupted方法,第一次返回true之後,後面在呼叫方法檢視這個“打斷標識”值,都是false。interrupted返回true後,會直接把這個值給清除掉。

五、執行緒安全

​ 當多個執行緒同時共享,同一個全域性變數或靜態變數,做寫的操作時,可能會發生資料衝突問題,也就是執行緒安全問題。但是做讀操作是不會發生資料衝突問題。

六、執行緒同步

Java中實現執行緒同步的方式,是給需要同步的程式碼進行synchronized關鍵字加鎖。

七、Synchronized

使用格式為:

synchronized (鎖物件){ 
    //操作共享變數的程式碼,這些程式碼需要執行緒同步,否則會有執行緒安全問題 
    //... 
}

image-20210305142202132

八、wait和notify

Object類中有三個方法: wait()notify()notifyAll()

三個核心點:

  • 任何物件中都一定有這三個方法

  • 只有物件作為鎖物件的時候,才可以呼叫

  • 只有在同步的程式碼塊中,才可以呼叫

​ 當前呼叫鎖物件的wait方法後,當前執行緒釋放鎖,然後進入到阻塞狀態,並且等待其他執行緒先喚醒自己,如果沒有其他執行緒喚醒自己,那麼就一直等著。所以現在的情況是,倆個執行緒t1和t2都是在處於阻塞狀態,等待別人喚醒自己,所以程式不執行了,但是也沒結束!

​ 如果有執行緒呼叫了notify()方法進行了喚醒,或者interrupt方法進行了打斷,那麼這個執行緒就會從等待池進入到鎖池,而進入到鎖池的執行緒,會時刻關注鎖物件是否可用,一旦可用,這個執行緒就會立刻自動恢復到RUNNABLE狀態。

​ 鎖物件.notify(),該方法可以在等待池中,隨機喚醒一個等待指定鎖物件的執行緒,使得這個執行緒進入到鎖池中,而進入到鎖池的執行緒, 一旦發現鎖可用,就可以自動恢復到RUNNABLE狀態了

​ 鎖物件.notifyAll(),該方法可以在等待池中,喚醒所有等待指定鎖物件的執行緒,使得這個執行緒進入到鎖池中,而進入到鎖池的執行緒, 一旦發現鎖可用,就可以自動恢復到RUNNABLE狀態了

image-20210305142851687

由圖可知,TIMED_WAITINGWAITINGBLOCKED都屬於執行緒阻塞,他們共同的特點是就是執行緒不執行程式碼,也不參與CPU的爭奪,除此之外,它們還有各自的特點:(重要)

  • 阻塞1,執行緒執行時,呼叫sleep或者join方法後,進入這種阻塞,該阻塞狀態可以恢復到RUNNABLE狀態,條件是執行緒被打斷了、或者指定的時間到了,或者join的執行緒結束了

  • 阻塞2,執行緒執行時,發現鎖不可用後,進入這種阻塞,該阻塞狀態可以恢復到RUNNABLE狀態,條件是執行緒需要爭奪的鎖物件變為可用了(別的執行緒把鎖釋放了)

  • 阻塞3,執行緒執行時,呼叫了wait方法後,執行緒先釋放鎖後,再進入這種阻塞,該阻塞狀態可以恢復到BLOCKED狀態(也就是阻塞2的情況),條件是執行緒被打斷了、或者是被別的執行緒喚醒了(notify方法)

九、死鎖

在程式中要儘量避免出現死鎖情況,一旦發生那麼只能手動停止JVM的執行,然後查詢並修改產生死鎖的問題程式碼

​ 簡單的描述死鎖就是:倆個執行緒t1和t2,t1拿著t2需要等待的鎖不釋放,而t2又拿著t1需要等待的鎖不釋放,倆個執行緒就這樣一直僵持下去。

相關文章