[03][01][01] 併發編程的基本認識

進程的概念

進程的本質是一個正在執行的程序,程序運行時系統會創建一個進程,並且給每個進程分配獨立的內存地址空間保證每個進程地址不會相互干擾。同時,在 CPU 對進程做時間片的切換時,保證進程切換過程中仍然要從進程切換之前運行的位置出開始執行。所以進程通常還會包括程序計數器、堆棧指針,如果內存中可以同時存放足夠多的程序,那 CPU 的利用率可以接近 100%

線程的概念

線程是操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以併發多個線程,每條線程並行執行不同的任務

線程解決的問題

  • 在多核 CPU 中,利用多線程可以實現真正意義上的並行執行
  • 在一個進程中,會存在多個同時執行的任務,如果其中一個任務被阻塞,將會引起不依賴該任務的任務也被阻塞。通過對不同任務創建不同的線程去處理,可以提升程序處理的實時性
  • 線程可以認爲是輕量級的進程,所以線程的創建、銷燬比進程更快

線程的創建

  • 繼承 Thread 類
  • 實現 Runnable 接口
  • Callable/Future帶返回值的線程
  • ThreadPool

線程可以合理的利用多核心 CPU 資源,提高程序的吞吐量

繼承 Thread 類

Thread 類本質上是實現了 Runnable 接口的一個實例,代表一個線程的實例。啓動線程的唯一方法就是通過 Thread 類的 start()實例方法。start()方法是一個 native 方法,它會啓動一個新線程,並執行 run()方法

public class ThreadDemo extends Thread {

    @Override
    public void run() {
        int sum = 0;

        for (int i = 0; i <= 100; i++) {
            sum += i;
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("計算結果爲" + sum);
    }

    public static void main(String[] args) {
        ThreadDemo threadDemo = new ThreadDemo();

        threadDemo.start();
        System.out.println("啓動線程" + threadDemo.getClass().getSimpleName());
    }
}

實現 Runnable 接口

如果自己的類已經 extends 另一個類,就無法直接 extends Thread,此時,可以實現一個 Runnable 接口

public class RunnableDemo implements Runnable{

    @Override
    public void run() {
        int sum = 0;

        for (int i = 0; i <= 100; i++) {
            sum += i;
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("計算結果爲" + sum);
    }

    public static void main(String[] args) {
        RunnableDemo runnableDemo = new RunnableDemo();

        new Thread(runnableDemo).start();
        System.out.println("啓動線程" + runnableDemo.getClass().getSimpleName());
    }
}

Callable/Future帶返回值的線程

有業務場景需要讓一步執行的線程在執行完成以後,提供一個返回值給到當前的主線程,主線程需要依賴這個值進行後續的邏輯處理,那麼這個時候,就需要用到帶返回值的線程了。Java 中提供了這樣的實現方式

public class CallableDemo implements Callable<String> {
    /**
     * 處理業務流程
     */
    @Override
    public String call() throws Exception {
        int a = 2;

        int b = 2;

        System.out.println(a + b);
        return "執行結果:" + (a + b);
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(1);

        CallableDemo callableDemo = new CallableDemo();

        Future<String> future = executorService.submit(callableDemo);

        System.out.println(future.get());
        executorService.shutdown();
    }
}

ThreadPool


多線程的實際應用

在工作中應該很少有場景能夠應用多線程了,因爲基於業務開發來說,很多使用異步的場景我們都通過分佈式消息隊列來做了。但並不是說多線程就不會被用到,你們如果有看一些框架的源碼,會發現線程的使用無處不在

在做文件跑批場景中,每天會有一些比如收益文件、對賬文件,我們會有一個定時任務去拿到數據然後通過線程去處理

zookeeper 源碼的異步責任鏈模式

時序圖

外界請求PrevProcessorSaveProcessorPrintProcessor處理request將請求做預處理將請求交給保存請求類處理保存請求將請求做打印處理打印請求外界請求PrevProcessorSaveProcessorPrintProcessor

代碼實現

