Java併發編程系列---線程的構建、啓動和停止

一、構造線程

在運行線程之前首先要構造一個線程對象,線程對象在構造的時候需要提供線程所需要的屬性,如線程所屬的線程組、線程優先級、是否是Daemon線程等信息。下面代碼摘自java.lang.Thread中對線程進行初始化的部分。

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");
        }

        this.name = name;
        //當前線程就是該線程的父線程
        Thread parent = currentThread();
		// 將daemon、priority屬性設置爲父線程的對應屬性
        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        this.target = target;
        setPriority(priority);
        // 將父線程的InheritableThreadLocal複製過來
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* 分配一個線程id */
        tid = nextThreadID();

在上述過程中,一個新構造的線程對象是由其parent線程來進行空間分配的,而child 線程繼承了parent是否爲Daemon、優先級和加載資源的contextClassLoader以及可繼承的 ThreadLocal,同時還會分配一個唯一的ID來標識這個child線程。至此,一個能夠運行的線程對象就初始化好了,在堆內存中等待着運行。

1.1 線程的默認命名

打開Thread類的源碼。

    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
    /* For autonumbering anonymous threads. */
    private static int threadInitNumber;
    private static synchronized int nextThreadNum() {
        return threadInitNumber++;
    }

可以看到初始的線程命名就是Thread-threadInitNumber。就是Thread加一個自增數字,例如:Thread-1

1.2 線程的命名

查看Thread類的構造函數發現。有幾個提供名字的構造函數。

    public Thread(String name) {
        init(null, null, name, 0);
    }
    public Thread(ThreadGroup group, String name) {
        init(group, null, name, 0);
    }
    public Thread(Runnable target, String name) {
        init(null, target, name, 0);
    }
    public Thread(ThreadGroup group, Runnable target, String name) {
        init(group, target, name, 0);
    }
    public Thread(ThreadGroup group, Runnable target, String name,
                  long stackSize) {
        init(group, target, name, stackSize);
    }

1.3 修改線程的名字

不論你使用的是默認的函數命名規則,還是指定了一個特殊的名字,在線程啓動之前還有一個機會可以對其進行修改,一旦線程啓動,名字將不再被修改。

    public final synchronized void setName(String name) {
        checkAccess();
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;
        //線程不是NEW狀態不行
        if (threadStatus != 0) {
            setNativeName(name);
        }
    }

二、啓動線程

線程對象在初始化完成之後,調用start()方法就可以啓動這個線程。
線程start()方法的 含義是:當前線程(即parent線程)同步告知Java虛擬機,只要線程規劃器空閒,應立即 啓動調用start()方法的線程。
注意 啓動一個線程前,最好爲這個線程設置線程名稱,因爲這樣在使用jstack分析程序或者進行問題排查時,就會給開發人員提供一些提示,自定義的線程最好能夠起個名字。

線程的start方法

public synchronized void start() {
       //表示剛新建的狀態爲NEW的線程,threadStatus=0 如果啓動多次就會拋異常
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
       //線程啓動之後會加入到一個線程組
        group.add(this);

        boolean started = false;
        try {
            start0();
            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 */
            }
        }
    }

線程啓動的run方法其實是被start0()啓動的。start0()是一個JNI方法。

三、線程中斷(interrupt)

中斷可以理解爲線程的一個標識位屬性,它表示一個運行中的線程是否被其他線程進 行了中斷操作。中斷好比其他線程對該線程打了個招呼,其他線程通過調用該線程的 interrupt()方法對其進行中斷操作。
線程通過檢查自身是否被中斷來進行響應,線程通過方法isInterrupted()來進行判斷是 否被中斷,也可以調用靜態方法Thread.interrupted()對當前線程的中斷標識位進行復位。 如果該線程已經處於終結狀態,即使該線程被中斷過,在調用該線程對象的isInterrupted() 時依舊會返回false。
從Java的API中可以看到,許多聲明拋出InterruptedException的方法(例如 Thread.sleep(long millis)方法)這些方法在拋出InterruptedException之前,Java虛擬機會先 將該線程的中斷標識位清除,然後拋出InterruptedException,此時調用isInterrupted()方法 將會返回false。

四、安全地終止線程

在上面提到的中斷狀態是線程的一個標識位,而中斷操作是一種簡便的線程間交互方式,而這種交互方式最適合用來取消或停止任務。除了中斷以外,還可以利用一個 boolean變量來控制是否需要停止任務並終止該線程。

在下面的代碼中,創建了一個線程CountThread,它不斷地進行變量累加,而主線程嘗試對其進行中斷操作和停止操作。

package com.example.demo.thread;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.concurrent.TimeUnit;

/**
 * @author : pengweiwei
 * @date : 2020/2/1 2:57 下午
 */
public class Shutdown {
    public static void main(String[] args) throws Exception {
        Runner one = new Runner();
        Thread countThread = new Thread(one, "CountThread");
        countThread.start();
// 睡眠1秒,main線程對CountThread進行中斷,使CountThread能夠感知中斷而結束 
        TimeUnit.SECONDS.sleep(1);
        countThread.interrupt();
        Runner two = new Runner();
        countThread = new Thread(two, "CountThread");
        countThread.start();
// 睡眠1秒,main線程對Runner two進行取消,使CountThread能夠感知on爲false而結束 
        TimeUnit.SECONDS.sleep(1);
        two.cancel();
    }

    private static class Runner implements Runnable {
        private long i;
        private volatile boolean on = true;

        @Override
        public void run() {
            while (on && !Thread.currentThread().isInterrupted()) {
                i++;
            }
            System.out.println("Count i = " + i);
        }

        public void cancel() {
            on = false;
        }
    }
}

示例在執行過程中,main線程通過中斷操作和cancel()方法均可使CountThread得以終止。這種通過標識位或者中斷操作的方式能夠使線程在終止時有機會去清理資源,而不是武斷地將線程停止,因此這種終止線程的做法顯得更加安全和優雅。

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