【原創】Java併發編程系列2:線程概念與基礎操做

【原創】Java併發編程系列2:線程概念與基礎操做

偉大的理想只有通過忘個人鬥爭和犧牲才能勝利實現。

本篇爲【Dali王的技術博客】Java併發編程系列第二篇,講講有關線程的那些事兒。主要內容是以下這些:java

  • 線程概念
  • 線程基礎操做

線程概念

進程表明了運行中的程序,一個運行的Java程序就是一個進程。在Java中,當咱們啓動main函數時就啓動了一個JVM的進程,而main函數所在的線程就是這個進程中的一個線程,稱爲主線程。
進程和線程的關係以下圖所示:
file面試

由上圖能夠看出來,一個進程中有多個線程,多個線程共享進程的堆的方法區資源,可是每一個線程有本身的程序計數器和棧區域。編程

線程基礎操做

線程建立與運行

Java中有三種線程建立方式,分別爲:繼承Thread類並重寫run方法,實現Runnable接口的run方法,使用FutureTask方式。
先看繼承Thread方式的實現,代碼示例以下:json

public class ThreadDemo {
    public static class DemoThread extends Thread {
        @Override
        public void run() {
            System.out.println("this is a child thread.");
        }
    }
    public static void main(String[] args) {
        System.out.println("this is main thread.")
        DemoThread thread = new DemoThread();
        thread.start();
    }
}

上面代碼中DemoThread類繼承了Thread類,並重寫了run方法。在main函數裏建立了一個DemoThread的實例,而後調用其start方法啓動了線程。微信

tips:調用start方法後線程並無立刻執行,而是處於就緒狀態,也就是這個線程已經獲取了除CPU資源外的其餘資源,等待獲取CPU資源後纔會真正處於運行狀態。
使用繼承方式,好處在於經過this就能夠獲取當前線程,缺點在於Java不支持多繼承,若是繼承了Thread類,那麼就不能再繼承其餘類。並且任務與代碼耦合嚴重,一個線程類只能執行一個任務,使用Runnable則沒有這個限制。
來看實現Runnable接口的run方法的方式,代碼示例以下:
public class RunnableDemo {
    public static class DemoRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("this is a child thread.");
        }
    }
    public static void main(String[] args) {
        System.out.println("this is main thread.");
        DemoRunnable runnable = new DemoRunnable();
        new Thread(runnable).start();
        new Thread(runnable).start();
    }
}

上面代碼兩個線程共用一個Runnable邏輯,若是須要,能夠給RunnableTask添加參數進行任務區分。在Java8中,可使用Lambda表達式對上述代碼進行簡化:併發

public static void main(String[] args) {
    System.out.println("this is main thread.");
    Thread t = new Thread(() -> System.out.println("this is child thread"));
    t.start();
}

上面兩種方式都有一個缺點,就是任務沒有返回值,下面看第三種,使用FutureTask的方式。代碼示例以下:ide

