Java 線程基礎,從這篇開始

線程做爲操做系統中最少調度單位,在當前系統的運行環境中,通常都擁有多核處理器,爲了更好的充分利用 CPU,掌握其正確使用方式,能更高效的使程序運行。同時,在 Java 面試中,也是極其重要的一個模塊。

線程簡介

一個獨立運行的程序是一個進程,一個進程中能夠包含一個或多個線程,每一個線程都有屬於本身的一些屬性,如堆棧,計數器等等。同時,一個線程在一個時間點上只能運行在一個 CPU 處理器核心上,不一樣線程之間也能夠訪問共享變量。線程在運行時,系統給每一個線程分配一些 CPU 時間片,CPU 在時間片這段時間運行某個線程,當這個時間片運行完又跳轉至下一段時間片線程,CPU 在這些線程中進行高速切換,使得程序像是在同時進行多個線程操做。java

線程的實現

實現線程經常使用的兩種方式:繼承 java.lang.Thread 類、實現 java.lang.Runnable 接口。面試

繼承 Thread 類方式

經過實例化 java.lang.Thread 類得到線程。建立 Thread 對象,通常使用繼承 Thread 類的方式,而後經過方法重寫覆蓋 Thread 的某些方法。segmentfault

首先建立一個繼承 Thread 的子類。網絡

public class DemoThread extends Thread{

    // 重寫 Thread 類中的 run 方法
    @Override
    public void run() {
        // currentThread().getName() 獲取當前線程名稱
        System.out.println("java.lang.Thread 建立的"+ currentThread().getName() +"線程");
    }
}

上面代碼 DemoThread 實例化的對象就表明一個線程,經過重寫 run 方法,在 run 方法中實現該線程的邏輯實現。多線程

public class Main {

    public static void main(String[] args) {
        // 實例化 DemoThread 獲得新建立的線程實例
        DemoThread thread = new DemoThread();
        // 給建立的子線程命名
        thread.setName("DemoThread 子線程");
        // 啓動線程
        thread.start();
        
        // 經過主線程打印信息
        System.out.println("main 線程");
    }

}

在程序執行的主線程中建立子線程,而且命名爲DemoThread 子線程,在程序的最後打印主線程打印的信息。調用線程必須調用start()方法,在調用此方法以前,子線程是不存在的,只有start()方法調用後,纔是真正的建立了線程。異步

執行結果:ide

從結果能夠看到,因爲在主線程中建立了一個子線程,子線程相對於主線程就至關因而一個異步操做,因此打印結果就有可能main線程先於子線程執行打印操做。測試

實現 Runnable 接口方式

因爲 Java 是單繼承的特性,因此當建立線程的子類繼承了其餘的類,就沒法實現繼承操做。這時就能夠經過實現 Runnable 接口,來實現線程建立的邏輯。this

首先建立一個實現 Runnable 的類。url

public class DemoRunnable implements Runnable {
    
    // 實現 Runnable 中的 run 方法
    @Override
    public void run() {
        System.out.println("java.lang.Runnable 建立的 "+ Thread.currentThread().getName() +"線程");
    }
}

Runnable 接口中定義有一個 run 方法,因此實現 Runnable 接口,就必須實現 run 方法。實際上 java.lang.Thread 類也實現了 Runnable 接口。

建立線程:

public class Main {

    public static void main(String[] args) {
        // 建立 Thread 實例,並給將要建立的線程給命名
        Thread thread = new Thread(new DemoRunnable(), "DemoRunnable 子線程");
        // 建立一個線程
        thread.start();

        System.out.println("main 線程");
    }

}

執行結果

一樣也實現了與繼承 Thread 方式同樣的結果。

建立 Thread 實例時,向新建立的 Thread 實例中傳入了一個實現 Runnable 接口的對象的參數。

Thread 中初始化 Thread#init 的具體實現:

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        // 給當前建立的 thread 實例中賦值線程名
        this.name = name;
        // 將要建立的線程的父線程即當前線程
        Thread parent = currentThread();
        // 添加到線程組操做
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            if (security != null) {
                g = security.getThreadGroup();
            }

            if (g == null) {
                g = parent.getThreadGroup();
            }
        }
        g.checkAccess();

        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }
        // 線程組中添加爲啓動的線程數
        g.addUnstarted();
        this.group = g;
        // 設置父線程的一些屬性到當前將要建立的線程
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        
        // 將當前傳入 target 的參數,賦值給當前 Thread 對象,使其持有 已實現 Runnable 接口的實例
        this.target = target;
        setPriority(priority);
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        
        // 設置線程的堆棧大小
        this.stackSize = stackSize;

        // 給建立的線程一個 id
        tid = nextThreadID();
    }

上面代碼建立 thread 對象時的 init 方法,經過傳入 Runnable 的實例對象,thread 對象中就持有該對象。

建立 thread 對象後,調用 start() 方法,該線程就運行持有 Runnable 實現類對象的 run() 方法。

例如本文中案例,就會執行 DemoRunnable#run 方法的邏輯。

這兩種方法建立線程的方式,具體使用哪一種,根據自身需求選擇。若是須要繼承其餘非 Thread 類,就須要使用 Runnable 接口。

線程狀態

Java 線程每一個時間點都存在於6種狀態中一種。

狀態 描述
NEW 初始狀態,thread 對象調用 start() 方法前
RUNNABLE 運行狀態,線程 start() 後的就緒或運行中
BLOCKED 阻塞狀態,線程得到鎖後的鎖定狀態
WAITING 等待狀態,線程進入等待狀態,不會被分配時間片,須要等待其餘線程來喚醒
TIME_WAITING 超時等待狀態,一樣不分配時間片,當時間達到設定的等待時間後自動喚醒
TERMINATED 終止狀態,表示當前線程執行完成

