Java 多執行緒乾貨系列—(一)Java 多執行緒基礎 | 掘金技術徵文

嘟嘟MD發表於2017-05-01

原本地址:Java多執行緒乾貨系列—(一)Java多執行緒基礎
部落格地址:tengj.top/

前言

多執行緒併發程式設計是Java程式設計中重要的一塊內容,也是面試重點覆蓋區域,所以學好多執行緒併發程式設計對我們來說極其重要,下面跟我一起開啟本次的學習之旅吧。

正文

執行緒與程式

1 執行緒:程式中負責程式執行的執行單元
執行緒本身依靠程式進行執行
執行緒是程式中的順序控制流,只能使用分配給程式的資源和環境

2 程式:執行中的程式
一個程式至少包含一個執行緒

3 單執行緒:程式中只存在一個執行緒,實際上主方法就是一個主執行緒

4 多執行緒:在一個程式中執行多個任務
目的是更好地使用CPU資源

執行緒的實現

繼承Thread類

java.lang包中定義, 繼承Thread類必須重寫run()方法

class MyThread extends Thread{
    private static int num = 0;

    public MyThread(){
        num++;
    }

    @Override
    public void run() {
        System.out.println("主動建立的第"+num+"個執行緒");
    }
}複製程式碼

建立好了自己的執行緒類之後,就可以建立執行緒物件了,然後通過start()方法去啟動執行緒。注意,不是呼叫run()方法啟動執行緒,run方法中只是定義需要執行的任務,如果呼叫run方法,即相當於在主執行緒中執行run方法,跟普通的方法呼叫沒有任何區別,此時並不會建立一個新的執行緒來執行定義的任務。

public class Test {
    public static void main(String[] args)  {
        MyThread thread = new MyThread();
        thread.start();
    }
}
class MyThread extends Thread{
    private static int num = 0;
    public MyThread(){
        num++;
    }
    @Override
    public void run() {
        System.out.println("主動建立的第"+num+"個執行緒");
    }
}複製程式碼

在上面程式碼中,通過呼叫start()方法,就會建立一個新的執行緒了。為了分清start()方法呼叫和run()方法呼叫的區別,請看下面一個例子:

public class Test {
    public static void main(String[] args)  {
        System.out.println("主執行緒ID:"+Thread.currentThread().getId());
        MyThread thread1 = new MyThread("thread1");
        thread1.start();
        MyThread thread2 = new MyThread("thread2");
        thread2.run();
    }
}


class MyThread extends Thread{
    private String name;

    public MyThread(String name){
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println("name:"+name+" 子執行緒ID:"+Thread.currentThread().getId());
    }
}複製程式碼

執行結果:

Java 多執行緒乾貨系列—(一)Java 多執行緒基礎 | 掘金技術徵文

從輸出結果可以得出以下結論:

1)thread1和thread2的執行緒ID不同,thread2和主執行緒ID相同,說明通過run方法呼叫並不會建立新的執行緒,而是在主執行緒中直接執行run方法,跟普通的方法呼叫沒有任何區別;

2)雖然thread1的start方法呼叫在thread2的run方法前面呼叫,但是先輸出的是thread2的run方法呼叫的相關資訊,說明新執行緒建立的過程不會阻塞主執行緒的後續執行。

實現Runnable介面

在Java中建立執行緒除了繼承Thread類之外,還可以通過實現Runnable介面來實現類似的功能。實現Runnable介面必須重寫其run方法。
下面是一個例子:

public class Test {
    public static void main(String[] args)  {
        System.out.println("主執行緒ID:"+Thread.currentThread().getId());
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
    }
} 
class MyRunnable implements Runnable{
    public MyRunnable() {
    }

    @Override
    public void run() {
        System.out.println("子執行緒ID:"+Thread.currentThread().getId());
    }
}複製程式碼

