1. 程式人生 > >Java並發編程:Thread類的使用

Java並發編程:Thread類的使用

可能 學習 結束 而是 先來 lock 停止 常用 ren

一.線程的狀態

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

  線程從創建到最終的消亡,要經歷若幹個狀態。一般來說,線程包括以下這幾個狀態:創建(new)、就緒(runnable)、運行(running)、阻塞(blocked)、time waiting、waiting、消亡(dead)。

public enum State {
    /**
     * Thread state for a thread which has not yet started.
     */
    NEW,

    /**
* Thread state for a runnable thread. A thread in the runnable * state is executing in the Java virtual machine but it may * be waiting for other resources from the operating system * such as processor. */ RUNNABLE, /** * Thread state for a thread blocked waiting for a monitor lock. * A thread in the blocked state is waiting for a monitor lock * to enter a synchronized block/method or * reenter a synchronized block/method after calling * {
@link Object#wait() Object.wait}. */ BLOCKED, /** * Thread state for a waiting thread. * A thread is in the waiting state due to calling one of the * following methods: * <ul> * <li>{@link Object#wait() Object.wait} with no timeout</li> * <li>{
@link #join() Thread.join} with no timeout</li> * <li>{@link LockSupport#park() LockSupport.park}</li> * </ul> * * <p>A thread in the waiting state is waiting for another thread to * perform a particular action. * * For example, a thread that has called <tt>Object.wait()</tt> * on an object is waiting for another thread to call * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on * that object. A thread that has called <tt>Thread.join()</tt> * is waiting for a specified thread to terminate. */ WAITING, /** * Thread state for a waiting thread with a specified waiting time. * A thread is in the timed waiting state due to calling one of * the following methods with a specified positive waiting time: * <ul> * <li>{@link #sleep Thread.sleep}</li> * <li>{@link Object#wait(long) Object.wait} with timeout</li> * <li>{@link #join(long) Thread.join} with timeout</li> * <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li> * <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li> * </ul> */ TIMED_WAITING, /** * Thread state for a terminated thread. * The thread has completed execution. */ TERMINATED; }

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

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

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

  當由於突然中斷(拋異常)或者子任務執行完畢,線程就會被消亡。

  下面這副圖描述了線程從創建到消亡之間的狀態:

技術分享

在有些教程上將blocked、waiting、time waiting統稱為阻塞狀態,這個也是可以的,只不過這裏我想將線程的狀態和Java中的方法調用聯系起來,所以將waiting和time waiting兩個狀態分離出來。

二.上下文切換

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

  由於可能當前線程的任務並沒有執行完畢,所以在切換時需要保存當前線程的運行狀態,以便下次重新切換回來時能夠繼續切換之前的狀態運行。舉個簡單的例子:比如一個線程A正在讀取一個文件的內容,正讀到文件的一半,此時需要暫停線程A,轉去執行線程B,當再次切換回來執行線程A的時候,我們不希望線程A又從文件的開頭來讀取,我們希望從上次暫停的地方開始繼續讀取,這就需要知道上次暫停的位置。

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

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

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

三.Thread類中的方法

  通過查看java.lang.Thread類的源碼可知:

public class Thread implements Runnable {
    /* Make sure registerNatives is the first thing <clinit> does. */
    private static native void registerNatives();
    static {
        registerNatives();
    }

    private volatile String name;
    private int            priority;
    private Thread         threadQ;
    private long           eetop;

    /* Whether or not to single_step this thread. */
    private boolean     single_step;

    /* Whether or not the thread is a daemon thread. */
    private boolean     daemon = false;

    /* JVM state */
    private boolean     stillborn = false;

    /* What will be run. */
    private Runnable target;

    /* The group of this thread */
    private ThreadGroup group;

    /* The context ClassLoader for this thread */
    private ClassLoader contextClassLoader;

    /* The inherited AccessControlContext of this thread */
    private AccessControlContext inheritedAccessControlContext;

    /* For autonumbering anonymous threads. */
    private static int threadInitNumber;
    private static synchronized int nextThreadNum() {
        return threadInitNumber++;
    }

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

  Thread類實現了Runnable接口,在Thread類中,有一些比較關鍵的屬性,比如name是表示Thread的名字,可以通過Thread類的構造器中的參數來指定線程名字,priority表示線程的優先級(最大值為10,最小值為1,默認值為5),daemon表示線程是否是守護線程,target表示要執行的任務。

  下面是Thread類中常用的方法:

  以下是關系到線程運行狀態的幾個方法:

  1)start方法

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

  2)run方法

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

  3)sleep方法

  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);
            }
        }
    }
}

輸出結果:

i:11
線程Thread-0進入睡眠狀態
線程Thread-0睡眠結束
i:12
i:13
線程Thread-1進入睡眠狀態
線程Thread-1睡眠結束
i:14

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

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

4)yield方法

  調用yield方法會讓當前線程交出CPU權限,讓CPU去執行其他的線程。它跟sleep方法類似,同樣不會釋放鎖。但是yield不能控制具體的交出CPU的時間,另外,yield方法只能讓擁有相同優先級的線程有獲取CPU執行時間的機會。

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

  5)join方法

  join方法有三個重載版本:

join()
join(long millis)     //參數為毫秒
join(long millis,int nanoseconds)    //第一參數為毫秒,第二個參數為納秒

  假如在main線程中,調用thread.join方法,則main方法會等待thread線程執行完畢或者等待一定的時間。如果調用的是無參join方法,則等待thread執行完畢,如果調用的是指定了時間參數的join方法,則等待一定的時間。

  看下面一個例子:

import java.io.IOException;

