java線程

前言

在java中,線程非常重要,我們要分清楚進程和線程的區別:進程是指一個內存中運行的應用程序,每個進程都擁有自己的一塊獨立的內存空間,進程之間的資源不共享;線程是CPU調度的最小單元,一個進程可以有多個線程,線程之間的堆空間是共享的,但棧空間是獨立的,java程序的進程至少包含主線程和後臺線程(垃圾回收線程)。瞭解這些知識後,來看下文有關線程的知識。

併發和並行

我們先來看一下概念:

  • 並行:指兩個或多個事件在同一時刻點發生
  • 併發:指兩個或多個事件在同一時間段內發生

對於單核CPU的計算機來說,它是不能並行的處理多個任務,它的每一時刻只能有一個程序執行時間片(時間片是指CPU分配給各個程序的運行時間),故在微觀上這些程序只是分時交替的運行,所以在宏觀看來在一段時間內有多個程序在同時運行,看起來像是並行運行。

對於多核CPU的計算機來說,它就可以並行的處理多個任務,可以做到多個程序在同一時刻同時運行。

同理對線程也一樣,但系統只有一個CPU時,線程會以某種順序執行,我們把這種情況稱爲線程調度,所以從宏觀角度上看線程是並行運行的,但是從微觀角度來看,卻是串行運行,即一個線程一個線程的運行。

線程的創建與啓動

有3種方式使用線程。

方式1:繼承Thread類

定義一個類繼承java.lang.Thread類,重寫Thread類中的run方法,如下:

public class MyThread extends Thread {
    public void run() {
        // ...
    }
}

//使用線程
public static void main(String[] args) {
    Thread thread = new MyThread();
     thread.start();
}

方式2:實現Runnable接口

2.1:定義一個類實現Runnable接口

實現 Runnable只能當做一個可以在線程中運行的任務,不是真正意義上的線程,因此最後還需要通過 Thread 來調用,如下:

public class MyRunnable implements Runnable {
    public void run() {
        // ...
    }
}

//使用線程
public static void main(String[] args) {
    MyRunnable instance = new MyRunnable();
    Thread thread = new Thread(instance);
    thread.start();
}

2.2、使用匿名內部類

這種方式只適用於這個線程只使用一次的情況,如下:

public class MyRunnable implements Runnable {

//使用線程
public static void main(String[] args) {
    new Thread(new Runnable(){
        public void run(){
            // ...
        }
    }).start();
}

方式3:實現Callable接口

與 Runnable 相比,Callable 可以有返回值,返回值通過 FutureTask 進行封裝,所以在創建Thread時,要把FutureTask 傳進去,如下:

public class MyCallable implements Callable<Integer> {
    public Integer call() {
        return 123;
    }
}

//使用線程
public static void main(String[] args) throws ExecutionException, InterruptedException {
    MyCallable mc = new MyCallable();
    FutureTask<Integer> ft = new FutureTask<>(mc);
    Thread thread = new Thread(ft);
    thread.start();
    System.out.println(ft.get());
}

繼承與實現的區別

1、繼承方式:

(1)java中類是單繼承的,如果繼承了Thread,該類就不能有其他父類了,但是可以實現多個接口

(2)從操作上分析,繼承方式更簡單,獲取線程名字也簡單

2、實現方式:

(1)java中類可以實現多接口,此時該類還可以繼承其他類,並且還可以實現其他接口

(2)從操作上分析,實現方式稍複雜,獲取線程名字也比較複雜,得通過Thread.currentThread來獲取當前線程得引用

綜上所述,實現接口會更好一些。

線程的生命週期

線程也是有生命週期,也就是存在不同的狀態,狀態之間相互轉換,線程可以處於以下的狀態之一:

1、NEW(新建狀態)

使用new創建一個線程對象,但還沒有調用線程的start方法,Thread t = new Thread(),此時屬於新建狀態。

2、RUNNABLE(可運行狀態)

但在新建狀態下線程調用了start方法,t.start(),此時進入了可運行狀態。可運行狀態又分爲兩種狀態:

