Java併發編程知識概覽(一)

原文博客地址:pjmike的博客java

進程與線程

下面比較簡單介紹下進程與線程的概念:git

進程

關於進程的定義,其實有不少:github

  • 一個正在執行的程序
  • 計算機中正在運行的程序的一個實例
  • 能夠分配給處理器並由處理器執行的一個實例。

我的以爲比較好的定義是:算法

進程是具備必定獨立功能的程序關於某個數據集合上的一次運行過程編程

說白了,進程就是CPU執行的一次任務,在單個CPU中一次只能運行一次任務,即CPU老是運行一個進程,其餘進程處於非運行狀態。可是如今的操做系統都是多核CPU,因此能夠同時運行多個進程,執行多個任務。bash

線程

線程其實是一個進程中的"子任務",在一個進程中能夠建立多個線程,打個比方,打開QQ就是執行了一個進程,而在QQ裏與他人聊天,同時下載文件等操做就是線程。網絡

爲何要使用多線程

多線程是指操做系統在單個進程內支持多個併發執行路徑的能力。每一個進程中只有一個線程在執行的傳統方法稱爲單線程方法。可是單線程存在不少弊端,它並不能充分利用CPU資源,並且單線程在應對複雜業務時響應時間也是較差的,因此咱們須要使用多線程來幫助咱們。使用多線程有以下好處(緣由):多線程

  • 更多的處理器核心:多線程可使用多個處理器資源,提高程序的執行效率
  • 更快的響應時間 :在複雜業務中,使用多線程能夠縮短響應時間,提高用戶體驗
  • 更好的編程模型 :在Java開發中,多線程編程提供了良好,考究而且一致的編程模型。

那咱們爲何去使用 多線程而不是使用多進程去進行併發程序的設計,是由於線程間的切換和調度的成本遠小於 進程併發

線程的狀態

首先有進程,其次纔是線程,其實線程的生命週期及各類狀態轉換和進程相似,下面看一張 進程的狀態轉換圖(圖片摘自網絡):ide

progress

進程通常有三種基本的狀態:

  • 運行態:程序正在運行
  • 就緒態:進程作好了準備,有機會就開始執行
  • 阻塞態:進程等待某一事件而中止運行

進程被建立後,加入就緒隊列等待被調度執行,CPU調度器根據必定的調度算法調度就緒隊列中的進程在處理機上執行。

上面簡述了進程的基本狀態及變化過程,線程也是相似的:

java_thread

該圖表示一個線程的生命週期狀態流轉圖,很清楚的描繪了一個線程從建立到終止的一個過程。線程的全部狀態在 Thread類中的State枚舉定義,以下:

public enum State {

    NEW,
    
    RUNNABLE,

    BLOCKED,

    WAITING,

    TIMED_WAITING,

    TERMINATED;
}
複製代碼
  • New: 表示剛剛建立的線程,這種線程還沒執行
  • Running: 表示線程已經調用 start()方法,線程只在執行
  • Blocked: 表示線程阻塞,等待獲取鎖,如碰到 synchronized同步塊,一旦獲取鎖就進入 Running 狀態繼續執行
  • Waiting: 表示線程進入一個無時間限制的等待,等待一些特殊的事件來喚醒,好比經過 wait() 方法等待的線程在等待notify()方法,而經過 join() 方法等待的線程則會等待目標線程的終止。一旦等到了指望了事件,線程會再次執行,進入Running狀態
  • Timed Waiting: 表示進行一個有時限的等待,如sleep(3000)等待(睡眠)3秒後,從新進入Running狀態繼續執行
  • Terminated: 表示線程執行結束,進入終止狀態

注意:從New 狀態出發後,線程不能再回到 New狀態,同理,處於 Terminated 的線程也不能回到 Running狀態

下面分別對線程的各類狀態進行相關說明

線程的相關操做

新建線程

新建線程有兩種方式:

  • 繼承Thread類
  • 實現Runnable接口

Thread類是在java.lang包中定義的,原本咱們能夠直接繼承Thread,重載 run() 方法來自定義線程,可是Java 是單繼承的,若是一個類已經繼承了其餘類,就沒法再繼承Thread,因此咱們這時就能夠實現 Runnable接口來實現

// 1. 繼承Thread類
public class Thread1 extends Thread{
    @Override
    public void run() {
        System.out.println("hello world");
    }

    public static void main(String[] args) {
        Thread1 thread1 = new Thread1();
        //調用start()運行線程,執行內部的run()方法
        thread1.start();
    }
}

//2. 實現Runnable接口
public class Thread2 implements Runnable{