Runnable的中文意思是“任務”,顧名思義,通過實現Runnable介面,我們定義了一個子任務,然後將子任務交由Thread去執行。注意,這種方式必須將Runnable作為Thread類的引數,然後通過Thread的start方法來建立一個新執行緒來執行該子任務。如果呼叫Runnable的run方法的話,是不會建立新執行緒的,這根普通的方法呼叫沒有任何區別。

事實上,檢視Thread類的實現原始碼會發現Thread類是實現了Runnable介面的。

在Java中,這2種方式都可以用來建立執行緒去執行子任務,具體選擇哪一種方式要看自己的需求。直接繼承Thread類的話,可能比實現Runnable介面看起來更加簡潔,但是由於Java只允許單繼承,所以如果自定義類需要繼承其他類,則只能選擇實現Runnable介面。

使用ExecutorService、Callable、Future實現有返回結果的多執行緒

多執行緒後續會學到,這裡暫時先知道一下有這種方法即可。

ExecutorService、Callable、Future這個物件實際上都是屬於Executor框架中的功能類。想要詳細瞭解Executor框架的可以訪問www.javaeye.com/topic/36659… ,這裡面對該框架做了很詳細的解釋。返回結果的執行緒是在JDK1.5中引入的新特徵,確實很實用,有了這種特徵我就不需要再為了得到返回值而大費周折了,而且即便實現了也可能漏洞百出。

可返回值的任務必須實現Callable介面,類似的,無返回值的任務必須Runnable介面。執行Callable任務後,可以獲取一個Future的物件,在該物件上呼叫get就可以獲取到Callable任務返回的Object了,再結合執行緒池介面ExecutorService就可以實現傳說中有返回結果的多執行緒了。下面提供了一個完整的有返回結果的多執行緒測試例子,在JDK1.5下驗證過沒問題可以直接使用。程式碼如下:

/**
* 有返回值的執行緒 
*/  
@SuppressWarnings("unchecked")  
public class Test {  
public static void main(String[] args) throws ExecutionException,  
    InterruptedException {  
   System.out.println("----程式開始執行----");  
   Date date1 = new Date();  

   int taskSize = 5;  
   // 建立一個執行緒池  
   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);  
    // System.out.println(">>>" + f.get().toString());  
    list.add(f);  
   }  
   // 關閉執行緒池  
   pool.shutdown();  

   // 獲取所有併發任務的執行結果  
   for (Future f : list) {  
    // 從Future物件上獲取任務的返回值,並輸出到控制檯  
    System.out.println(">>>" + f.get().toString());  
   }  

   Date date2 = new Date();  
   System.out.println("----程式結束執行----,程式執行時間【"  
     + (date2.getTime() - date1.getTime()) + "毫秒】");  
}  
}  

class MyCallable implements Callable<Object> {  
private String taskNum;  

MyCallable(String taskNum) {  
   this.taskNum = taskNum;  
}  

public Object call() throws Exception {  
   System.out.println(">>>" + taskNum + "任務啟動");  
   Date dateTmp1 = new Date();  
   Thread.sleep(1000);  
   Date dateTmp2 = new Date();  
   long time = dateTmp2.getTime() - dateTmp1.getTime();  
   System.out.println(">>>" + taskNum + "任務終止");  
   return taskNum + "任務返回執行結果,當前任務時間【" + time + "毫秒】";  
}
}複製程式碼

程式碼說明:
上述程式碼中Executors類,提供了一系列工廠方法用於創先執行緒池,返回的執行緒池都實現了ExecutorService介面。
public static ExecutorService newFixedThreadPool(int nThreads)
建立固定數目執行緒的執行緒池。

public static ExecutorService newCachedThreadPool()
建立一個可快取的執行緒池,呼叫execute 將重用以前構造的執行緒(如果執行緒可用)。如果現有執行緒沒有可用的,則建立一個新執行緒並新增到池中。終止並從快取中移除那些已有 60 秒鐘未被使用的執行緒。

public static ExecutorService newSingleThreadExecutor()
建立一個單執行緒化的Executor。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
建立一個支援定時及週期性的任務執行的執行緒池,多數情況下可用來替代Timer類。