  • 請求類
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Request {

    private String name;
}
  • 請求處理接口
/**
 * 責任鏈模式的請求處理接口
 */
public interface IRequestProcessor {

    void process(Request request);
}
  • 請求預處理類
/**
 * 預處理
 */
public class PrevProcessor extends Thread implements IRequestProcessor {
    //阻塞隊列
    LinkedBlockingQueue<Request> requests = new LinkedBlockingQueue<>();

    private IRequestProcessor nextProcessor;

    private volatile boolean isFinish = false;

    public PrevProcessor() {
    }

    public PrevProcessor(IRequestProcessor nextProcessor) {
        this.nextProcessor = nextProcessor;
    }

    public void shutdown() { //對外提供關閉的方法
        isFinish = true;
    }

    @Override
    public void run() {
        //不建議這麼寫
        while (!isFinish) {
            try {
                //阻塞式獲取數據
                Request request = requests.take();
                //真正的處理邏輯
                System.out.println("prevProcessor:" + request);
                //交給下一個責任鏈
                if (nextProcessor != null) {
                    nextProcessor.process(request);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void process(Request request) {
        //TODO 根據實際需求去做一些處理
        requests.add(request);
    }
}
  • 保存處理類
/**
 * 保存處理
 */
public class SaveProcessor extends Thread implements IRequestProcessor {
    //阻塞隊列
    LinkedBlockingQueue<Request> requests = new LinkedBlockingQueue<>();

    private IRequestProcessor nextProcessor;

    private volatile boolean isFinish = false;

    public SaveProcessor() {
    }

    public SaveProcessor(IRequestProcessor nextProcessor) {
        this.nextProcessor = nextProcessor;
    }

    /**
     * 對外提供關閉的方法
     */
    public void shutdown() {
        isFinish = true;
    }

    @Override
    public void run() {
        //不建議這麼寫
        while (!isFinish) {
            try {
                //阻塞式獲取數據
                Request request = requests.take();
                //真正的處理邏輯; store to mysql 。
                System.out.println("SaveProcessor:" + request);
                //交給下一個責任鏈
                if (nextProcessor != null) {
                    nextProcessor.process(request);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void process(Request request) {
        //TODO 根據實際需求去做一些處理
        requests.add(request);
    }
}
  • 打印處理類
/**
 * 打印處理
 */
public class PrintProcessor extends Thread implements IRequestProcessor {
    //阻塞隊列
    LinkedBlockingQueue<Request> requests = new LinkedBlockingQueue<>();

    private IRequestProcessor nextProcessor;

    private volatile boolean isFinish = false;

    public PrintProcessor() {
    }

    public PrintProcessor(IRequestProcessor nextProcessor) {
        this.nextProcessor = nextProcessor;
    }

    /**
     * 對外提供關閉的方法
     */
    public void shutdown() {
        isFinish = true;
    }

    @Override

    public void run() {
        //不建議這麼寫
        while (!isFinish) {
            try {
                //阻塞式獲取數據
                //消費者
                Request request = requests.take();
                //真正的處理邏輯
                System.out.println("PrintProcessor:" + request);
                //交給下一個責任鏈
                if (nextProcessor != null) {
                    nextProcessor.process(request);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void process(Request request) {
        //TODO 根據實際需求去做一些處理
        requests.add(request); //生產者
    }
}
  • 測試類
/**
 * 請求測試類
 */
public class ProcessorDemo {

    static IRequestProcessor requestProcessor;

    public void setUp() {
        PrintProcessor printProcessor = new PrintProcessor();
        printProcessor.start();

        SaveProcessor saveProcessor = new SaveProcessor(printProcessor);
        saveProcessor.start();

        requestProcessor = new PrevProcessor(saveProcessor);
        ((PrevProcessor) requestProcessor).start();
    }


    public static void main(String[] args) throws InterruptedException {
        ProcessorDemo processorDemo = new ProcessorDemo();
        processorDemo.setUp();

        Request request = new Request();
        request.setName("責任鏈請求測試");
        requestProcessor.process(request);

        Thread.sleep(2000L);
        System.out.println("2秒後發起新的請求");

        requestProcessor.process(new Request("請求 11"));
    }
}

輸出結果
prevProcessor:Request(name=責任鏈請求測試)
SaveProcessor:Request(name=責任鏈請求測試)
PrintProcessor:Request(name=責任鏈請求測試)
2秒後發起新的請求
prevProcessor:Request(name=請求 11)
SaveProcessor:Request(name=請求 11)
PrintProcessor:Request(name=請求 11)

線程的生命週期

查看 Thread 類的源碼,存在枚舉內部類 State,定義了線程生命週期的六個狀態

  • NEW
  • RUNNABLE
  • BLOCKED
  • WAITING
  • TIMED_WAITING
  • TERMINATED

NEW:初始狀態,線程被構建,但是還沒有調用 start() 方法
RUNNABLED:運行狀態,JAVA 線程把操作系統中的就緒和運行兩種狀態統一稱爲“運行中”
BLOCKED:阻塞狀態,表示線程進入等待狀態,也就是線程,因爲某種原因放棄了 CPU 使用權,阻塞也分爲幾種情況

  • 等待阻塞:運行的線程執行 wait 方法,jvm 會把當前線程放入到等待隊列
  • 同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被其他線程鎖佔用了,那麼 jvm 會把當前的線程放入到鎖池中
  • 其他阻塞:運行的線程執行 Thread.sleep 或者 t.join 方法,或者發出了 I/O 請求時,JVM 會把當前線程設置爲阻塞狀態,當 sleep 結束、join 線程終止、io 處理完畢則線程恢復

TIME_WAITING:超時等待狀態,超時以後自動返回
TERMINATED:終止狀態,表示當前線程執行完畢

線程的生命週期

調試代碼

通過代碼來重現線程的相關狀態

public class ThreadStatusDemo {

    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Time_Waiting_Thread").start();

        new Thread(() -> {
            while (true) {
                synchronized (ThreadStatusDemo.class) {
                    try {
                        ThreadStatusDemo.class.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "Waiting_Thread").start();

        //BLOCKED
        new Thread(new BlockedDemo(), "Blocked01_Thread").start();
        new Thread(new BlockedDemo(), "Blocked02_Thread").start();
    }


    static class BlockedDemo extends Thread {
        @Override
        public void run() {
            synchronized (BlockedDemo.class) {
                while (true) {
                    try {
                        TimeUnit.SECONDS.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

使用 jdk 自帶命令 jps 和 jstack 查看運行中線程的狀態

Waiting_Thread 線程與 Time_Waiting_Thread 在代碼中分別調用了 wait() 和 sleep(100)方法,它們之間不存在競爭鎖,線程分別狀態爲 WAITING,TIMED_WAITING 狀態

Blocked01_Thread 與 Blocked02_Thread 對 BlockedDemo 存在鎖競爭,執行 sleep(100),導致兩個線程一個是 TIMED_WAITING,另一個是 BLOCKED

代碼調試查看線程狀態

代碼調試查看線程狀態

線程的啓動

在編寫代碼中,都是通過調用start()方法去啓動一個線程,當 run() 方法中的代碼執行完畢以後,線程的生命週期也將終止。調用 start() 方法的語義是當前線程告訴 JVM,啓動調用 start 方法的線程

學習線程的時候會比較疑惑,啓動一個線程爲什麼是調用 start() 方法,而不是 run() 方法,這做一個簡單的分析,先簡單看一下 start() 方法的定義

public class Thread implements Runnable {
    ......
    static {
        registerNatives();
        EMPTY_STACK_TRACE = new StackTraceElement[0];
        SUBCLASS_IMPLEMENTATION_PERMISSION = new RuntimePermission("enableContextClassLoaderOverride");
    }
    ......
    public synchronized void start() {
        if (this.threadStatus != 0) {
            throw new IllegalThreadStateException();
        } else {
            this.group.add(this);
            boolean var1 = false;

            try {
                // 此處是start()方法的核心代碼
                this.start0();
                var1 = true;
            } finally {
                try {
                    if (!var1) {
                        this.group.threadStartFailed(this);
                    }
                } catch (Throwable var8) {
                }
            }
        }
    }
    ......
    private native void start0();
    ......
}

JDK8對應的 jdk 和 hotspot 源碼地址:https://github.com/huaweirookie/toalibaba/tree/master/concurrent-programming/thread-basic/src/main/resources/jdk8

調用 start() 方法實際上是調用一個 native 方法 start0() 來啓動一個線程,首先 start0() 這個方法是在Thread 的靜態塊中來註冊的,registerNatives 的 本 地 方 法 的 定 義 在 文 件 Thread.c,Thread.c 定義了各個操作系統平臺要用的關於線程的公共數據和操作,以下是 Thread.c 的全部內容

文件地址信息:jdk8/jdk/src/share/native/java/lang/Thread.c

static JNINativeMethod methods[] = {
    {"start0",           "()V",        (void *)&JVM_StartThread},
    {"stop0",            "(" OBJ ")V", (void *)&JVM_StopThread},
    {"isAlive",          "()Z",        (void *)&JVM_IsThreadAlive},
    {"suspend0",         "()V",        (void *)&JVM_SuspendThread},
    {"resume0",          "()V",        (void *)&JVM_ResumeThread},
    {"setPriority0",     "(I)V",       (void *)&JVM_SetThreadPriority},
    {"yield",            "()V",        (void *)&JVM_Yield},
    {"sleep",            "(J)V",       (void *)&JVM_Sleep},
    {"currentThread",    "()" THD,     (void *)&JVM_CurrentThread},
    {"countStackFrames", "()I",        (void *)&JVM_CountStackFrames},
    {"interrupt0",       "()V",        (void *)&JVM_Interrupt},
    {"isInterrupted",    "(Z)Z",       (void *)&JVM_IsInterrupted},
    {"holdsLock",        "(" OBJ ")Z", (void *)&JVM_HoldsLock},
    {"getThreads",        "()[" THD,   (void *)&JVM_GetAllThreads},
    {"dumpThreads",      "([" THD ")[[" STE, (void *)&JVM_DumpThreads},
    {"setNativeName",    "(" STR ")V", (void *)&JVM_SetNativeThreadName},
};

#undef THD
#undef OBJ
#undef STE
#undef STR

JNIEXPORT void JNICALL
Java_java_lang_Thread_registerNatives(JNIEnv *env, jclass cls)
{
    (*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
}

從這段代碼可以看出, start0() 實際會執行 JVM_StartThread 方法,從名字上來看,似乎是在 JVM 層面去啓動一個線程,在 JVM 層面一定會調用 Java 中定義的 run() 方法。那接下來繼續去找找答案。我們找到 jvm.cpp 這個文件,這個文件需要下載 hotspot 的源碼才能找到

源碼地址:jdk8/hotspot/src/share/vm/prims/jvm.cpp

JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
  JVMWrapper("JVM_StartThread");
  ......
  native_thread = new JavaThread(&thread_entry, sz);
  ......
}

JVM_ENTRY 是用來定義 JVM_StartThread 函數的,在這個函數裏面創建了一個真正和平臺有關的本地線程,繼續尋找 JavaThread 的定義在 hotspot 的源碼中 thread.cpp 文件中 1558 行的位置可以找到如下代碼

JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) :
  Thread()
#if INCLUDE_ALL_GCS
  , _satb_mark_queue(&_satb_mark_queue_set),
  _dirty_card_queue(&_dirty_card_queue_set)
#endif // INCLUDE_ALL_GCS
{
  if (TraceThreadEvents) {
    tty->print_cr("creating thread %p", this);
  }
  initialize();
  _jni_attach_state = _not_attaching_via_jni;
  set_entry_point(entry_point);
  // Create the native thread itself.
  // %note runtime_23
  os::ThreadType thr_type = os::java_thread;
  thr_type = entry_point == &compiler_thread_entry ? os::compiler_thread :
                                                     os::java_thread;
  os::create_thread(this, thr_type, stack_sz);
  _safepoint_visible = false;
}

這個方法有兩個參數:
第一個是函數名稱,線程創建成功之後會根據這個函數名稱調用對應的函數
第二個是當前進程內已經有的線程數量
最後我們重點關注與一下 os::create_thread,實際就是調用平臺創建線程的方法來創建線程

接下來就是線程的啓動,會調用 Thread.cpp 文件中的 Thread::start(Thread* thread) 方法,代碼如下

void Thread::start(Thread* thread) {
  trace("start", thread);
  // Start is different from resume in that its safety is guaranteed by context or
  // being called from a Java method synchronized on the Thread object.
  if (!DisableStartThread) {
    if (thread->is_Java_thread()) {
      // Initialize the thread state to RUNNABLE before starting this thread.
      // Can not set it after the thread started because we do not know the
      // exact thread state at that time. It could be in MONITOR_WAIT or
      // in SLEEPING or some other state.
      java_lang_Thread::set_thread_status(((JavaThread*)thread)->threadObj(),
                                          java_lang_Thread::RUNNABLE);
    }
    os::start_thread(thread);
  }
}

start() 方法中有一個函數調用:os::start_thread(thread),調用平臺啓動線程的方法,最終會調用 Thread.cpp 文件中的 JavaThread::run() 方法

線程的終止

線程的啓動過程大家都非常熟悉,但是如何終止一個線程呢?

線程的終止,並不是簡單的調用 stop 命令去。雖然 api 仍然可以調用,但是和其他的線程控制方法如 suspend、resume 一樣都是過期了的不建議使用,就拿 stop 來說,stop 方法在結束一個線程時並不會保證線程的資源正常釋放,因此會導致程序可能出現一些不確定的狀態。要優雅的去中斷一個線程,在線程中提供了一個 interrupt 方法

interrupt 方法

當其他線程通過調用當前線程的 interrupt 方法,表示向當前線程打個招呼,告訴他可以中斷線程的執行了,至於什麼時候中斷,取決於當前線程自己。線程通過檢查自身是否被中斷來進行相應,可以通過 isInterrupted()來判斷是否被中斷。通過下面這個例子,來實現了線程終止的邏輯

public class InterruptDemo {

    private static int i;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            //默認是false  _interrupted state
            while (!Thread.currentThread().isInterrupted()) {
                i++;
            }
            System.out.println("i:" + i);
        });
        thread.start();

        TimeUnit.SECONDS.sleep(1);
        //把isInterrupted設置成true
        thread.interrupt();
    }
}

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

Thread.interrupted

上面的案例中通過 interrupt,設置了一個標識告訴線程可以終止了,線程中還提供了靜態方法 Thread.interrupted() 對設置中斷標識的線程復位。比如在上面的案例中,外面的線程調用 thread.interrupt() 來設置中斷標識,而在線程裏面又通過 Thread.interrupted 把線程的標識又進行了復位

public class InterruptDemo {

    private static int i;
    
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (true) {
                if (Thread.currentThread().isInterrupted()){
                    System.out.println("before:" + Thread.currentThread().isInterrupted());

                    //對線程進行復位,由 true 變成 false
                    Thread.interrupted();

                    System.out.println("after:" + Thread.currentThread().isInterrupted());
                }
            }
        }, "interruptDemo");
        
        thread.start();
        TimeUnit.SECONDS.sleep(1);
        thread.interrupt();
    }
}

其他的線程復位

除了通過 Thread.interrupted 方法對線程中斷標識進行復位以外,還有一種被動復位的場景,就是對拋出 InterruptedException 異常的方法,在 InterruptedException 拋出之前,JVM 會先把線程的中斷標識位清除,然後纔會拋出 InterruptedException,這個時候如果調用 isInterrupted 方法,將會返回 false 分別通過下面兩個 demo 來演示覆位的效果

爲什麼要復位

Thread.interrupted() 是屬於當前線程的,是當前線程對外界中斷信號的一個響應,表示自己已經得到了中斷信號,但不會立刻中斷自己,具體什麼時候中斷由自己決定,讓外界知道在自身中斷前,他的中斷狀態仍然是 false,這就是復位的原因

線程的終止原理

我們來看一下 thread.interrupt() 方法做了什麼事情

public class Thread implements Runnable {
......
    public void interrupt() {
        if (this != Thread.currentThread())
            checkAccess();

        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();
                b.interrupt(this);
                return;
            }
        }
        interrupt0();
    }
}
......

這個方法裏面,調用了 interrupt0(),這個方法在前面分析 start() 方法的時候見過,是一個 native 方法,這裏就不再重複貼代碼了,同樣我們找到 jvm.cpp 文件,找到 JVM_Interrupt 的定義

JVM_ENTRY(void, JVM_Interrupt(JNIEnv* env, jobject jthread))
  JVMWrapper("JVM_Interrupt");

  // Ensure that the C++ Thread and OSThread structures aren't freed before we operate
  oop java_thread = JNIHandles::resolve_non_null(jthread);
  MutexLockerEx ml(thread->threadObj() == java_thread ? NULL : Threads_lock);
  // We need to re-resolve the java_thread, since a GC might have happened during the
  // acquire of the lock
  JavaThread* thr = java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread));
  if (thr != NULL) {
    Thread::interrupt(thr);
  }
JVM_END

這個方法比較簡單,直接調用了 Thread::interrupt(thr) 這個方法,這個方法的定義在 Thread.cpp 文件中,代碼如下

void Thread::interrupt(Thread* thread) {
  trace("interrupt", thread);
  debug_only(check_for_dangling_thread_pointer(thread);)
  os::interrupt(thread);
}

Thread::interrupt 方法調用了 os::interrupt 方法,這個是調用平臺的 interrupt 方法,這個方法的實現是在 os_*.cpp 文件中,其中星號代表的是不同平臺,因爲 jvm 是跨平臺的,所以對於不同的操作平臺,線程的調度方式都是不一樣的。我們以 os_linux.cpp 文件爲例

void os::interrupt(Thread* thread) {
  assert(Thread::current() == thread || Threads_lock->owned_by_self(),
    "possibility of dangling Thread pointer");
  //獲取本地線程池對象
  OSThread* osthread = thread->osthread();
  //判斷本地線程對象是否爲中斷
  if (!osthread->interrupted()) {
    //設置中斷狀態爲 true
    osthread->set_interrupted(true);
    // More than one thread can get here with the same value of osthread,
    // resulting in multiple notifications.  We do, however, want the store
    // to interrupted() to be visible to other threads before we execute unpark().
    //這裏是內存屏障
    OrderAccess::fence();
    //_SleepEvent相當於 Thread.sleep,表示如果線程調用了 sleep 方法,則通過 unpark 喚醒
    ParkEvent * const slp = thread->_SleepEvent ;
    if (slp != NULL) slp->unpark() ;
  }

  // For JSR166. Unpark even if interrupt status already was set
  if (thread->is_Java_thread())
    ((JavaThread*)thread)->parker()->unpark();
  //_ParkEvent用於 synchronized 同步塊和 object.wait(),這裏相當於也是通過 unpark 進行喚醒
  ParkEvent * ev = thread->_ParkEvent ;
  if (ev != NULL) ev->unpark() ;
}

set_interrupted(true) 實際上就是調用 osThread.hpp 中的 set_interrupted()方法,在 osThread 中定義了一個成員屬性 volatile jint _interrupted;

通過上面的代碼分析可以知道,thread.interrupt() 方法實際就是設置一個 interrupted 狀態標識爲 true、並且通過 ParkEvent 的 unpark 方法來喚醒線程

  • 對於 synchronized 阻塞的線程,被喚醒以後會繼續嘗試獲取鎖,如果失敗仍然可能被 park
  • 在調用 ParkEvent 的 park 方法之前,會先判斷線程的中斷狀態,如果爲 true,會清除當前線程的中斷標識
  • Object.wait 、 Thread.sleep 、 Thread.join 會拋出 InterruptedException

InterruptedException

爲什麼 Object.wait、Thread.sleep 和 Thread.join 都會拋出 InterruptedException?

這幾個方法有一個共同點,都是屬於阻塞的方法

而阻塞方法的釋放會取決於一些外部的事件,但是阻塞方法可能因爲等不到外部的觸發事件而導致無法終止,所以
它允許一個線程請求自己來停止它正在做的事情。當一個方法拋出 InterruptedException 時,它是在告訴調用者如果執行該方法的線程被中斷,它會嘗試停止正在做的事情並且通過拋出 InterruptedException 表示提前返回

所以,這個異常的意思是表示一個阻塞被其他線程中斷了。然後,由於線程調用了 interrupt() 中斷方法,那麼 Object.wait、Thread.sleep 等被阻塞的線程被喚醒以後會通過 is_interrupted 方法判斷中斷標識的狀態變化,如果發現中斷標識爲 true,則先清除中斷標識,然後拋出 InterruptedException

需要注意的是,InterruptedException 異常的拋出並不意味着線程必須終止,而是提醒當前線程有中斷的操作發生,至於接下來怎麼處理取決於線程本身,比如

  • 直接捕獲異常不做任何處理
  • 將異常往外拋出
  • 停止當前線程,並打印異常信息

爲了讓大家能夠更好的理解上面這段話,我們以 Thread.sleep 爲例直接從 jdk 的源碼中找到中斷標識的清除以及異常拋出的方法代碼找到 is_interrupted() 方法,linux 平臺中的實現在 os_linux.cpp 文件中,代碼如下

bool os::is_interrupted(Thread* thread, bool clear_interrupted) {
  assert(Thread::current() == thread || Threads_lock->owned_by_self(),
    "possibility of dangling Thread pointer");

  OSThread* osthread = thread->osthread();
  //獲取線程的中斷標識
  bool interrupted = osthread->interrupted();
  //如果中斷標識爲 true
  if (interrupted && clear_interrupted) {
    //設置中斷標識爲 false
    osthread->set_interrupted(false);
    // consider thread->_SleepEvent->reset() ... optional optimization
  }

  return interrupted;
}

找到 Thread.sleep 這個操作在 jdk 中的源碼體現,怎麼找?相信如果前面大家有認真看的話,應該能很快找到,代碼在 jvm.cpp 文件中

JVM_ENTRY(void, JVM_Sleep(JNIEnv* env, jclass threadClass, jlong millis))
  JVMWrapper("JVM_Sleep");

  if (millis < 0) {
    THROW_MSG(vmSymbols::java_lang_IllegalArgumentException(), "timeout value is negative");
  }
  //判斷並清除線程中斷狀態,如果中斷狀態爲 true,拋出中斷異常
  if (Thread::is_interrupted (THREAD, true) && !HAS_PENDING_EXCEPTION) {
    THROW_MSG(vmSymbols::java_lang_InterruptedException(), "sleep interrupted");
  }

  // Save current thread state and restore it at the end of this block.
  // And set new thread state to SLEEPING.
  JavaThreadSleepState jtss(thread);

注意上面加了中文註釋的地方的代碼,先判斷 is_interrupted 的狀態,然後拋出一個 InterruptedException 異常。到此爲止,我們就已經分析清楚了中斷的整個流程

資源信息

本文設計的代碼可前往下載:https://github.com/huaweirookie/toalibaba/tree/master/concurrent-programming/thread-basic

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