02.併發程式設計(2)Thread類原始碼分析

wustor發表於2017-12-08

概述

在說執行緒之前先說下程式,程式和執行緒都是一個時間段的描述,是CPU工作時間段的描述。

程式,是併發執行的程式在執行過程中分配和管理資源的基本單位,是一個動態概念,竟爭計算機系統資源的基本單位。每一個程式都有一個自己的地址空間,即程式空間或(虛空間)。

執行緒,在網路或多使用者環境下,一個伺服器通常需要接收大量且不確定數量使用者的併發請求,為每一個請求都建立一個程式顯然是行不通的,——無論是從系統資源開銷方面或是響應使用者請求的效率方面來看。因此,作業系統中執行緒的概念便被引進了。執行緒,是程式的一部分,一個沒有執行緒的程式可以被看作是單執行緒的。執行緒有時又被稱為輕權程式或輕量級程式,也是 CPU 排程的一個基本單位。

建立方式

執行緒的建立有三種方式:繼承Thread,實現Runnable介面,利用Callable跟Future

繼承Thread

(1)定義Thread類的子類,並重寫該類的run方法,該run方法的方法體就代表了執行緒要完成的任務。因此把run()方法稱為執行體。 (2)建立Thread子類的例項,即建立了執行緒物件。 (3)呼叫執行緒物件的start()方法來啟動該執行緒。

    public class FirstMethod extends Thread {
        @Override
        public void run() {
            super.run();
        }
    }
      FirstMethod firstMethod = new FirstMethod();
      firstMethod.start();
複製程式碼

實現Runnable介面

  • (1)定義runnable介面的實現類,並重寫該介面的run()方法,該run()方法的方法體同樣是該執行緒的執行緒執行體。
  • (2)建立 Runnable實現類的例項,並依此例項作為Thread的target來建立Thread物件,該Thread物件才是真正的執行緒物件。
  • (3)呼叫執行緒物件的start()方法來啟動該執行緒。
public class SecondMethod implements Runnable{
        @Override
        public void run() {

        }
    }
SecondMethod secondMethod=new SecondMethod();
new Thread(secondMethod).start();
複製程式碼

通過Callable跟FutureTask建立執行緒

1)建立Callable介面的實現類,並實現call()方法,該call()方法將作為執行緒執行體,並且有返回值。 (2)建立Callable實現類的例項,使用FutureTask類來包裝Callable物件,該FutureTask物件封裝了該Callable物件的call()方法的返回值。 (3)使用FutureTask物件作為Thread物件的target建立並啟動新執行緒。 (4)呼叫FutureTask物件的get()方法來獲得子執行緒執行結束後的返回值

  public class ThirdMethod implements Callable<String> {
        @Override
        public String call() throws Exception {
            return Thread.currentThread().getName();
        }
    }
   
   ThirdMethod thirdMethod=new ThirdMethod();
   FutureTask<String> futureTask=new FutureTask<String>(thirdMethod);
        try {
            String threadName = futureTask.get();

        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } 
複製程式碼

對比分析

實現Runnable和實現Callable介面的方式基本相同,不過是後者執行call()方法有返回值,後者執行緒執行體run()方法無返回值,因此可以把這兩種方式歸為一種這種方式與繼承Thread類的方法之間的差別如下:

1、介面建立執行緒可以實現資源共享,比如多個執行緒可以共享一個Runnable資源 2、但是程式設計稍微複雜,如果需要訪問當前執行緒,必須呼叫Thread.currentThread()方法。 3、介面建立線可以避免由於Java的單繼承特性而帶來的侷限。

現在通過一個程式設計師改Bug的例子來描述一下,一共有15個bug,現在安排3個程式設計師去Debug:

通過Thread來實現

public class BugThread extends Thread {
    private volatile int bugNumber = 5;

    @Override
    public void run() {
        for (int i = 0; i < bugNumber; i++) {
            System.out.println("bugNumber--->" + bugNumber--);
        }
    }
}
  
public class Main {
    public static void main(String[] args) {
        new BugThread().start();
        new BugThread().start();
        new BugThread().start();

    }
}
複製程式碼

執行結果:

Thread-0-----5
Thread-1-----5
Thread-2-----5
Thread-0-----4
Thread-2-----4
Thread-1-----4
Thread-2-----3
Thread-0-----3
Thread-2-----2
Thread-1-----3
Thread-2-----1
Thread-0-----2
Thread-0-----1
Thread-1-----2
Thread-1-----1
複製程式碼