其中 NEW、RUNNABLE、TERMINATED 比較好理解,如今主要針對 BLOCKED、WAITING 和 TIME_WAITING 進行案例講解。

BLOCKED

阻塞狀態 是將兩個線程之間處於競爭關係,同時在調用 run 時進行加鎖。

首先仍是使用上面 Runnable 實現的方式進行改造。

public class DemoRunnable implements Runnable {

    @Override
    public void run() {
        // 經過對DemoRunnable加同步鎖,進行無限循環不退出
        synchronized (DemoRunnable.class){
            while (true){
                System.out.println("java.lang.Runnable 建立的 "+ Thread.currentThread().getName() +"線程");
            }
        }
    }
}

先競爭到 DemoRunnable 類的線程進入 run 會一直執行下去,未競爭到的線程則會一直處於阻塞狀態。

建立兩個線程

public class Main {
    
    public static void main(String[] args) {
        // 建立兩個線程測試
        new Thread(new DemoRunnable(), "test-blocked-1")
                .start();
        new Thread(new DemoRunnable(), "test-blocked-2")
                .start();
    }

}

經過分析執行後的線程如圖:

能夠得知線程test-blocked-1競爭到 DemoRunnable 類,一直都在運行 while 循環,因此狀態爲 RUNNABLE。因爲 DemoRunnable#run 中加了同步鎖鎖住 DemoRunnable 類,因此test-blocked-2一直處於 BLOCKED 阻塞狀態。

WAITING

等待狀態 線程是不被分配 CPU 時間片,線程若是要從新被喚醒,必須顯示被其它線程喚醒,不然會一直等待下去。

實現等待狀態例子

public class DemoRunnable implements Runnable {

    @Override
    public void run() {
        while (true){
            // 調用 wait 方法,使線程在當前實例上處於等待狀態
            synchronized (this){
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("java.lang.Runnable 建立的 "+ Thread.currentThread().getName() +"線程");
            }
        }
    }
}

// 建立線程
public class Main {
    
    public static void main(String[] args) {
        new Thread(new DemoRunnable(), "test-waiting")
                .start();
    }

}

建立該實例線程後,分析 test-waiting 線程,該線程處於 WAITING 狀態。

TIME_WAITING

超時等待狀態 線程也是不被分配 CPU 時間片,可是它經過設置的間隔時間後,能夠自動喚醒當前線程。也就是說,將等待狀態的線程加個時間限制就是超時等待狀態。

只需對上面 WAITING 狀態案例增長 wait 時間限制。

public class DemoRunnable implements Runnable {

    @Override
    public void run() {
        while (true){
            synchronized (this){
                try {
                    // 增長等待時長
                    this.wait(1000000, 999999);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("java.lang.Runnable 建立的 "+ Thread.currentThread().getName() +"線程");
            }
        }
    }
}

分析線程結果,能夠看到 test-time_waiting 線程處於超時等待狀態,使用 sleep 睡眠時,線程也是屬於超時等待狀態。

線程狀態之間的轉換,如圖(來源網絡):

Thread 經常使用方法

currentThread()

currentThread 是獲取當前線程實例,返回 Thread 對象,這是一個靜態方法,使用以下

Thread.currentThread();

start()

start 方法是啓動線程的入口方法,這個就是上面實現建立線程例子中的 start 方法。

run()

run 方法是線程建立後,線程會主動調用 run 方法執行裏面的邏輯。

join()

join 方法即線程同步,好比上繼承 Thread 方法實現建立線程的例子中,若是在 thread.start() 後調用 thread.join() 方法,則 main 線程打印的信息必定在子線程打印的信息以後。這裏的 main 線程會等待子線程執行完後,再繼續執行。

getName()

getName 返回線程名稱。

getId()

獲取線程 Id,這是返回一個 long 類型的 Id 值。

setDaemon()

setDaemon(boolean on) 方法是設置線程類型,setDaemon 接受一個 boolean 類型參數。設置爲 true 時,線程類型爲守護線程,設置爲 false 時,線程類型爲用戶線程。

yield()

yield 方法是線程讓步,讓當前線程進入就緒狀態,去執行其它相同優先級的線程,但不必定會執行其餘線程,有可能讓步後的線程再次被執行。

setPriority()

setPriority(int newPriority) 是設置線程執行的優先級,數值爲1~10,默認值爲5,數值越大線程越先執行。

interrupt()

interrupt 方法的做用是中斷線程,可是它仍是會繼續運行。它只是表示其餘線程給打了箇中斷標誌。

interrupted()

interrupted 方法是檢查當前線程是否被中斷。調用此方法時會清除該線程的中斷標誌。

isInterrupted()

isInterrupted 方法檢測當前線程是否被中斷,若是被中斷了,也不會清除中斷標誌。

總結

本文對線程的經常使用功能及概念進行了分析,主要是講解單線程的一些操做,線程操做的使用在生產中是極容易出現問題的,因此在掌握概念和使用後,須要多研究,多思考應用的設計及實現。在掌握多線程操做時,必須對這些的基本使用和概念進行掌握,從此會出進一步對多線程分析的文章。

推薦閱讀

《你必須會的 JDK 動態代理和 CGLIB 動態代理》

《Dubbo 擴展點加載機制:從 Java SPI 到 Dubbo SPI》

《volatile 手摸手帶你解析》

相關文章
相關標籤/搜索