ExecutoreService提供了submit()方法,傳遞一個Callable,或Runnable,返回Future。如果Executor後臺執行緒池還沒有完成Callable的計算,這呼叫返回Future物件的get()方法,會阻塞直到計算完成。

執行緒的狀態

在正式學習Thread類中的具體方法之前,我們先來了解一下執行緒有哪些狀態,這個將會有助於後面對Thread類中的方法的理解。

  • 建立(new)狀態: 準備好了一個多執行緒的物件
  • 就緒(runnable)狀態: 呼叫了start()方法, 等待CPU進行排程
  • 執行(running)狀態: 執行run()方法
  • 阻塞(blocked)狀態: 暫時停止執行, 可能將資源交給其它執行緒使用
  • 終止(dead)狀態: 執行緒銷燬

當需要新起一個執行緒來執行某個子任務時,就建立了一個執行緒。但是執行緒建立之後,不會立即進入就緒狀態,因為執行緒的執行需要一些條件(比如記憶體資源,在前面的JVM記憶體區域劃分一篇博文中知道程式計數器、Java棧、本地方法棧都是執行緒私有的,所以需要為執行緒分配一定的記憶體空間),只有執行緒執行需要的所有條件滿足了,才進入就緒狀態。

當執行緒進入就緒狀態後,不代表立刻就能獲取CPU執行時間,也許此時CPU正在執行其他的事情,因此它要等待。當得到CPU執行時間之後,執行緒便真正進入執行狀態。

執行緒在執行狀態過程中,可能有多個原因導致當前執行緒不繼續執行下去,比如使用者主動讓執行緒睡眠(睡眠一定的時間之後再重新執行)、使用者主動讓執行緒等待,或者被同步塊給阻塞,此時就對應著多個狀態:time waiting(睡眠或等待一定的事件)、waiting(等待被喚醒)、blocked(阻塞)。

當由於突然中斷或者子任務執行完畢,執行緒就會被消亡。

下面這副圖描述了執行緒從建立到消亡之間的狀態:

Java 多執行緒乾貨系列—(一)Java 多執行緒基礎 | 掘金技術徵文

在有些教程上將blocked、waiting、time waiting統稱為阻塞狀態,這個也是可以的,只不過這裡我想將執行緒的狀態和Java中的方法呼叫聯絡起來,所以將waiting和time waiting兩個狀態分離出來。

注:sleep和wait的區別:

  • sleepThread類的方法,waitObject類中定義的方法.
  • Thread.sleep不會導致鎖行為的改變, 如果當前執行緒是擁有鎖的, 那麼Thread.sleep不會讓執行緒釋放鎖.
  • Thread.sleepObject.wait都會暫停當前的執行緒. OS會將執行時間分配給其它執行緒. 區別是, 呼叫wait後, 需要別的執行緒執行notify/notifyAll才能夠重新獲得CPU執行時間.

上下文切換

對於單核CPU來說(對於多核CPU,此處就理解為一個核),CPU在一個時刻只能執行一個執行緒,當在執行一個執行緒的過程中轉去執行另外一個執行緒,這個叫做執行緒上下文切換(對於程式也是類似)。

由於可能當前執行緒的任務並沒有執行完畢,所以在切換時需要儲存執行緒的執行狀態,以便下次重新切換回來時能夠繼續切換之前的狀態執行。舉個簡單的例子:比如一個執行緒A正在讀取一個檔案的內容,正讀到檔案的一半,此時需要暫停執行緒A,轉去執行執行緒B,當再次切換回來執行執行緒A的時候,我們不希望執行緒A又從檔案的開頭來讀取。

因此需要記錄執行緒A的執行狀態,那麼會記錄哪些資料呢?因為下次恢復時需要知道在這之前當前執行緒已經執行到哪條指令了,所以需要記錄程式計數器的值,另外比如說執行緒正在進行某個計算的時候被掛起了,那麼下次繼續執行的時候需要知道之前掛起時變數的值時多少,因此需要記錄CPU暫存器的狀態。所以一般來說,執行緒上下文切換過程中會記錄程式計數器、CPU暫存器狀態等資料。