通過Runnable來實現

public class BugRunnable implements Runnable {
    private volatile int bugNumber = 15;

    @Override
    public void run() {
        while (bugNumber > 0)
 System.out.println(Thread.currentThread().getName() + "-----" + bugNumber--);
    }
}
  
    public static void main(String[] args) {
        BugRunnable bugRunnable = new BugRunnable();
        new Thread(bugRunnable).start();
        new Thread(bugRunnable).start();
        new Thread(bugRunnable).start();

    }
複製程式碼

執行結果

Thread-0-----15
Thread-0-----14
Thread-0-----13
Thread-0-----12
Thread-1-----11
Thread-0-----10
Thread-1-----9
Thread-0-----8
Thread-1-----7
Thread-0-----6
Thread-1-----5
Thread-0-----4
Thread-1-----3
Thread-0-----2
Thread-1-----1
複製程式碼

原始碼分析

成員變數

	private volatile char  name[];//執行緒名稱的位元組陣列
	private int    priority;//執行緒優先順序
    private boolean single_step;  //執行緒是否單步
    private boolean daemon = false;  //是否是守護執行緒
    private boolean stillborn = false; //JVM state
    private Runnable target;  //從構造方法傳過來的Runnable
    private ThreadGroup group; //執行緒組
    private ClassLoader contextClassLoader;  //類載入器
    private static int threadInitNumber;  //執行緒編號
    private volatile int threadStatus = 0; //初始狀態
    public final static int MIN_PRIORITY = 1; //最低優先順序
    public final static int NORM_PRIORITY = 5; //預設優先順序
    public final static int MAX_PRIORITY = 10; //最高優先順序
  
複製程式碼

執行緒狀態

 public enum State {
       //Thread state for a thread which has not yet started.
        NEW,
	   //Thread state for a runnable thread. 
        RUNNABLE,
       //Thread state for a thread blocked waiting for a monitor lock.
        BLOCKED,
      // Thread state for a waiting thread.
        WAITING,
      //Thread state for a waiting thread with a specified waiting time.
        TIMED_WAITING,
      //Thread state for a terminated thread
        TERMINATED;
    }
複製程式碼

執行緒的狀態有NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED,可以整理成如下表格

執行緒狀態 解釋
New 還未呼叫 start() 方法
RUNNABLE 呼叫了 start() ,此時執行緒已經準備好被執行,處於就緒佇列
BLOCKED 執行緒阻塞於鎖或者呼叫了 sleep
WAITING 執行緒由於某種原因等待其他執行緒
TIME_WAITING 與 WAITING 的區別是可以在特定時間後自動返回
TERMINATED 執行完畢或者被其他執行緒殺死

構造方法

thread_constructor
Thread有很多構造方法,但是通過觀察最終呼叫瞭如下方法:

  /**
     * Initializes a Thread.
     *
     * @param g //執行緒組
     * @param target //構造方法傳過來的Runnable
     * @param name //執行緒名稱
     * @param stackSize //給執行緒分配的棧的深度
     * @param acc //上下文載入器
     */
   private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }
        this.name = name.toCharArray();
        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();
        this.target = target;//初始化target
        setPriority(priority);//設定優先順序
        if (parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
 ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;//設定棧深度
        /* Set thread ID */
        tid = nextThreadID();//設定執行緒ID
    }
複製程式碼

start方法

    public synchronized void start() {
   
        if (threadStatus != 0)//判斷執行緒是否準備好
        group.add(this);//將啟動的執行緒執行緒組
        boolean started = false;
        try {
            start0();//本地方法,JVM呼叫target的run方法
            started = true;//更改啟動標誌
        } finally {
            try {
                if (!started)
                    group.threadStartFailed(this);//通知執行緒組啟動失敗
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

複製程式碼
@Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
複製程式碼

synchronized 關鍵字說明start方法是同步的,並且是啟動這個執行緒進行執行,JVM將會呼叫這個執行緒的run方法,這樣產生的結果是,兩個執行緒執行著,其中一個是呼叫start()方法的執行緒執行,另一個執行緒是執行run方法的執行緒。

sleep()方法

 public static void sleep(long millis, int nanos)
    throws InterruptedException {
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
            millis++;
        }

        sleep(millis);//呼叫本地方法
    }
複製程式碼

執行緒休眠一段時間,讓其他執行緒有機會繼續執行,需要捕捉異常。

yield()方法

  public static native void yield();
複製程式碼
  • yield是一個靜態的原生(native)方法
  • yield告訴當前正在執行的執行緒把執行機會交給執行緒池中擁有相同優先順序的執行緒。
  • yield不能保證使得當前正在執行的執行緒迅速轉換到可執行的狀態 它僅能使一個執行緒從執行狀態轉到可執行狀態,而不是等待或阻塞狀態

join()方法

  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;
            }
        }
    }