public class CallableDemo implements Callable<JsonObject> {
    @Override
    public JsonObject call() throws Exception {
        return new JsonObject();
    }
    public static void main(String[] args) {
        System.out.println("this is main thread.");
        FutureTask<JsonObject> futureTask = new FutureTask<>(new CallableDemo());   // 1. 可複用的FutureTask
        new Thread(futureTask).start();
        try {
            JsonObject result = futureTask.get();
            System.out.println(result.toString());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

        // 2. 一次性的FutureTask
        FutureTask<JsonObject> innerFutureTask = new FutureTask<>(() -> {
            JsonObject jsonObject = new JsonObject();
            jsonObject.addProperty("name", "Dali");
            return jsonObject;
        });
        new Thread(innerFutureTask).start();

        try {
            JsonObject innerResult = innerFutureTask.get();
            System.out.println(innerResult.toString());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

如上代碼,CallableDemo實現了Callable接口的call方法,在main函數中使用CallableDemo的實例建立了一個FutureTask,而後使用建立的FutureTask對象做爲任務建立了一個線程並啓動它,最後經過FutureTask等待任務執行完畢並返回結果。
一樣的,上面的操做過程適合於須要複用的任務,若是對於一次性的任務,大能夠經過Lambda來簡化代碼,如註釋2處。函數

等待線程終止

在項目中常常會遇到一個場景,就是須要等待某幾件事情完成後才能繼續往下執行。Thread類中有一個join方法就能夠用來處理這種場景。直接上代碼示例:學習

public static void main(String[] args) throws InterruptedException {
        System.out.println("main thread starts");
        Thread t1 = new Thread(() -> System.out.println("this is thread 1"));
        Thread t2 = new Thread(() -> System.out.println("this is thread 2"));
        t1.start();
        t2.start();
        System.out.println("main thread waits child threads to be over");
        t1.join();
        t2.join();
        System.out.println("child threads are over");
    }

上面代碼在主線程裏啓動了兩個線程,而後分別調用了它們的join方法,主線程會在調用t1.join()後被阻塞,等待其執行完畢後返回;而後主線程調用t2.join()後再次被阻塞,等待t2執行完畢後返回。上面代碼的執行結果以下:this

main thread starts
main thread waits child threads to be over
this is thread 1
this is thread 2
child threads are over

須要注意的是,線程1調用線程2的join方法後會被阻塞,當其餘線程調用了線程1的interrupt方法中斷了線程1時,線程1會拋出一個InterruptedException異常而返回。

讓線程睡眠

Thread類中有一個static的sleep方法,當一個執行中的線程調用了Thread的sleep方法後,調用線程會暫時讓出指定時間的執行權,也就是在這期間不參與CPU的調度,可是該線程所擁有的監視器資源,好比鎖仍是不讓出的。指定的睡眠時間到了後該函數會正常返回,線程就處於就緒狀態,而後等待CPU的調度執行。

tips:面試當中wait和sleep常常會被用來比較,須要多加體會兩者的區別。
調用某個對象的wait()方法,至關於讓當前線程交出此對象的monitor,而後進入等待狀態,等待後續再次得到此對象的鎖;notify()方法可以喚醒一個正在等待該對象的monitor的線程,當有多個線程都在等待該對象的monitor的話,則只能喚醒其中一個線程,具體喚醒哪一個線程則不得而知。
調用某個對象的wait()方法和notify()方法,當前線程必須擁有這個對象的monitor,所以調用wait()方法和notify()方法必須在同步塊或者同步方法中進行(synchronized塊或者synchronized方法)。

看一個線程睡眠的代碼示例:

private static final Lock lock = new ReentrantLock();
public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
       // 獲取獨佔鎖
       lock.lock();
       System.out.println("thread1 get to sleep");
        try {
            Thread.sleep(1000);
            System.out.println("thread1 is awake");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    });
    Thread t2 = new Thread(() -> {
        // 獲取獨佔鎖
        lock.lock();
        System.out.println("thread2 get to sleep");
        try {
            Thread.sleep(1000);
            System.out.println("thread2 is awake");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    });

    t1.start();
    t2.start();
}

上面的代碼建立了一個獨佔鎖,而後建立了兩個線程,每一個線程在內部先獲取鎖,而後睡眠,睡眠結束後會釋放鎖。執行結果以下:

thread1 get to sleep
thread1 is awake
thread2 get to sleep
thread2 is awake

從執行結果來看,線程1先獲取鎖,而後睡眠,再被喚醒,以後才輪到線程2獲取到鎖,也即在線程1sleep期間,線程1並無釋放鎖。
須要注意的是,若是子線程在睡眠期間,主線程中斷了它,子線程就會在調用sleep方法處拋出了InterruptedException異常。

線程讓出CPU

Thread類中有一個static的yield方法,當一個線程調用yield方法時,實際就是暗示線程調度器當前線程請求讓出本身的CPU使用,若是該線程還有沒用完的時間片也會放棄,這意味着線程調度器能夠進行下一輪的線程調度了。
當一個線程調用yield方法時,當前線程會讓出CPU使用權,而後處於就緒狀態,線程調度器會從線程就緒隊列裏面獲取一個線程優先級最高的線程,固然也有可能會調度到剛剛讓出CPU的那個線程來獲取CPU執行權。
請看代碼示例:

public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 10; i++) {
            if (i == 8) {
                System.out.println("current thread: " + Thread.currentThread() + " yield cpu");
            }
            Thread.yield(); // 2
        }
        System.out.println("current thread: " + Thread.currentThread() + " is over");
    });

    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 10; i++) {
            if (i == 8) {
                System.out.println("current thread: " + Thread.currentThread() + " yield cpu");
            }
            Thread.yield(); // 1
        }
        System.out.println("current thread: " + Thread.currentThread() + " is over");
    });
    t1.start();
    t2.start();
}

在如上的代碼中,兩個線程的功能同樣,運行屢次,同一線程的兩行輸出是順序的,可是總體順序是不肯定的,取決於線程調度器的調度狀況。
當把上面代碼中1和2處代碼註釋掉,會發現結果只有一個,以下:

current thread: Thread[Thread-1,5,main] yield cpu
current thread: Thread[Thread-0,5,main] yield cpu
current thread: Thread[Thread-1,5,main] is over
current thread: Thread[Thread-0,5,main] is over

從結果可知,Thread.yiled方法生效使得兩個線程分別在執行過程當中放棄CPU,而後在調度另外一個線程,這裏的兩個線程有點互相謙讓的感受,最終是因爲只有兩個線程,最終仍是執行完了兩個任務。

tips:sleep和yield的區別:
當線程調用sleep方法時,調用線程會阻塞掛起指定的時間,在這期間線程調度器不會去調度該線程。而調用yield方法時,線程只是讓出本身剩餘的時間片,並無被阻塞掛起,而是出於就緒狀態,線程調度器下一次調度時就可能調度到當前線程執行。

線程中斷

Java中的線程中斷是一種線程間的協做模式。每一個線程對象裏都有一個boolean類型的標識(經過isInterrupted()方法返回),表明着是否有中斷請求(interrupt()方法)。例如,當線程t1想中斷線程t2,只須要在線程t1中將線程t2對象的中斷標識置爲true,而後線程2能夠選擇在合適的時候處理該中斷請求,甚至能夠不理會該請求,就像這個線程沒有被中斷同樣。
在上面章節中也講到了線程中斷的一些內容,此處就再也不用代碼來展開了。

Java併發編程大綱

繼續附上Java編程的系統學習大綱以供參考:
Java併發編程.pngfile

【參考資料】

  1. 《Java併發編程之美》
本文由微信公衆號【Dali王的技術博客】原創,掃碼關注獲取更多原創技術文章。
Dali王的技術博客
相關文章
相關標籤/搜索