說簡單點的:對於執行緒的上下文切換實際上就是 儲存和恢復CPU狀態的過程,它使得執行緒執行能夠從中斷點恢復執行

雖然多執行緒可以使得任務執行的效率得到提升,但是由於線上程切換時同樣會帶來一定的開銷代價,並且多個執行緒會導致系統資源佔用的增加,所以在進行多執行緒程式設計時要注意這些因素。

執行緒的常用方法

編號 方法 說明
1 public void start() 使該執行緒開始執行;Java 虛擬機器呼叫該執行緒的 run 方法。
2 public void run() 如果該執行緒是使用獨立的 Runnable 執行物件構造的,則呼叫該 Runnable 物件的 run 方法;否則,該方法不執行任何操作並返回。
3 public final void setName(String name) 改變執行緒名稱,使之與引數 name 相同。
4 public final void setPriority(int priority) 更改執行緒的優先順序。
5 public final void setDaemon(boolean on) 將該執行緒標記為守護執行緒或使用者執行緒。
6 public final void join(long millisec) 等待該執行緒終止的時間最長為 millis 毫秒。
7 public void interrupt() 中斷執行緒。
8 public final boolean isAlive() 測試執行緒是否處於活動狀態。
9 public static void yield() 暫停當前正在執行的執行緒物件,並執行其他執行緒。
10 public static void sleep(long millisec) 在指定的毫秒數內讓當前正在執行的執行緒休眠(暫停執行),此操作受到系統計時器和排程程式精度和準確性的影響。
11 public static Thread currentThread() 返回對當前正在執行的執行緒物件的引用。

Java 多執行緒乾貨系列—(一)Java 多執行緒基礎 | 掘金技術徵文

靜態方法

currentThread()方法

currentThread()方法可以返回程式碼段正在被哪個執行緒呼叫的資訊。

public class Run1{
    public static void main(String[] args){                 
    System.out.println(Thread.currentThread().getName());
    }
}複製程式碼

sleep()方法

方法sleep()的作用是在指定的毫秒數內讓當前“正在執行的執行緒”休眠(暫停執行)。這個“正在執行的執行緒”是指this.currentThread()返回的執行緒。

sleep方法有兩個過載版本:

sleep(long millis)     //引數為毫秒
sleep(long millis,int nanoseconds)    //第一引數為毫秒,第二個引數為納秒複製程式碼

sleep相當於讓執行緒睡眠,交出CPU,讓CPU去執行其他的任務。
但是有一點要非常注意,sleep方法不會釋放鎖,也就是說如果當前執行緒持有對某個物件的鎖,則即使呼叫sleep方法,其他執行緒也無法訪問這個物件。看下面這個例子就清楚了:

public class Test {

    private int i = 10;
    private Object object = new Object();

    public static void main(String[] args) throws IOException  {
        Test test = new Test();
        MyThread thread1 = test.new MyThread();
        MyThread thread2 = test.new MyThread();
        thread1.start();
        thread2.start();
    } 


    class MyThread extends Thread{
        @Override
        public void run() {
            synchronized (object) {
                i++;
                System.out.println("i:"+i);
                try {
                    System.out.println("執行緒"+Thread.currentThread().getName()+"進入睡眠狀態");
                    Thread.currentThread().sleep(10000);
                } catch (InterruptedException e) {
                    // TODO: handle exception
                }
                System.out.println("執行緒"+Thread.currentThread().getName()+"睡眠結束");
                i++;
                System.out.println("i:"+i);
            }
        }
    }
}複製程式碼

輸出結果:

Java 多執行緒乾貨系列—(一)Java 多執行緒基礎 | 掘金技術徵文

從上面輸出結果可以看出,當Thread-0進入睡眠狀態之後,Thread-1並沒有去執行具體的任務。只有當Thread-0執行完之後,此時Thread-0釋放了物件鎖,Thread-1才開始執行。