  • ready(就緒狀態):線程對象調用stat方法後,等待JVM的調度,此時線程並沒有運行。
  • running(運行狀態):線程對象獲得JVM調度,此時線程開始運行,如果存在多個CPU,那麼允許多個線程並行運行。

線程的start方法只能調用一次,否則報錯(IllegalThreadStateException)。

3、BLOCKED(阻塞狀態)

正在運行的線程因爲某些原因放棄CPU,暫時停止運行,就會進入阻塞狀態,此時JVM不會給該線程分配CPU,直到線程重新進入就緒狀態,纔有機會轉到運行狀態,阻塞狀態只能先進入就緒狀態,不能跳過就緒狀態直接進入運行狀態。線程進入阻塞狀態常見的情況有:

  • 1、當A線程處於運行狀態時,試圖獲取同步鎖,卻被B線程獲取,此時JVM把當前A線程放到對象的鎖池中,A線程進入阻塞狀態,等待獲取對象的同步鎖。
  • 2、當線程處於運行狀態時,發出了IO請求,此時進入阻塞狀態。

4、WAITING(等待狀態)

正在運行的線程調用了無參數的wait方法,此時JVM把該線程放入對象的等待池中,此時線程進入等待狀態,等待狀態的線程只能被其他線程喚醒,否則不會被分配 CPU 時間片。下面是讓線程進入等待狀態的方法:

進入方法 退出方法
無Timeout參數的Object.wait() Object.notify() / Object.notifyAll()
無Timeout參數的Thread.join() 方法 被調用的線程執行完畢
LockSupport.park() 方法 LockSupport.unpark(Thread)

5、TIMED WAITING(計時等待狀態)

正在運行的線程調用了有參數的wait方法,此時JVM把該線程放入對象的等待池中,此時線程進入計時等待狀態,計時等待狀態的線程被其它線程顯式地喚醒,在一定時間之後會被系統自動喚醒。下面是讓線程進入等待狀態的方法:

進入方法 退出方法
調用Thread.sleep(int timeout) 方法 時間結束
有Timeout 參數的 Object.wait() 方法 時間結束 / Object.notify() / Object.notifyAll()
有Timeout 參數的 Thread.join() 方法 時間結束 / 被調用的線程執行完畢
LockSupport.parkNanos() 方法 LockSupport.unpark(Thread)
LockSupport.parkUntil() 方法 LockSupport.unpark(Thread)

ps:阻塞和等待的區別在於,阻塞是被動的,它是在等待獲取一個排它鎖。而等待是主動的,通過調用 Thread.sleep() 和 Object.wait() 等方法進入。

6、TREMINATED(終止狀態)

又稱死亡狀態,表示線程的終止。線程進入終止狀態的情況有:

  • 1、正常執行完run方法,線程正常退出。
  • 2、遇到異常而退出

線程一旦終止了,就不能再次啓動,否則報錯(IllegalThreadStateException)

線程的狀態轉換圖

線程的生命週期圖

Thread類中過時的方法

因爲存在線程安全問題,所以棄用了,如下:

  • void suspend():暫停當前線程。
  • void resume():恢復當前線程。
  • void stop():結束當前線程

線程之間的通信

如果一個線程從頭到尾的執行完一個任務,不需要和其他線程打交道的話,那麼就不會存在安全性問題了,由於java內存模式的存在,如下:

線程、主內存、工作內存三者之間的交互關係

每一個java線程都有自己的工作內存,線程之間要想協作完成一個任務,就必須通過主內存來通信,所以這裏就涉及到對共享資源的競爭,在主內存中的東西都是線程之間共享,所以這裏就必須通過一些手段來讓線程之間完成正常通信。主要有以下兩種方法:

1、wait() / notify() notifyAll() 機制

它們都是Object類中的方法,它們的主要作用如下:

  • wait():執行該方法的線程對象釋放同步鎖(這是因爲,如果沒有釋放鎖,那麼其它線程就無法進入對象的同步方法或者同步控制塊中,那麼就無法執行 notify() 或者 notifyAll() 來喚醒掛起的線程,造成死鎖),然後JVM把該線程存放在等待池中,等待其他線程喚醒該線程
  • notify():執行該方法的線程喚醒在等待池中等待的任意一個線程,把線程轉到鎖池中等待
  • notifyAll():執行該方法的線程喚醒在等待池中等待的所有線程,把線程轉到鎖池中等待

注意:上述方法只能在同步方法或者同步代碼中使用,否則會報IllegalMonitorStateException異常,還有上述方法只能被同步監聽鎖對象來調用,不能使用其他對象調用,否則會報IllegalMonitorStateException異常。

假設A線程和B線程共同操作一個X對象,A、B線程可以通過X對象的wait方法和notify方法進行通信,流程如下:

1、當A線程執行X對象的同步方法時,A線程持有X對象的鎖,則B線程沒有執行同步方法的機會,B線程在X對象的鎖池中等待。

2、A線程在同步方法中執行X.wait()時,A線程釋放X對象的鎖,進入X對象的等待池中。

3、在X對象的鎖池中等待獲取鎖的B線程在這時獲取X對象的鎖,執行X對象的另一個同步方法。

4、B線程在同步方法中執行X.notify()或notifyAll()時,JVM把A線程從X對象的等待池中移到X對象的鎖池中,等待獲取鎖。

5、B線程執行完同步方法,釋放鎖,A線程獲取鎖,從上次停下來的地方繼續執行同步方法。

下面以一個ATM機存錢取錢的例子說明,ATM機要在銀行把錢存進去後,其他人才能取錢,如果沒錢取,只能先回家等待,等銀行通知你有錢取了,再來取,如果有錢取,就直接取錢。

ATM機,存錢和取錢方法都是同步方法:

public class ATM {

    private int money;
    private boolean isEmpty = true;//標誌ATM是否有錢的狀態

    /**
     * 往ATM機中存錢
     */
    public synchronized void push(int money){

        try{
            //ATM中有錢,等待被人把錢取走
            while (!isEmpty){
                this.wait();
            }

            //ATM中沒錢了,開始存錢
            System.out.println(Thread.currentThread().getName() + ":" + "發現ATM機沒錢了,存錢中...");
            Thread.sleep(2000);
            this.money = money;
            System.out.println(Thread.currentThread().getName() + ":" + "存錢完畢,存了" + money + "元");

            //存錢完畢,把標誌置爲false
            isEmpty = false;

            //ATM中有錢了,通知別人取錢
            this.notify();

        }catch (InterruptedException e){
            e.printStackTrace();
        }

    }

    /**
     * 從ATM機中取錢
     */
    public synchronized void pop(){
            try {

                //ATM中沒錢取,等待通知
                while (isEmpty){
                    System.out.println(Thread.currentThread().getName() + ":" + "ATM機沒錢,等待中...");
                    this.wait();
                }

                //ATM中有錢了,開始取錢
                System.out.println(Thread.currentThread().getName() + ":" + "收到通知,取錢中...");
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() + ":"+ "取出完畢,取出了" + this.money + "錢");

                //取錢完畢,把標誌置爲true
                isEmpty = true;

                //ATM沒錢了,通知銀行存錢
                this.notify();

            } catch (InterruptedException e) {
                e.printStackTrace();
        }
    }

}

銀行, 需要傳入同一個ATM示例:

public class Blank implements  Runnable  {

    private ATM mAtm;//共享資源

    public Blank(ATM atm){
        this.mAtm = atm;
    }

    @Override
    public void run() {
        //銀行來存錢
        for(int i = 0; i < 2; i++){
            mAtm.push(100);
        }
    }
}

小明, 需要傳入同一個ATM示例:

public class Person implements Runnable{

    private ATM mAtm;//共享資源

    public Person(ATM atm){
        this.mAtm = atm;
    }

    @Override
    public void run() {
        //這個人來取錢
        mAtm.pop();
    }
}

