Java併發程式設計(一)Thread詳解

王磊的部落格發表於2018-10-09

一、概述

在開始學習Thread之前,我們先來了解一下 執行緒和程式之間的關係:

執行緒(Thread)是程式的一個實體,是CPU排程和分派的基本單位。 執行緒不能夠獨立執行,必須依存在應用程式中,由應用程式提供多個執行緒執行控制。 執行緒和程式的關係是:執行緒是屬於程式的,執行緒執行在程式空間內,同一程式所產生的執行緒共享同一記憶體空間,當程式退出時該程式所產生的執行緒都會被強制退出並清除。

由上描述,可以得知執行緒作為cpu的基本排程單位,只有把多執行緒用好,才能充分利用cpu的多核資源。

本文基於JDK 8(也可以叫JDK 1.8)。

二、執行緒使用

2.1 啟動執行緒

建立執行緒有四種方式:

  • 實現Runnable介面
  • 繼承Thread類
  • 使用JDK 8 的Lambda
  • 使用Callable和Future

2.1.1 Runnable建立方式

public class MyThread implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}
複製程式碼
Thread thread = new Thread(new MyThread());
thread.start();
複製程式碼

2.1.2 繼承Thread建立方式

public class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}
複製程式碼
MyThread thread = new MyThread();
thread.start();
複製程式碼

以上程式碼有更簡單的寫法,如下:

Thread thread = new Thread(){
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
};
thread.start();
複製程式碼

2.1.3 Lambda建立方式

new Thread(()-> System.out.println(Thread.currentThread().getName())).start();
複製程式碼

2.1.4 使用Callable和Future

看原始碼可以知道Thread的父類是Runnable是JDK1.0提供的,而Callable和Runnable類似,是JDK1.5提供的,彌補了呼叫執行緒沒有返回值的情況,可以看做是Runnable的一個補充,下面看看Callable的實現。

public class MyThread implements Callable<String> {

    @Override
    public String call() throws Exception {
        System.out.println(Thread.currentThread().getName());
        return Thread.currentThread().getName();
    }
}
複製程式碼
Callable<String> callable = new MyThread();
FutureTask<String> ft = new FutureTask<>(callable);
new Thread(ft,"threadName").start();
System.out.println(ft.get());
複製程式碼

2.1.5 run()和start()的區別

真正啟動執行緒的是start()方法而不是run(),run()和普通的成員方法一樣,可以重複使用,但不能啟動一個新執行緒。

2.2 Thread的常用方法

Thread類方法

方法 說明
start() 啟動執行緒
setName(String name) 設定執行緒名稱
setPriority(int priority) 設定執行緒優先順序,預設5,取值1-10
join(long millisec) 掛起執行緒xx毫秒,引數可以不傳
interrupt() 終止執行緒
isAlive() 測試執行緒是否處於活動狀態

Thread靜態(static)方法

方法 說明
yield() 暫停當前正在執行的執行緒物件,並執行其他執行緒。
sleep(long millisec)/sleep(long millis, int nanos) 掛起執行緒xx秒,引數不可省略
currentThread() 返回對當前正在執行的執行緒物件的引用
holdsLock(Object x) 當前執行緒是否擁有鎖

2.3 sleep()和wait()的區別

sleep為執行緒的方法,而wait為Object的方法,他們的功能相似,最大本質的區別是:sleep不釋放鎖,wait釋放鎖。

用法上的不同:sleep(milliseconds)可以用時間指定來使他自動醒過來,如果時間不到你只能呼叫interreput()來終止執行緒;wait()可以用notify()/notifyAll()直接喚起。

重點: 測試wait和sleep釋放鎖的程式碼如下:

public class SynchronizedTest extends Thread {
    int number = 10;
    public synchronized void first(){
        System.out.println("this is first!");
        number = number+1;
    }
    public synchronized void secord() throws InterruptedException {
        System.out.println("this is secord!!");
        Thread.sleep(1000);
//        this.wait(1000);
        number = number*100;
    }
    @Override
    public void run() {
        first();
    }
}
複製程式碼
SynchronizedTest synchronizedTest = new SynchronizedTest();
synchronizedTest.start();
synchronizedTest.secord();
// 主執行緒稍等10毫秒
Thread.sleep(10);
System.out.println(synchronizedTest.number);
複製程式碼

根據結果可以得知:

  • 執行sleep(1000)執行的結果是:1001
  • 執行wait(1000)執行的結果是:1100

總結: 使用 sleep(1000)不釋放同步鎖,執行的是10*100+1=1001,wait(1000)釋放了鎖,執行的順序是(10+1)x100=1100,所以sleep不釋放鎖,wait釋放鎖。

三、執行緒狀態

3.1 執行緒狀態概覽

執行緒狀態:

  • NEW 尚未啟動
  • RUNNABLE 正在執行中
  • BLOCKED 阻塞的(被同步鎖或者IO鎖阻塞)
  • WAITING 永久等待狀態
  • TIMED_WAITING 等待指定的時間重新被喚醒的狀態
  • TERMINATED 執行完成