注意,如果呼叫了sleep方法,必須捕獲InterruptedException異常或者將該異常向上層丟擲。當執行緒睡眠時間滿後,不一定會立即得到執行,因為此時可能CPU正在執行其他的任務。所以說呼叫sleep方法相當於讓執行緒進入阻塞狀態。

yield()方法

呼叫yield方法會讓當前執行緒交出CPU許可權,讓CPU去執行其他的執行緒。它跟sleep方法類似,同樣不會釋放鎖。但是yield不能控制具體的交出CPU的時間,另外,yield方法只能讓擁有相同優先順序的執行緒有獲取CPU執行時間的機會。

注意,呼叫yield方法並不會讓執行緒進入阻塞狀態,而是讓執行緒重回就緒狀態,它只需要等待重新獲取CPU執行時間,這一點是和sleep方法不一樣的。
程式碼:

public class MyThread  extends Thread{
    @Override
    public void run() {
        long beginTime=System.currentTimeMillis();
        int count=0;
        for (int i=0;i<50000000;i++){
            count=count+(i+1);
            //Thread.yield();
        }
        long endTime=System.currentTimeMillis();
        System.out.println("用時:"+(endTime-beginTime)+" 毫秒!");
    }
}

public class Run {
    public static void main(String[] args) {
        MyThread t= new MyThread();
        t.start();
    }
}複製程式碼

執行結果:

用時:3 毫秒!複製程式碼

如果將 //Thread.yield();的註釋去掉,執行結果如下:

用時:16080 毫秒!複製程式碼

物件方法

start()方法

start()用來啟動一個執行緒,當呼叫start方法後,系統才會開啟一個新的執行緒來執行使用者定義的子任務,在這個過程中,會為相應的執行緒分配需要的資源。

run()方法

run()方法是不需要使用者來呼叫的,當通過start方法啟動一個執行緒之後,當執行緒獲得了CPU執行時間,便進入run方法體去執行具體的任務。注意,繼承Thread類必須重寫run方法,在run方法中定義具體要執行的任務。

getId()

getId()的作用是取得執行緒的唯一標識
程式碼:

public class Test {
    public static void main(String[] args) {
        Thread t= Thread.currentThread();
        System.out.println(t.getName()+" "+t.getId());
    }
}複製程式碼

輸出:

main 1複製程式碼

isAlive()方法

方法isAlive()的功能是判斷當前執行緒是否處於活動狀態
程式碼:

public class MyThread  extends Thread{
    @Override
    public void run() {
        System.out.println("run="+this.isAlive());
    }
}
public class RunTest {
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread=new MyThread();
        System.out.println("begin =="+myThread.isAlive());
        myThread.start();
        System.out.println("end =="+myThread.isAlive());
    }
}複製程式碼

程式執行結果:

begin ==false
run=true
end ==false複製程式碼

方法isAlive()的作用是測試執行緒是否偶處於活動狀態。什麼是活動狀態呢?活動狀態就是執行緒已經啟動且尚未終止。執行緒處於正在執行或準備開始執行的狀態,就認為執行緒是“存活”的。
有個需要注意的地方

  System.out.println("end =="+myThread.isAlive());複製程式碼

雖然上面的例項中列印的值是true,但此值是不確定的。列印true值是因為myThread執行緒還未執行完畢,所以輸出true。如果程式碼改成下面這樣,加了個sleep休眠:

public static void main(String[] args) throws InterruptedException {
        MyThread myThread=new MyThread();
        System.out.println("begin =="+myThread.isAlive());
        myThread.start();
        Thread.sleep(1000);
        System.out.println("end =="+myThread.isAlive());
    }複製程式碼

則上述程式碼執行的結果輸出為false,因為mythread物件已經在1秒之內執行完畢。

join()方法

在很多情況下,主執行緒建立並啟動了執行緒,如果子執行緒中藥進行大量耗時運算,主執行緒往往將早於子執行緒結束之前結束。這時,如果主執行緒想等待子執行緒執行完成之後再結束,比如子執行緒處理一個資料,主執行緒要取得這個資料中的值,就要用到join()方法了。方法join()的作用是等待執行緒物件銷燬。