public class Test {

    public static void main(String[] args) throws IOException {
        System.out.println("進入線程:"+Thread.currentThread().getName());
        Test test = new Test();
        MyThread thread1 = test.new MyThread();
        thread1.start();
        try {
            System.out.println("線程"+Thread.currentThread().getName()+"等待");
            thread1.join();
            System.out.println("線程"+Thread.currentThread().getName()+"繼續執行");
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println("進入線程:"+Thread.currentThread().getName());
            try {
                Thread.currentThread().sleep(5000);
            } catch (InterruptedException e) {
                // TODO: handle exception
            }
            System.out.println("線程"+Thread.currentThread().getName()+"執行完畢");
        }
    }
}

輸出結果:

進入線程main
線程main等待
進入線程Thread-0
線程Thread-0執行完畢
線程main繼續執行

  可以看出,當調用thread1.join()方法後,main線程會進入等待,然後等待thread1執行完之後再繼續執行。

  實際上調用join方法是調用了Object的wait方法,這個可以通過查看源碼得知:

public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
}

  wait方法會讓線程進入阻塞狀態,並且會釋放線程占有的鎖,並交出CPU執行權限。

  由於wait方法會讓線程釋放對象鎖,所以join方法同樣會讓線程釋放對一個對象持有的鎖。具體的wait方法使用在後面文章中給出。

  6)interrupt方法

  interrupt,顧名思義,即中斷的意思。單獨調用interrupt方法可以使得處於阻塞狀態的線程拋出一個異常,也就說,它可以用來中斷一個正處於阻塞狀態的線程;另外,通過interrupt方法和isInterrupted()方法來停止正在運行的線程。

  下面看一個例子:

import java.io.IOException;

public class Test {

    public static void main(String[] args) throws IOException {
        Test test = new Test();
        MyThread thread = test.new MyThread();
        thread.start();
        try {
            Thread.currentThread().sleep(2000);
        } catch (InterruptedException e) {

        }
        thread.interrupt();
    }

    class MyThread extends Thread {
        @Override
        public void run() {
            try {
                System.out.println("進入睡眠狀態");
                Thread.currentThread().sleep(10000);
                System.out.println("睡眠完畢");
            } catch (InterruptedException e) {
                System.out.println("得到中斷異常");
            }
            System.out.println("run方法執行完畢");
        }
    }
}

輸出結果:

進入睡眠狀態
得到中斷異常
run方法執行完畢

從這裏可以看出,通過interrupt方法可以中斷處於阻塞狀態的線程。那麽能不能中斷處於非阻塞狀態的線程呢?看下面這個例子:

public class Test {
     
    public static void main(String[] args) throws IOException  {
        Test test = new Test();
        MyThread thread = test.new MyThread();
        thread.start();
        try {
            Thread.currentThread().sleep(2000);
        } catch (InterruptedException e) {
             
        }
        thread.interrupt();
    } 
     
    class MyThread extends Thread{
        @Override
        public void run() {
            int i = 0;
            while(i<Integer.MAX_VALUE){
                System.out.println(i+" while循環");
                i++;
            }
        }
    }
}

運行該程序會發現,while循環會一直運行直到變量i的值超出Integer.MAX_VALUE。所以說直接調用interrupt方法不能中斷正在運行中的線程。

  但是如果配合isInterrupted()能夠中斷正在運行的線程,因為調用interrupt方法相當於將中斷標誌位置為true,那麽可以通過調用isInterrupted()判斷中斷標誌是否被置位來中斷線程的執行。比如下面這段代碼:

public class Test {
     
    public static void main(String[] args) throws IOException  {
        Test test = new Test();
        MyThread thread = test.new MyThread();
        thread.start();
        try {
            Thread.currentThread().sleep(2000);
        } catch (InterruptedException e) {
             
        }
        thread.interrupt();
    } 
     
    class MyThread extends Thread{
        @Override
        public void run() {
            int i = 0;
            while(!isInterrupted() && i<Integer.MAX_VALUE){
                System.out.println(i+" while循環");
                i++;
            }
        }
    }
}

  運行會發現,打印若幹個值之後,while循環就停止打印了。

  但是一般情況下不建議通過這種方式來中斷線程,一般會在MyThread類中增加一個屬性 isStop來標誌是否結束while循環,然後再在while循環中判斷isStop的值

class MyThread extends Thread{
        private volatile boolean isStop = false;
        @Override
        public void run() {
            int i = 0;
            while(!isStop){
                i++;
            }
        }
         
        public void setStop(boolean stop){
            this.isStop = stop;
        }
    }

那麽就可以在外面通過調用setStop方法來終止while循環。

  7)stop方法

  stop方法已經是一個廢棄的方法,它是一個不安全的方法。因為調用stop方法會直接終止run方法的調用,並且會拋出一個ThreadDeath錯誤,如果線程持有某個對象鎖的話,會完全釋放鎖,導致對象狀態不一致。所以stop方法基本是不會被用到的。

  8)destroy方法

  destroy方法也是廢棄的方法。基本不會被使用到。

  以下是關系到線程屬性的幾個方法:

  1)getId

  用來得到線程ID

  2)getName和setName

  用來得到或者設置線程名稱。

  3)getPriority和setPriority

  用來獲取和設置線程優先級。

  4)setDaemon和isDaemon

  用來設置線程是否成為守護線程和判斷線程是否是守護線程。

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

  Thread類有一個比較常用的靜態方法currentThread()用來獲取當前線程。

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

技術分享

參考:

http://www.cnblogs.com/dolphin0520/p/3920357.html

Java並發編程:Thread類的使用