執行緒的狀態可以使用getState()檢視,更多狀態詳情,檢視Thread原始碼,如下圖:

Java併發程式設計(一)Thread詳解

3.2 執行緒的狀態程式碼實現

3.2.1 NEW 尚未啟動狀態

Thread thread = new Thread() {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
};
// 只宣告不呼叫start()方法,得到的狀態是NEW
System.out.println(thread.getState()); // NEW
複製程式碼

3.2.2 RUNNABLE 執行狀態

Thread thread = new Thread() {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
};
thread.start();
System.out.println(thread.getState()); // RUNNABLE
複製程式碼

3.2.3 BLOCKED 阻塞狀態

使用synchronized同步阻塞實現,程式碼如下:

public class MyCounter {
    int counter;
    public synchronized void increase()  {
        counter++;
        try {
            Thread.sleep(10*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
複製程式碼
MyCounter myCounter = new MyCounter();
// 執行緒1呼叫同步執行緒,模擬阻塞
new Thread(()-> myCounter.increase()).start();
// 執行緒2繼續呼叫同步阻塞方法
Thread thread = new Thread(()-> myCounter.increase());
thread.start();

// 讓主執行緒等10毫秒
Thread.currentThread().sleep(10);
// 列印執行緒2,為阻塞狀態:BLOCKED
System.out.println(thread.getState());
複製程式碼

3.2.4 WAITING 永久等待狀態

public class MyThread extends Thread{
    @Override
    public void run() {
        synchronized (MyThread.class){
            try {
                MyThread.class.wait();
                System.out.println(Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
複製程式碼
Thread thread = new Thread(new MyThread());
thread.start();
// 主執行緒掛起200毫秒,等thread執行完成
Thread.sleep(200);
// 輸出WAITING,執行緒thread一直處於被掛起狀態
System.out.println(thread.getState());
複製程式碼

喚醒執行緒: 可使用 notify/notifyAll 方法,程式碼如下:

synchronized (MyThread.class) {
    MyThread.class.notify();
}
複製程式碼

使執行緒WAITING的方法:

  • Object的wait() 不設定超時時間
  • Thread.join()不設定超時時間
  • LockSupport的park()

檢視Thread原始碼可以知道Thread的join方法,底層使用的是Object的wait實現的,如下圖:

Java併發程式設計(一)Thread詳解

注意: 檢視Object的原始碼可知wait(),不傳遞引數,等同於wait(0),設定的“0”不是立即執行,而是無限的等待,不執行,如下圖:

Java併發程式設計(一)Thread詳解

3.2.5 TIMED_WAITING 超時等待狀態

TIMED_WAITING狀態,只需要給wait設定上時間即可,程式碼如下:

public class MyThread extends Thread{
    @Override
    public void run() {
        synchronized (MyThread.class){
            try {
                MyThread.class.wait(1000);
                System.out.println(Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
複製程式碼

呼叫程式碼還是一樣的,如下:

Thread thread = new Thread(new MyThread());
thread.start();
// 主執行緒掛起200毫秒,等thread執行完成
Thread.sleep(200);
// 輸出TIMED_WAITING
System.out.println(thread.getState());
synchronized (MyThread.class) {
    MyThread.class.notify();
}
複製程式碼

3.2.6 TERMINATED 完成狀態

Thread thread = new Thread(()-> System.out.println(Thread.currentThread().getName()));
thread.start();
// 讓主執行緒等10毫秒
Thread.currentThread().sleep(10);
System.out.println(thread.getState());
複製程式碼

四、死鎖

根據前面的知識,我們知道使用sleep的時候是不釋放鎖的,所以利用這個特性我們可以很輕易的寫出死鎖的程式碼,具體的流程如圖(圖片來源於楊曉峰老師文章):

Java併發程式設計(一)Thread詳解

程式碼如下:

static  Object object1 = new Object();
static  Object object2 = new Object();

public static void main(String[] args) {

    Thread thread = new Thread(){
        @Override
        public void run() {
            synchronized (object1){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (object2){
                    System.out.println(Thread.currentThread().getName());
                }
            }
        }
    };

    Thread thread2 = new Thread(){
        @Override
        public void run() {
            synchronized (object2){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (object1){
                    System.out.println(Thread.currentThread().getName());
                }
            }
        }
    };

    thread.start();
    thread2.start();
複製程式碼

執行上面的程式碼,程式會處於無限等待之中。

五、總結

根據上面的內容,我們已經系統的學習Thread的使用了,然而學而不思則罔,最後留一個思考題:根據本文介紹的知識,怎麼能避免死鎖?(哈哈,賣個關子,根據文章的知識點組合可以得出答案)

原始碼下載:github.com/vipstone/ja…


參考文件

docs.oracle.com/javase/8/do…

相關文章