public class Thread4 extends Thread{
    public Thread4(String name) {
        super(name);
    }
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(getName() + "  " + i);
        }
    }
    public static void main(String[] args) throws InterruptedException {
        // 啟動子程式
        new Thread4("new thread").start();
        for (int i = 0; i < 10; i++) {
            if (i == 5) {
                Thread4 th = new Thread4("joined thread");
                th.start();
                th.join();
            }
            System.out.println(Thread.currentThread().getName() + "  " + i);
        }
    }
}複製程式碼

執行結果:

main  0
main  1
main  2
main  3
main  4
new thread  0
new thread  1
new thread  2
new thread  3
new thread  4
joined thread  0
joined thread  1
joined thread  2
joined thread  3
joined thread  4
main  5
main  6
main  7
main  8
main  9複製程式碼

由上可以看出main主執行緒等待joined thread執行緒先執行完了才結束的。如果把th.join()這行註釋掉,執行結果如下:

main  0
main  1
main  2
main  3
main  4
main  5
main  6
main  7
main  8
main  9
new thread  0
new thread  1
new thread  2
new thread  3
new thread  4
joined thread  0
joined thread  1
joined thread  2
joined thread  3
joined thread  4複製程式碼

getName和setName

用來得到或者設定執行緒名稱。

getPriority和setPriority

用來獲取和設定執行緒優先順序。

setDaemon和isDaemon

用來設定執行緒是否成為守護執行緒和判斷執行緒是否是守護執行緒。

守護執行緒和使用者執行緒的區別在於:守護執行緒依賴於建立它的執行緒,而使用者執行緒則不依賴。舉個簡單的例子:如果在main執行緒中建立了一個守護執行緒,當main方法執行完畢之後,守護執行緒也會隨著消亡。而使用者執行緒則不會,使用者執行緒會一直執行直到其執行完畢。在JVM中,像垃圾收集器執行緒就是守護執行緒。

在上面已經說到了Thread類中的大部分方法,那麼Thread類中的方法呼叫到底會引起執行緒狀態發生怎樣的變化呢?下面一幅圖就是在上面的圖上進行改進而來的:

Java 多執行緒乾貨系列—(一)Java 多執行緒基礎 | 掘金技術徵文

停止執行緒

停止執行緒是在多執行緒開發時很重要的技術點,掌握此技術可以對執行緒的停止進行有效的處理。
停止一個執行緒可以使用Thread.stop()方法,但最好不用它。該方法是不安全的,已被棄用。
在Java中有以下3種方法可以終止正在執行的執行緒:

  • 使用退出標誌,使執行緒正常退出,也就是當run方法完成後執行緒終止
  • 使用stop方法強行終止執行緒,但是不推薦使用這個方法,因為stop和suspend及resume一樣,都是作廢過期的方法,使用他們可能產生不可預料的結果。
  • 使用interrupt方法中斷執行緒,但這個不會終止一個正在執行的執行緒,還需要加入一個判斷才可以完成執行緒的停止。

暫停執行緒

interrupt()方法

執行緒的優先順序

在作業系統中,執行緒可以劃分優先順序,優先順序較高的執行緒得到的CPU資源較多,也就是CPU優先執行優先順序較高的執行緒物件中的任務。
設定執行緒優先順序有助於幫“執行緒規劃器”確定在下一次選擇哪一個執行緒來優先執行。
設定執行緒的優先順序使用setPriority()方法,此方法在JDK的原始碼如下:

public final void setPriority(int newPriority) {
        ThreadGroup g;
        checkAccess();
        if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
            throw new IllegalArgumentException();
        }
        if((g = getThreadGroup()) != null) {
            if (newPriority > g.getMaxPriority()) {
                newPriority = g.getMaxPriority();
            }
            setPriority0(priority = newPriority);
        }
    }複製程式碼