    @Override
    public void run() {
        System.out.println("hello world");
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new Thread2());
        thread.start();
    }
}
複製代碼

Thread有一個重要的構造方法:

public Thread(Runnable target) 複製代碼

它傳入一個Runnable 接口的實例,在start()方法調用時,新的線程就會執行 Runnable.run()方法,實際上,默認的Thread.run()就是這麼作的:

public void run() {
    if (target != null) {
        target.run();
    }
}
複製代碼

默認的Thread.run()就是直接調用 內部的 Runnable接口

終止線程

通常來講,線程在執行完畢後就會結束,無須手動關閉,可是仍是有些後臺常駐線程可能不會自動關閉。

Thread提供了一個 stop()方法,該方法能夠當即將一個線程終止,可是目前stop()已經被廢棄,不推薦使用,緣由是 stop()方法太過於簡單粗暴,強行把執行到一半的線程終止,可能會引發一些數據不一致的問題。

線程中斷

線程中斷是一種重要的線程協做機制,它並不會使線程當即退出,而是給線程發送一個通知,告知目標線程,有人但願你退出。至於目標線程接到通知後如何處理,則徹底由目標線程自行決定。

與線程中斷有關的,有三個方法:

public void Thread.interrupt()  //中斷線程
public boolean Thread.isInterrupted()  //判斷是否被中斷
public static boolean Thread.interrupted()  //判斷是否被中斷,並清除當前中斷狀態
複製代碼
  • Thread.interrupt()方法是一個實例方法,通知目標線程中斷,設置中斷標誌位,該標誌位代表當前線程已經被中斷了
  • Thread.isInterrupted()方法也是實例方法,判斷當前線程是否有被中斷(經過檢查中斷標誌位)
  • 靜態方法Thread.interrupted也是用來判斷當前線程的中斷狀態的,但同時會清楚當前線程的中斷標誌位狀態

舉例說明:

public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (true) {
                //經過中斷標誌位判斷是否被中斷
                if (Thread.currentThread().isInterrupted()) {
                    //中斷邏輯處理
                    System.out.println("Interrupted!");
                    // break退出循環
                    break;
                }
            }
        });
        //開啓線程
        thread.start();
        Thread.sleep(2000);
        //設置中斷標誌位
        thread.interrupt();
    }
複製代碼

Thread有個 sleep方法,它會讓當前線程休眠若干時間,它會拋出一個 InterruptedException中斷異常。InterruptedException不是運行時異常,也就是說程序必須捕獲而且處理它,當線程在 sleep()休眠時,若是被中斷,這個異常就產生了。

public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread() {
        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    System.out.println("Interrupted When Sleep");
                    //中斷異常會清楚中斷標記,從新設置中斷狀態
                    Thread.currentThread().interrupt();
                }
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("Interrupted!");
                    break;
                }
            }
        }
    };
    thread.start();
    Thread.sleep(2000);
    thread.interrupt();
}

//output:

Interrupted When Sleep
Interrupted!
複製代碼

Thread.sleep() 方法因爲中斷而拋出異常,此時,它會清楚中斷標記,若是不加處理,那麼在下一次循環開始時,就沒法捕獲這個中斷,故在異常處理中,再次設置中斷標誌位

等待(wait) 和通知(notify)

爲了支持多線程之間的協做,JDK 提供了兩個很是重要的接口線程等待 wait() 方法和通知 notify() 方法,這兩個方法定義在 Object類中。

public final void wait(long timeout) throws InterruptedException;

public final void notify();
複製代碼

當在一個對象實例上調用了object.wait() 方法,那麼它就會進入object對象的等待隊列進行等待,這個等待隊列中,可能會有多個線程,由於系統運行多個線程同時等待某一個對象。當object.notify()被調用時,它就會從 這個等待隊列中,隨機選擇一個線程,並將其喚醒

除了notify()方法外,Object 對象還有一個相似的 notifyAll()方法 ,它和notify()的功能同樣,不一樣的是,它會喚醒這個等待隊列中全部等待的線程

下面看一個代碼示例:

public class Example {
    public static void main(String[] args) {
        final Object object = new Object();
        new Thread(() -> {
            System.out.println("thread A is waiting to get lock");
            synchronized (object) {
                System.out.println("thread A already get lock");
                try {
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println("thread A do wait method");
                    object.wait();
                    System.out.println("thread A wait end");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(() -> {
            System.out.println("thread B is waiting to get lock");
            synchronized (object) {
                System.out.println("thread B already get lock");
                try {
                    TimeUnit.SECONDS.sleep(5);
                    System.out.println("thread B do wait method");
                    object.notify();
                    System.out.println("thread B do notify method");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

複製代碼

執行結果:

thread A is waiting to get lock
thread A already get lock
thread B is waiting to get lock
thread A do wait method
thread B already get lock
thread B do wait method
thread B do notify method
thread A wait end
複製代碼

上述開啓了兩個線程A和B,A執行 wait()方法前,A先申請 object 的對象鎖,在執行 object.wait()時,持有object的鎖,執行後,A會進入等待,並釋放 object的鎖。

B在執行notify()以前也會先得到 object的對象鎖,執行notify()以後,釋放object的鎖,而後A從新得到鎖後繼續執行

等待線程結束(join) 和 謙讓(join)

join方法使當前線程等待調用 join方法的線程結束後才能繼續往下執行,下面是代碼示例:

public class ThreadExample {
    public volatile static int i = 0;
    public static class Thread_ extends Thread{
        @Override
        public void run() {
            for (i = 0; i < 1000000; i++) {

            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread_ A = new Thread_();
        A.start();
        //讓當前的主線程等待thread_線程執行完畢後才往下執行
        A.join();
        System.out.println(i);
    }
}

複製代碼

join()的本質其實是讓調用線程wait()在當前線程對象實例上,當線程執行完畢後,被等待線程退出前調用 notifyAll()通知全部的等待線程繼續執行

以上面的例子說明,線程A調用join()使main線程 wait()在A對象實例上,等線程A執行完畢,線程A調用notifyAll(),main線程等到通知繼續往下執行。

而Thread.yield()方法的定義以下:

public static native void yield();
複製代碼

它是靜態方法,一旦執行,它會使當前調用該方法的線程讓出CPU,可是讓出CPU不表明 當前線程不執行,當前線程讓出CPU後,還會進行CPU資源的爭奪,可是是否可以被分配到,就不必定了。

Thread.yield()的調用就好像在說:

我已經完成了一些最重要的工做了,我應該是能夠休息一下了,能夠給其餘線程一些工做機會了

守護線程(Daemon)

守護線程是一種特殊的下線程,它是系統的守護者,在後臺默默地完成一些系統性的服務,好比垃圾回收線程,JIT線程就能夠理解爲守護線程,與之對應的是用戶線程,用戶線程能夠理解爲系統的工做線程,它會完成這個程序應該要完成的業務操做。當一個Java應用內,只有守護線程時,Java虛擬機就會天然退出。

代碼示例以下:

public class DaemonDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("I am alive");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        //將線程t設置爲守護線程
        t.setDaemon(true);
        t.start();
        //主線程休眠10s
        Thread.sleep(10000);
    }
}
複製代碼

輸出結果:

I am alive
I am alive
I am alive
I am alive
I am alive
I am alive
I am alive
I am alive
I am alive
I am alive
複製代碼

上面的例子,將t設置爲守護線程,系統中只有主線程 main 爲用戶線程,在 main休眠10s後,守護線程退出,整個線程也退出。

線程優先級

Java 中的線程能夠有本身的優先級,優先級高的線程在競爭資源時會更有優點,更可能搶佔資源。線程的優先級調度和操做系統密切相關,固然高優先級可能也會有搶佔失敗的時候,可是大部分狀況下,高優先級比低優先級對於搶佔資源來講更有優點。

代碼示例以下:

public class PriorityDemo {
    private static int count = 0;
    public static void main(String[] args) {
        Thread A = new Thread() {
            @Override
            public void run() {
                while (true) {
                    synchronized (PriorityDemo.class) {
                        count++;
                        if (count > 100000) {
                            System.out.println("HighPriority is complete");
                            break;
                        }
                    }
                }
            }
        };
        Thread B = new Thread() {
            @Override
            public void run() {
                while (true) {
                    synchronized (PriorityDemo.class) {
                        count++;
                        if (count > 100000) {
                            System.out.println("LowPriority is complete");
                            break;
                        }
                    }
                }
            }
        };
        //設置線程A爲高優先級
        A.setPriority(Thread.MAX_PRIORITY);
        //設置線程B爲低優先級
        B.setPriority(Thread.MIN_PRIORITY);
        //啓動線程B
        B.start();
        //啓動線程A
        A.start();
    }
}
複製代碼

運行結果:

HighPriority is complete
LowPriority is complete
複製代碼

由上能夠初步得出,高優先級的線程在大部分狀況下會首先完成任務。

總結

上面簡單總結了一部分Java併發編程所涉及到知識點,算是入門Java併發的一個開端。

參考資料 & 鳴謝

相關文章
相關標籤/搜索