客戶端操作,我特地讓小明提前來取錢,此時ATM機中是沒錢的,小明要等待:

 public static void main(String[] args){

        //創建一個ATM機
        ATM atm = new ATM();
        //小明來取錢
        Thread tPerson = new Thread(new Person(atm), "XiaoMing");
        tPerson.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //銀行來存錢
        Thread tBlank = new Thread(new Blank(atm), "Blank");
        tBlank.start();
    }

輸出結果:

XiaoMing:ATM機沒錢,等待中...
Blank:發現ATM機沒錢了,存錢中...
Blank:存錢完畢,存了100元
XiaoMing:收到通知,取錢中...
XiaoMing:取出完畢,取出了100錢
Blank:發現ATM機沒錢了,存錢中...
Blank:存錢完畢,存了100

可以看到,小明總是在收到ATM的通知後纔來取錢,如果通過這個存錢取錢的例子還不瞭解wait/notify機制的話,可以看看這個修廁所的例子

ps: wait() 和 sleep() 的區別是什麼,首先wait()是Object的方法,而sleep()是Thread的靜態方法,其次調用wait()會釋放同步鎖,而sleep()不會,最後一點不同的是調用wait方法需要先獲得鎖,而調用sleep方法是不需要的。

2、await() / signal() signalAll()機制

從java5開始,可以使用Lock機制取代synchronized代碼塊和synchronized方法,使用java.util.concurrent 類庫中提供的Condition 接口的await / signal() signalAll()方法取代Object的wait() / notify() notifyAll() 方法。

下面使用Lock機制和Condition 提供的方法改寫上面的那個例子,如下:

ATM2:

public class ATM2 {

    private int money;
    private boolean isEmpty = true;//標誌ATM是否有錢的狀態

    private Lock mLock = new ReentrantLock();//新建一個lock
    private Condition mCondition = mLock.newCondition();//通過lock的newCondition方法獲得一個Condition對象

    /**
     * 往ATM機中存錢
     */
    public void push(int money){
        mLock.lock();//獲取鎖
        try{
            //ATM中有錢,等待被人把錢取走
            while (!isEmpty){
                mCondition.await();
            }

            //ATM中沒錢了,開始存錢
            System.out.println(Thread.currentThread().getName() + ":" + "發現ATM機沒錢了,存錢中...");
            Thread.sleep(2000);
            this.money = money;
            System.out.println(Thread.currentThread().getName() + ":" + "存錢完畢,存了" + money + "元");

            //存錢完畢,把標誌置爲false
            isEmpty = false;

            //ATM中有錢了,通知別人取錢
            mCondition.signal();

        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            mLock.unlock();//釋放鎖
        }

    }

    /**
     * 從ATM機中取錢
     */
    public void pop(){
        mLock.lock();//獲取鎖
        try {

            //ATM中沒錢取,等待通知
            while (isEmpty){
                System.out.println(Thread.currentThread().getName() + ":" + "ATM機沒錢,等待中...");
                 mCondition.await();
            }

            //ATM中有錢了,開始取錢
            System.out.println(Thread.currentThread().getName() + ":" + "收到通知,取錢中...");
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + ":"+ "取出完畢,取出了" + this.money + "錢");

            //取錢完畢,把標誌置爲true
            isEmpty = true;

            //ATM沒錢了,通知銀行存錢
            mCondition.signal();

        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            mLock.unlock();//釋放鎖
        }
    }
}

可以看到ATM2改寫ATM後,把方法的synchronized去掉,因爲Lock機制沒有同步鎖的概念,然後獲取lock鎖,在finally裏釋放lock鎖,還把原本Object.wait()用Condition.await()代替,原本Object.notify()用Condition.signal()代替。

客戶端操作只是把ATM換成ATM2,輸出結果和上面的一樣,就不在累述。

死鎖

多線程通信的時候很容易造成死鎖,死鎖無法解決,只能避免。

1、死鎖是什麼?

當A線程等待獲取由B線程持有的鎖,而B線程正在等待獲取由A線程持有的鎖,發生死鎖現象,JVM既不檢測也不會避免這種情況,所以程序員必須保證不導致死鎖。

2、如何避免死鎖?

