Java線程圖文總結

jopen 8年前發布 | 14K 次閱讀 Java開發

實現方式

簡單介紹一下Java多線程實現方式,有以下三種:

1、繼承Thread類

2、實現Runnable接口

3、使用ExecutorService、Callable、Future實現有返回結果的多線程

區別是前兩種執行完之后不帶返回值,最后一種帶返回值,其中最常用為前兩種。


線程的狀態

java線程的整個生命周期有5個狀態:新建,就緒,運行中,阻塞,結束。

5個狀態之間的關系將結合下圖理解:

上圖為java線程生命周期期間的各種命運,下面介紹常見的幾種命運。

命運一

新線程創建成功,調用start()進入就緒狀態,即進入待運行的線程池中等待,等待獲取CPU的使用權。當獲得CPU使用權,該線程從就緒狀態進入運行狀態。運行過程中,運氣好的,一次運行就把所要執行的任務執行完畢,線程結束;命運不好的,運行中途被CPU暫停運行,重新回到就緒狀態,等待分配,然后再等待進入運行期,直到最后運行完畢,最后結束。


命運二

新線程創建成功,進入就緒狀態,獲取了CPU使用權,處于運行狀態。這里意外出現,該線程執行了sleep、yield、join三者其中一個命令。sleep、join需要被暫停執行一段時間,線程進入阻塞狀態。休息時間到,再重新進入就緒狀態;而yield是從運行狀態直接跳會就緒狀態。當到了就緒狀態后再重新等待CPU調度,重新進入運行期。run -> block -> run -> block.... 此種狀態會持續,一直到該線程的任務執行完畢。sleep、yield、join三者區別如下:

sleep:CPU暫停當前線程運行,同時讓就緒狀態(待運行池中)優先級較低的一個線程運行。當前線程被暫停后,會處于阻塞狀態n秒(n由開發人員設定),時間到了之后,會自動回到就緒狀態,等待CPU重新調度,重新從剛剛暫停的地方運行。注意,若代碼塊中包含了對象的鎖,在睡眠的過程中是不會釋放掉對象的鎖的,其他線程是不能訪問到共享數據的。

join:將指定的線程執行完成后,再運行當前線程剩下的任務。典型的例子是將兩個交替執行的線程合并順序執行。請結合下面代碼理解:

public static void main(String[] args) throws IOException {
    final Thread aThread = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println("a:" + i);
            }
        }
    });

    Thread bThread = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                aThread.join();
            } catch (InterruptedException e) {
            }

            for (int i = 0; i < 5; i++) {
                System.out.println("b:" + i);
            }
        }
    });

    bThread.start();
    aThread.start();

    try {
        bThread.join();
    } catch (InterruptedException e) {
    }

    for (int i = 0; i < 5; i++) {
        System.out.println("c:" + i);
    }
}

輸出結果:

a:0
a:1
a:2
a:3
a:4
b:0
b:1
b:2
b:3
b:4
c:0
c:1
c:2
c:3
c:4

如果把代碼中的join部分去掉,就不能保證a、b、c的輸出順序。

yield:實質是當前正在運行的線程直接回到就緒狀態,而不用進入阻塞狀態等待,且只會選擇優先級相同的線程進入運行狀態。若待運行的線程池中,沒有當前線程優先級相同的線程,或者當前線程又被選中運行,則當前線程還是會繼續運行,會誤造成yield無法達到目的的效果。


命運三

線程a成功進入創建、就緒、運行流程,運行過程中,需要訪問資源i,而此時資源i正在被另外的線程b訪問并且上鎖了,此時線程a就會暫停運行,進入資源i的鎖池,等待線程b釋放資源i的鎖。當線程a獲得資源i的鎖時,會從資源i的鎖池中進入就緒狀態(待運行池),等待調度。以下為示例代碼:

public static void main(String[] args) throws IOException {
    final Object obj = 1;
    Thread aThread = new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized(obj){
                for (int i = 0; i < 3; i++) {
                    System.out.println("線程a在使用obj...");
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                    }
                }
            }
        }
    });

    Thread bThread = new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized(obj){
                for (int i = 0; i < 3; i++) {
                    System.out.println("線程b在使用obj...");
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                    }
                }
            }
        }
    });
    bThread.start();
    aThread.start();
}

輸出結果是:

線程b在使用obj...
線程b在使用obj...
線程b在使用obj...
線程a在使用obj...
線程a在使用obj...
線程a在使用obj...

兩個線程都start()之后,兩個線程隨機先后訪問到obj對象,有可能是a先訪問obj,有可能是b先訪問obj。我的結果就是b先訪問obj,拿到obj的鎖,之后線程a無法立即訪問到obj,a就進入obj的鎖池中等待;當b中被鎖的代碼塊跑完且釋放鎖,a拿到obj的鎖,重新進入就緒狀態,等待分配運行。


命運四

線程a成功的創建、就緒、運行,運行過程中調用obj.wait(),a就從run狀態進入到obj的等待池,等待其他線程調用obj.notify;當其他線程調用notify之后,線程a從obj的等待池中,進入到obj的鎖池,等待獲得鎖以重新進入就緒狀態恢復運行。下面具體解析一下wait、notify、notifyAll的作用:

wait:在已經獲得obj的鎖的前提下,主動釋放obj的鎖,釋放后當前線程會進入obj的等待池,即開始休眠。另外,當調用了wait之后,并不是立即釋放對象obj的鎖,而是在相應的synchronized()語句塊執行結束后才真正釋放。

notify的作用是從對象obj的等待池中隨機抽取一個線程放到對象obj的鎖池中,讓該線程繼續在鎖池中等待鎖,成功拿到鎖后,等待分配執行。

notifyAll的作用跟notify類似,只不過是把對象obj的等待池中的所有的線程全部取出,放到對象obj的鎖池中。

需要明確的一點:wait、notify、notifyAll均為在當前線程獲取對象的鎖的前提下執行的,所以這三個操作都必須在synchronized()塊中執行。

舉個例子:線程a、線程b、線程c需要協同工作,線程b、c的任務需要在a線程的任務完成后才能開始。那就是說a在完成任務后,需要通知b和c恢復工作。這種情況就可通過wait,notifyAll來實現協同工作。請看以下代碼:

public static void main(String[] args) {
    final Object object = new Object();
    Thread a = new Thread() {
        public void run() {
            System.out.println("a start");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }
            synchronized (object) {
                System.out.println("a finish, notify b and c");
                object.notifyAll(); //wait,notify,notifyAll必須在synchronized塊里面使用
            }
        }
    };

    Thread b = new Thread() {
        public void run() {
            synchronized (object) {
                System.out.println("b is starting, waiting for a finish...");
                try {
                    object.wait();
                } catch (InterruptedException e) {
                }
                System.out.println("b end");
            }
        }
    };

    Thread c = new Thread() {
        public void run() {
            synchronized (object) {
                System.out.println("c is starting, waiting for a finish...");
                try {
                    object.wait();
                } catch (InterruptedException e) {
                }
                System.out.println("c end");
            }
        }
    };

    a.start();
    b.start();
    c.start();
}

其中一次的輸出結果:

a start
b is starting, waiting for a finish...
c is starting, waiting for a finish...
a finish, notify b and c
c end
b end

因為線程在鎖池中是被隨機抽取的,所以不可能保證b,c哪個先運行。上面的結果就是c先被抽取。

上述全部討論的前提是線程運行中沒有遇到exception的情況,若遇上了exception,就直接end。


以上為本人對Java線程的理解,是基于線程的淺層部分展開討論,歡迎指正。若想深入了解線程,可以看看java.util.concurrent包下的類。本人之前寫過一個基于多線程實現的遠程監控程序,有興趣可參考一下:

http://my.oschina.net/ericquan8/blog/383882



來自: http://my.oschina.net/ericquan8/blog/384655

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!