在Java中,執行緒的優先順序分為1~10這10個等級,如果小於1或大於10,則JDK丟擲異常throw new IllegalArgumentException()。
JDK中使用3個常量來預置定義優先順序的值,程式碼如下:
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;

執行緒優先順序特性:

  • 繼承性
    比如A執行緒啟動B執行緒,則B執行緒的優先順序與A是一樣的。
  • 規則性
    高優先順序的執行緒總是大部分先執行完,但不代表高優先順序執行緒全部先執行完。
  • 隨機性
    優先順序較高的執行緒不一定每一次都先執行完。

守護執行緒

在Java執行緒中有兩種執行緒,一種是User Thread(使用者執行緒),另一種是Daemon Thread(守護執行緒)。
Daemon的作用是為其他執行緒的執行提供服務,比如說GC執行緒。其實User Thread執行緒和Daemon Thread守護執行緒本質上來說去沒啥區別的,唯一的區別之處就在虛擬機器的離開:如果User Thread全部撤離,那麼Daemon Thread也就沒啥執行緒好服務的了,所以虛擬機器也就退出了。

守護執行緒並非虛擬機器內部可以提供,使用者也可以自行的設定守護執行緒,方法:public final void setDaemon(boolean on) ;但是有幾點需要注意:

  • thread.setDaemon(true)必須在thread.start()之前設定,否則會跑出一個IllegalThreadStateException異常。你不能把正在執行的常規執行緒設定為守護執行緒。 (備註:這點與守護程式有著明顯的區別,守護程式是建立後,讓程式擺脫原會話的控制+讓程式擺脫原程式組的控制+讓程式擺脫原控制終端的控制;所以說寄託於虛擬機器的語言機制跟系統級語言有著本質上面的區別)

  • 在Daemon執行緒中產生的新執行緒也是Daemon的。 (這一點又是有著本質的區別了:守護程式fork()出來的子程式不再是守護程式,儘管它把父程式的程式相關資訊複製過去了,但是子程式的程式的父程式不是init程式,所謂的守護程式本質上說就是“父程式掛掉,init收養,然後檔案0,1,2都是/dev/null,當前目錄到/”)

  • 不是所有的應用都可以分配給Daemon執行緒來進行服務,比如讀寫操作或者計算邏輯。因為在Daemon Thread還沒來的及進行操作時,虛擬機器可能已經退出了。

同步與死鎖

  1. 同步程式碼塊
    在程式碼塊上加上"synchronized"關鍵字,則此程式碼塊就稱為同步程式碼塊

  2. 同步程式碼塊格式

    synchronized(同步物件){
     需要同步的程式碼塊;
    }複製程式碼
  3. 同步方法
    除了程式碼塊可以同步,方法也是可以同步的
  4. 方法同步格式
    synchronized void 方法名稱(){}複製程式碼
    synchronized後續會單獨來學習。(●'◡'●)

面試題

執行緒和程式有什麼區別?
答:一個程式是一個獨立(self contained)的執行環境,它可以被看作一個程式或者一個應用。而執行緒是在程式中執行的一個任務。執行緒是程式的子集,一個程式可以有很多執行緒,每條執行緒並行執行不同的任務。不同的程式使用不同的記憶體空間,而所有的執行緒共享一片相同的記憶體空間。別把它和棧記憶體搞混,每個執行緒都擁有單獨的棧記憶體用來儲存本地資料。

如何在Java中實現執行緒?
答:
建立執行緒有兩種方式:
一、繼承 Thread 類,擴充套件執行緒。
二、實現 Runnable 介面。

啟動一個執行緒是呼叫run()還是start()方法?
答:啟動一個執行緒是呼叫start()方法,使執行緒所代表的虛擬處理機處於可執行狀態,這意味著它可以由JVM 排程並執行,這並不意味著執行緒就會立即執行。run()方法是執行緒啟動後要進行回撥(callback)的方法。