複製程式碼

join方法是等待該執行緒執行,直到超時或者終止,可以作為執行緒通訊的一種方式,A執行緒呼叫B執行緒的join(阻塞),等待B完成後再往下執行。

yieldjoin

  • join方法用執行緒物件呼叫,如果在一個執行緒A中呼叫另一個執行緒B的join方法,執行緒A將會等待執行緒B執行完畢後再執行。
  • yield可以直接用Thread類呼叫,yield讓出CPU執行權給同等級的執行緒,如果沒有相同級別的執行緒在等待CPU的執行權,則該執行緒繼續執行。

interrupt()方法

    public void interrupt() {
        if (this != Thread.currentThread())
            checkAccess();//檢查許可權
synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();        
                b.interrupt(this);
                return;
            }
        }
        interrupt0();
    }
複製程式碼

interrupt()方法是中斷當前的執行緒, 此外還有isInterrupt,以及interrupted方法

  • interrupt():將執行緒置為中斷狀態
  • isInterrupt():執行緒是否中斷
  • interrupted():返回執行緒的上次的中斷狀態,並清除中斷狀態。 一般來說,阻塞函式:如sleep()、join()、wait()等在檢查到執行緒的中斷狀態的時候,會丟擲InteruptedExeption, 同時會清除執行緒的中斷狀態。

執行緒間通訊

前面說過,Java中的執行緒在底層是通過共享記憶體進行通訊的,在應用層則是通過呼叫Object物件的wait()方法和notify()方法或notifyAll()方法來實現執行緒間的通訊。 Object是所有類的超類,主要通過:notify()、notifyAll()、wait()、wait(long)和wait(long,int)這幾個方法來進行執行緒間通訊。

1、wait()

public final void wait()  throws InterruptedException,IllegalMonitorStateException
複製程式碼
  • 休眠當前執行緒,釋放鎖,直到接到通知或被中斷為止
  • 在呼叫wait()之前,執行緒必須要獲得該物件的物件級別鎖

2、notify()

public final native void notify() throws IllegalMonitorStateException
複製程式碼
  • 通知那些呼叫了wait()方法的執行緒。
  • 每次只能通知單個執行緒,單個執行緒等待,則通知當前執行緒,如果有多個,則隨機抽取一個來進行通知
  • 必須等到當前執行緒釋放鎖後,wait所在的執行緒也才可以獲取該物件鎖,但不驚動其他同樣在等待被該物件notify的執行緒們。
  • wait()等待的是被notify或notifyAll,而不是鎖。

3、notifyAll()

public final native void notifyAll() throws IllegalMonitorStateException
複製程式碼
  • 使所有原來在該物件上wait的執行緒統統退出wait的狀態
  • 所有被通知的執行緒都會去競爭物件鎖。
  • 獲得鎖的執行緒,會繼續往下執行,釋放鎖後,wait中的執行緒繼續競爭物件鎖

wait()和sleep()的區別

  • sleep()方法是執行緒類Thread的靜態方法,導致此執行緒暫停執行指定時間,將執行機會給其他執行緒,但是監控狀態依然保持,到時後會自動恢復(執行緒回到就緒(ready)狀態),因為呼叫sleep 不會釋放物件鎖。
  • wait()是Object 類的方法,對此物件呼叫wait()方法導致本執行緒放棄物件鎖(執行緒暫停執行),進入等待此物件的等待鎖定池,只有針對此物件發出notify 方法(或notifyAll)後本執行緒才進入物件鎖定池準備獲得物件鎖進入就緒狀態。

總結

通過對執行緒原始碼的簡單分析,可以看出執行緒也是有自己的生命週期的,但是由於原始碼中有很多native方法,導致了很難追蹤原始碼,所以只能大致理解一下執行緒的各種狀態以及通訊過程,下面可以通過一副流程圖來總結一下:

Thread_life

參考資料

Java程式設計思想

wangchangchung.github.io

www.jianshu.com/p/5b9fdae43…

相關文章