線程詳解

1. 概念

線程,CPU調度的基本單位,被包裹在進程裏面,同一個進程裏的線程共享同一片內存空間。其中,守護線程是特殊的線程,守護線程自創建伊始會在後臺爲用戶線程提供服務,其生命貫徹進程的整個生命週期。

2. 特點

  • 輕型實體:這種輕體現在線程是程序執行流的最小單位,執行時僅向系統請求執行所必須的一點兒資源。
  • 共享進程內存空間:同一個進程裏的線程擁有同一個內存空間地址(進程空間地址),共享這片內存空間,同一個進程裏的線程互相通信不需要調用內核。
  • 併發執行:線程通過互奪CPU資源實現併發執行,這種併發效果不是嚴格意義的同時執行,而是在多個線程之間高速切換,以至於給人併發執行的錯覺。

3. 組成

  • 線程ID:線程標識符
  • 當前指令指針(PC)
  • 寄存器集合:存儲單元寄存器的集合
  • 堆棧:兩種數據結構

4. 狀態

  • 運行:線程佔有處理機正在執行
  • 阻塞:線程在等待某個事件(如某個信號量),邏輯上不可執行
  • 就緒:線程一切準備就緒,等待處理機執行,邏輯上可執行

5. 生命週期

  1. 新建狀態
    • 通過new方法新建一個線程,該線程被加載進堆內存,此時的線程不具有運行所必須的資源
  2. 可運行態
    • 調用線程的start()方法,使該線程獲得運行所需的一點系統資源,同時調用run()方法。
  3. 不可運行態
    • 以下方法會使線程進入不可運行態
      • 線程調用sleep()方法進入睡眠
      • 線程調用wait()方法
      • 發生I/O阻塞
    • 以下方法可使線程脫離不可運行態
      • sleep()時間結束
      • 被notify()喚醒
      • 等待輸入輸出完成
  4. 消亡態
    • 當線程的run()方法走到生命的盡頭,線程進入消亡態,消亡了的線程不能再被start()

線程的生命週期

5. 多線程

  • 多個線程併發執行的過程稱作多線程

6. Java實現多線程的兩個方法

1、 繼承Thread類,重寫run()方法

代碼示例

class SubThread extends Thread{

    public void run(){
        ... ; //線程體,線程所要實現的功能
    }
}

public class TestThread{
    public static void main(String[] args){
        SubThread st = new SubThread();
        st.start();
    }
}

2、 實現Runable方法,重寫run()方法,並使用Thread構造函數構造線程

代碼示例

class SubThread implememts Runable{

    public void run(){
        ... ; //線程體,線程所要實現的功能
    }
}

public class TestThread{

    public static void main(String[] args){

        SubThread r = new SubThread();
        Thread st = new Thread(r);
        st.start()
    }

7. 繼承 VS 實現

  • 實現的方式優於繼承的方式,原因如下:

    1. 繼承實現多線程難免會走進單繼承的尷尬,而實現的方式則可以避免這個情況實現多繼承。
    2. 在子線程中需要對共享數據進行操作的時候,實現的方式只要在實現Runable的類中定義一個普通的變量就可以實現數據共享,因爲在實例化時實現Runable的對象只被創建一次,之後以該對象爲參數實例化的線程共享同一片內存數據;而繼承的方式每次實例化一個對象會重新開闢一個內存空間,如果要操作同一片內存空間就不得不用static修飾,這種方式修飾的內存空間生命週期長。

8. 線程的方法

  1. start():開啓一個線程,獲得運行所需的系統資源;調用run()方法
  2. run():線程體,線程要實現的主體功能
  3. sleep(Long l):顯式地讓線程睡眠l毫秒,睡眠時讓出CPU執行權
  4. join():暫停當前線程,讓調用該方法的線程參與進來,並在該線程執行結束後纔開始執行當前線程
  5. currentThread():返回當前佔有處理機的線程
  6. setName():設置線程名字
  7. getName():返回線程名字
  8. yield():讓出CPU的執行權,但並不是說讓出CPU執行權就一定是下一個線程執行,因爲有可能讓出執行權後又搶到執行權,繼續執行。
  9. isAlive():判斷一個線程是否消亡

代碼示例

public class TestThread{

    public static void main(String[] args){

        SubThread st = new SubThread();  
        st.start();
        //st.run();   該方法僅僅是調用st對象的run()方法,而不是一個線程,執行這一步的還是主線程
        //st.start();   一個線程在消亡之後就等同於人類的死亡,是不會有重新開始的
            st.sleep(1000); //這裏要闡述線程和對象的關係,線程是線程,對象是對象,即使這裏使用SubThread對象
                            //調用的sleep()方法,但是實際上是主線程在執行這個方法,兩者不衝突
    }
}

9. 線程的優先級

線程的優先級並不是絕對的優先,而是混沌的。被給予了高優先級的線程僅僅只是在概率上有較大概率搶到CPU執行權,而非絕對。線程預設的優先級別有以下三個:

  • NORM_PRIORITY = 5
  • MIN_PRIORITY = 1
  • MAX_PRIORITY = 10

其中,一般我們創建一個線程優先級都爲NORM_PRIORITY。

設置和獲得線程優先級的方法如下:

  • setPriority(int i)
  • getPriority()

PS:線程創建時繼承父類線程優先級。線程優先級在1~10之間,離開這個範圍虛擬機會報java.lang.IllegalArgumentException錯誤。

10. 線程的同步機制

1. 線程的安全問題

代碼示例

class RunnableImpl implements Runnable{

    private int num = 20;

    public void run(){
        while(true){
            if(num >= 1){
                try {
                    Thread.currentThread().sleep(10);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                System.out.println(num--);
            }else{
                break;
            }
        }
    }
}

public class TestThread{

    public static void main(String[] args){

        RunnableImpl ri = new RunnableImpl();
        Thread t1 = new Thread(ri);
        Thread t2 = new Thread(ri);
        t1.start();
        t2.start(); 
    }
}

上述程序其中一次輸出結果如下:

20
19
18
17
16
15
14
13
12
11
10
10
9
8
7
6
5
4
3
2
1
1

由此可見上述程序是存在安全隱患的,程序的本意是讓兩個線程相互協作打印出20到1,然而由程序可以看出,出現了一些重複值,這些重複值的出現意味着該程序是線程不安全的。
出現上述線程安全的原因是:當線程t1通過if條件判斷後,還沒來得及執行num--的的操作就失去了CPU操作權,而後線程t2進來自然會導致一些數字被打印多次。解決這種線程不安全的問題的關鍵是:當某個線程進入某個共享區域後,對該區域數據進行操作期間其他線程必須不能再進入該區域,直到這個線程對該區域的操作完畢。

2. 解決線程安全問題的兩個方案

  • 當多個線程嘗試操作共享數據時,將操作共享數據的代碼塊用synchrosized(mutex)聲明爲同步代碼塊。

代碼示例

class RunnableImpl implements Runnable{

    private int num = 20;

    public void run(){
        while(true){
            synchronized(this){
                if(num >= 1){
                    try {
                        Thread.currentThread().sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(num--);
                }else{
                    break;
                }
            }
        }
    }
}

public class TestThread{

    public static void main(String[] args){

        RunnableImpl ri = new RunnableImpl();
        Thread t1 = new Thread(ri);
        Thread t2 = new Thread(ri);
        t1.start();
        t2.start(); 
    }
}

其中mutex是互斥鎖,它可以是任意對象,但必須是同一對象,不同對象代表不同鎖。在這裏用this表示用RunnableImpl實例化的對象本身。

  • 將對共享數據操作部分封裝成一個方法,用synchrosized修飾。

代碼示例

class RunnableImpl implements Runnable{

    private int num = 20;

    public void run(){
        while(num >= 1){
            printNum();
        }
    }

    public synchronized void printNum(){
        try {
            Thread.currentThread().sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(num--);
    }
}

public class TestThread{

    public static void main(String[] args){

        RunnableImpl ri = new RunnableImpl();
        Thread t1 = new Thread(ri);
        Thread t2 = new Thread(ri);
        t1.start();
        t2.start(); 
    }
}

上述解決方案是將共享數據操作放進一個synchronized聲明的方法中,該解決方案的互斥鎖默認爲this。

其實,解決這種線程問題的方法就是提出“鎖”的概念,當一個線程進入共享數據操作時就上鎖(lockd),直到該線程對操作完畢。實質上當某個線程進入被synchronized修飾的代碼塊或方法時,該方法塊的狀態改變,只有被同步監視器(mutex)標記的線程可進入該區域,當此條線程離開,狀態恢復。示意圖如下(只是想試試GIF圖製作,請多包涵):

這裏寫圖片描述

11. 線程的通信

線程的通信主要是三個方法:

  • wait():使線程掛起,並交出CPU執行權,被掛起的線程會被放進等待隊列中等待喚醒
  • notify():喚醒等待隊列中優先級最高的一個線程
  • notifyAll():喚醒所有線程

首先,這些方法不是Java.lang.Thread裏的方法,而是Java.lang.Object的方法,也就是所有的對象都具有該方法,但該方法只能使用在同步代碼塊或同步方法中。

發佈了34 篇原創文章 · 獲贊 3 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章