Thread類的sleep()方法和物件的wait()方法都可以讓執行緒暫停執行,它們有什麼區別?
答:sleep()方法(休眠)是執行緒類(Thread)的靜態方法,呼叫此方法會讓當前執行緒暫停執行指定的時間,將執行機會(CPU)讓給其他執行緒,但是物件的鎖依然保持,因此休眠時間結束後會自動恢復(執行緒回到就緒狀態,請參考第66題中的執行緒狀態轉換圖)。wait()是Object類的方法,呼叫物件的wait()方法導致當前執行緒放棄物件的鎖(執行緒暫停執行),進入物件的等待池(wait pool),只有呼叫物件的notify()方法(或notifyAll()方法)時才能喚醒等待池中的執行緒進入等鎖池(lock pool),如果執行緒重新獲得物件的鎖就可以進入就緒狀態。

執行緒的sleep()方法和yield()方法有什麼區別?
答:
① sleep()方法給其他執行緒執行機會時不考慮執行緒的優先順序,因此會給低優先順序的執行緒以執行的機會;yield()方法只會給相同優先順序或更高優先順序的執行緒以執行的機會;
② 執行緒執行sleep()方法後轉入阻塞(blocked)狀態,而執行yield()方法後轉入就緒(ready)狀態;
③ sleep()方法宣告丟擲InterruptedException,而yield()方法沒有宣告任何異常;
④ sleep()方法比yield()方法(跟作業系統CPU排程相關)具有更好的可移植性。

請說出與執行緒同步以及執行緒排程相關的方法。
答:

  • wait():使一個執行緒處於等待(阻塞)狀態,並且釋放所持有的物件的鎖;
  • sleep():使一個正在執行的執行緒處於睡眠狀態,是一個靜態方法,呼叫此方法要處理InterruptedException異常;
  • notify():喚醒一個處於等待狀態的執行緒,當然在呼叫此方法的時候,並不能確切的喚醒某一個等待狀態的執行緒,而是由JVM確定喚醒哪個執行緒,而且與優先順序無關;
  • notityAll():喚醒所有處於等待狀態的執行緒,該方法並不是將物件的鎖給所有執行緒,而是讓它們競爭,只有獲得鎖的執行緒才能進入就緒狀態;

總結

以上就是多執行緒的一些基礎概念,可能總結的不夠仔細,多多包涵。後續會針對一些比較重要的知識點單獨列出來總結。學好多執行緒是拿高薪的基礎,小夥伴一起加油吧!

參考

該文為本人學習的筆記,方便以後自己跳槽前複習。參考網上各大帖子,取其精華整合自己的理解而成。還有,關注我個人主頁的公眾號,裡面電子書資源有《Java多執行緒程式設計核心技術》以及《JAVA併發程式設計實踐》高清版,需要的小夥伴自己取。

《Java多執行緒程式設計核心技術》
《JAVA併發程式設計實踐》
Java併發程式設計:Thread類的使用
關於Java併發程式設計的總結和思考
JAVA多執行緒實現的三種方式

整理的思維導圖

個人整理的多執行緒基礎的思維導圖,匯出的圖片無法檢視備註的一些資訊,所以需要原始檔的童鞋可以關注我個人主頁上的公眾號,回覆多執行緒基礎即可獲取原始檔。

Java 多執行緒乾貨系列—(一)Java 多執行緒基礎 | 掘金技術徵文


一直覺得自己寫的不是技術,而是情懷,一篇篇文章是自己這一路走來的痕跡。靠專業技能的成功是最具可複製性的,希望我的這條路能讓你少走彎路,希望我能幫你抹去知識的蒙塵,希望我能幫你理清知識的脈絡,希望未來技術之巔上有你也有我。

訂閱博主微信公眾號:嘟爺java超神學堂(javaLearn)三大好處:

  • 獲取最新博主部落格更新資訊,首發公眾號
  • 獲取大量視訊,電子書,精品破解軟體資源
  • 可以跟博主聊天,歡迎程式媛妹妹來撩我

Java 多執行緒乾貨系列—(一)Java 多執行緒基礎 | 掘金技術徵文

掘金技術徵文第三期:聊聊你的最佳實踐

相關文章