1、當多個線程都要訪問共享資源A、B、C時,保證每一個線程都按照相同的順序去訪問去訪問他們,比如先訪問A,接着訪問B,最後訪問C。

2、不要使用Thread類中過時的方法,因爲容易導致死鎖,所以被廢棄,例如A線程獲得對象鎖,正在執行一個同步方法,如果B線程調用A線程的suspend(),此時A線程暫停運行,放棄CPU,但是不會放棄鎖,所以B就永遠不會得到A持有的鎖。

線程的控制操作

下面來看一些可以控制線程的操作。

1、線程休眠

讓執行的線程暫停等待一段時間,進入計時等待狀態,使用如下:

public static void main(String[] args){
    Thread.sleep(1000);
}

調用sleep()後,當前線程放棄CPU,在指定的時間段內,sleep所在的線程不會獲得執行的機會,在此狀態下該線程不會釋放同步鎖。

2、聯合線程

在線程中調用另一個線程的 join() 方法,會將當前線程置於阻塞狀態,等待另一個線程完成後才繼續執行,使用如下:

public class JoinThread extends Thread {

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("JoinThread執行完畢!");
    }
}

 public static void main(String[] args) throws InterruptedException {
        JoinThread joinThread = new JoinThread();
        joinThread.start();
        System.out.println("主線程等待...");
        joinThread.join();//主線程等join線程執行完畢後才繼續執行
        System.out.println("主線程執行完畢");
    }

輸出結果:
主線程等待...
JoinThread執行完畢!
主線程執行完畢

對於以上代碼,主線程會等join線程執行完畢後才繼續執行,因此最後的結果能保證join線程的輸出先於主線程的輸出。

3、後臺線程

顧名思義,在後臺運行的線程,其目的是爲其他線程提供服務,也稱“守護線程”,JVM的垃圾回收線程就是典型的後臺線程,通過**t.setDaemon(true)**把一個線程設置爲後臺線程,如下:

public class DeamonThread extends Thread {
    @Override
    public void run() {
        System.out.println(getName());
    }
}

 public static void main(String[] args) throws InterruptedException {
        //主線程不是後臺線程,是前臺線程
        DeamonThread deamonThread = new DeamonThread();
        deamonThread.setDaemon(true);//設置子線程爲後臺線程
        deamonThread.start();
        //通過deamonThread.isDaemon()判斷是否是後臺線程
        System.out.println(deamonThread.isDaemon());
    }

輸出結果:true

後臺線程有以下幾個特點:

1、若所有的前臺線程死亡,後臺線程自動死亡,若前臺線程沒有結束,後臺線程是不會結束的。

2、前臺線程創建的線程默認是前臺線程,可以通過setDaemon(true)設置爲後臺線程,在後臺線程創建的新線程,新線程是後臺線程。

注意:**t.setDaemon(true)**方法必須在start方法前調用,否則會報IllegalMonitorStateException異常

4、線程禮讓

對靜態方法 Thread.yield() 的調用,聲明瞭當前線程已經完成了生命週期中最重要的部分,可以切換給其它線程來執行。如下:

public class YieldThread extends Thread {
    @Override
    public void run() {
        System.out.println("已經完成重要部分,可以讓其他線程獲取CPU時間片");
        Thread.yield();
    }
}

該方法只是對線程調度器的一個建議,而且也只是建議具有相同優先級的其它線程可以運行。也就是說,就算你執行了這個方法,該線程還是有可能繼續運行下去。

5、線程組

java.lang.ThreadGroup類表示線程組,可以對一組線程進行集中管理,當用戶在創建線程對象時,可以通過構造器指定其所屬的線程組:Thread(ThreadGroup group, String name)。

如果A線程創建B線程,如果沒有設置B線程的分組,那麼B線程加入到A線程的線程組,一旦線程加入某個線程組,該線程就一直存在於該線程組中直到線程死亡,不能在中途修改線程的分組。

當java程序運行時,JVM會創建名爲main的線程組,在默認情況下,所以的線程都屬於該線程組。

結語

限於篇幅,還有ThreadLocal線程池的知識點沒寫,就留到下一篇了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章