讓面試官心服口服:Thread.sleep、synchronized、LockSupport.park的線程阻塞有何區別?


前言

在日常編碼的過程中,我們經常會使用Thread.sleep、LockSupport.park()主動阻塞線程,或者使用synchronized和Object.wait來阻塞線程保證並發安全。此時我們會發現,對於Thread.sleep和Object.wait方法是會拋出InterruptedException,而LockSupport.park()和synchronized則不會。而當我們調用Thread.interrupt方法時,除了synchronized,其他線程阻塞的方式都會被喚醒。

於是本文就來探究一下Thread.sleep、LockSupport.park()、synchronized和Object.wait的線程阻塞的原理以及InterruptedException的本質

本文主要分為以下幾個部分

1.Thread.sleep的原理

2.LockSupport.park()的原理

3.synchronized線程阻塞的原理

4.ParkEvent和parker對象的原理

5.Thread.interrupt的原理

6.對於synchronized打斷原理的擴展

1.Thread.sleep的原理

Thread.java

首先還是從java入手,查看sleep方法,可以發現它直接就是一個native方法:

public static native void sleep(long millis) throws InterruptedException;

為了查看native方法的具體邏輯,我們就需要下載openjdk和hotspot的源碼了,下載地址:http://hg.openjdk.java.net/jdk8

查看Thread.c:jdk源碼目錄src/java.base/share/native/libjava

可以看到對應的jvm方法是JVM_Sleep:

static JNINativeMethod methods[] = {
    ...
    {"sleep",            "(J)V",       (void *)&JVM_Sleep},
    ...
};

查看jvm.cpp,hotspot目錄src/share/vm/prims

找到JVM_Sleep方法,我們關注其重點邏輯:

方法的邏輯中,首先會做2個校驗,分別是睡眠時間和線程的打斷標記。其實這2個數據的校驗都是可以放到java層,不過jvm的設計者將其放到了jvm的邏輯中去判斷。

如果睡眠的時間為0,那么會調用系統級別的睡眠方法os::sleep(),睡眠時間為最小時間間隔。在睡眠之前會保存線程當前的狀態,並將其設置為SLEEPING。在睡眠結束之后恢復線程狀態。

接着就是sleep方法的重點,如果睡眠時間不為0,同樣需要保存和恢復線程的狀態,並調用系統級別的睡眠方法os::sleep()。當然睡眠的時間會變成指定的毫秒數。

最重要的區別是,此時會判斷os::sleep()的返回值,如果是打斷狀態,那么就會拋出一個InterruptException!這里其實就是InterruptException產生的源頭

JVM_ENTRY(void, JVM_Sleep(JNIEnv* env, jclass threadClass, jlong millis))
  JVMWrapper("JVM_Sleep");
	//如果睡眠的時間小於0,則拋出異常。這里數據的校驗在jvm層邏輯中校驗
  if (millis < 0) {
    THROW_MSG(vmSymbols::java_lang_IllegalArgumentException(), "timeout value is negative");
  }
  //如果線程已經被打斷了,那么也拋出異常
  if (Thread::is_interrupted (THREAD, true) && !HAS_PENDING_EXCEPTION) {
    THROW_MSG(vmSymbols::java_lang_InterruptedException(), "sleep interrupted");
  }
  ...
  //這里允許睡眠時間為0
  if (millis == 0) {
    ...{
      //獲取並保存線程的舊狀態
      ThreadState old_state = thread->osthread()->get_state();
      //將線程的狀態設置為SLEEPING
      thread->osthread()->set_state(SLEEPING);
      //調用系統級別的sleep方法,此時只會睡眠最小時間間隔
      os::sleep(thread, MinSleepInterval, false);
      //恢復線程的狀態
      thread->osthread()->set_state(old_state);
    }
  } else {
    //獲取並保存線程的舊狀態
    ThreadState old_state = thread->osthread()->get_state();
    //將線程的狀態設置為SLEEPING
    thread->osthread()->set_state(SLEEPING);
    //睡眠指定的毫秒數,並判斷返回值
    if (os::sleep(thread, millis, true) == OS_INTRPT) {
        ...
        //拋出InterruptedException異常
        THROW_MSG(vmSymbols::java_lang_InterruptedException(), "sleep interrupted");
    }
    //恢復線程的狀態
    thread->osthread()->set_state(old_state);
  }
JVM_END

查看os_posix.cpp,hotspot目錄src/os/posix/vm

我們接着查看os::sleep()方法:

首先獲取線程的SleepEvent對象,這個是線程睡眠的關鍵

根據是否允許打斷分為2個大分支,其中邏輯大部分是相同的,區別在於允許打斷的分支中會在循環中額外判斷打斷標記,如果打斷標記為true,則返回打斷狀態,並在外層方法中拋出InterruptedException

最終線程睡眠是調用SleepEvent對象的park方法完成的,該對象內部的原理后面統一說

int os::sleep(Thread* thread, jlong millis, bool interruptible) {
  //獲取thread中的_SleepEvent對象
  ParkEvent * const slp = thread->_SleepEvent ;
  ...
  //如果是允許被打斷
  if (interruptible) {
    //記錄下當前時間戳,這是時間比較的基准
    jlong prevtime = javaTimeNanos();

    for (;;) {
      //檢查打斷標記,如果打斷標記為ture,則直接返回
      if (os::is_interrupted(thread, true)) {
        return OS_INTRPT;
      }
      //線程被喚醒后的當前時間戳
      jlong newtime = javaTimeNanos();
      //睡眠毫秒數減去當前已經經過的毫秒數
      millis -= (newtime - prevtime) / NANOSECS_PER_MILLISEC;
      //如果小於0,那么說明已經睡眠了足夠多的時間,直接返回
      if (millis <= 0) {
        return OS_OK;
      }
      //更新基准時間
      prevtime = newtime;
      //調用_SleepEvent對象的park方法,阻塞線程
      slp->park(millis);
    }
  } else {
    //如果不能打斷,除了不再返回OS_INTRPT以外,邏輯是完全相同的
    for (;;) {
      ...
      slp->park(millis);
      ...
    }
    return OS_OK ;
  }
}

所以Thread.sleep的在jvm層面上是調用thread中SleepEvent對象的park()方法實現阻塞線程,在此過程中會通過判斷時間戳來決定線程的睡眠時間是否達到了指定的毫秒。

InterruptedException的本質是一個jvm級別對打斷標記的判斷,並且jvm也提供了不可打斷的sleep邏輯。

2.LockSupport.park()的原理

除了我們經常使用的Thread.sleep,在jdk中還有很多時候需要阻塞線程時使用的是LockSupport.park()方法(例如ReentrantLock),接下去我們同樣需要看下LockSupport.park()的底層實現

LockSupport.java

從java代碼入手,查看LockSupport.park()方法,可以看到它直接調用了Usafe類中的park方法:

public static void park() {
    UNSAFE.park(false, 0L);
}

Unsafe.java

查看Unsafe.park,可以看到是一個native方法

public native void park(boolean var1, long var2);

查看unsafe.cpp,hotspot目錄src/share/vm/prims

找到park方法,這個方法就比sleep簡單粗暴多了,直接調用thread中的parker對象的park()方法阻塞線程

UNSAFE_ENTRY(void, Unsafe_Park(JNIEnv *env, jobject unsafe, jboolean isAbsolute, jlong time)) {
  ...
  //簡單粗暴,直接調用thread中的parker對象的park方法阻塞線程
  thread->parker()->park(isAbsolute != 0, time);
  ...
} UNSAFE_END

所以LockSupport.park方法是不會拋出InterruptedException異常的。當一個線程調用LockSupport.park阻塞后,如果被喚醒,那么就直接執行之后的邏輯。而對於打斷的響應則需要使用該方法的用戶在Java級別的代碼上通過調用Thread.interrupted()判斷打斷標記自行處理。

相比而言Thread.sleep則設計更為復雜,除了在jvm級別上對打斷作出響應,更提供了不可被打斷的邏輯,保證調用該方法的線程一定可以阻塞指定的時間,而這個功能是LockSupport.park所做不到的。

3.synchronized線程阻塞的原理

再看一下synchronized在線程阻塞上的原理。synchronized本身其實都可寫幾篇文章來探討,不過本文僅關注於其線程阻塞部分的邏輯。

synchronized的阻塞包括2部分:

1.調用synchronized(obj)時,如果沒有搶到鎖,那么會進入隊列等待,並阻塞線程。

2.獲取到鎖之后,調用obj.wait()方法進行等待,此時也會阻塞線程。

先來看情況一。因為這種情況並非是調用類中的某個方法,而是一個關鍵字,因此我們是無法從某個類文件入手。那么我們就需要直接查看字節碼了。

首先創建一個簡單的java類

public class Synchronized{
    public void test(){
        synchronized(this){
        }
    }
}

編譯成.class文件后,再查看其字節碼

javac Synchronized.java
javap -v Synchronized.class

synchronized關鍵字在字節碼上體現為monitorentermonitorexit指令。

public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         ...
         3: monitorenter
         4: aload_1
         5: monitorexit
         ...

查看bytecodeInterpreter.cpp,hotspot目錄/src/share/vm/interpreter

該文件中的方法都是用來解析各種字節碼命令的。接着我們找到monitorenter方法:

這個方法就是synchronized關鍵字的具體加鎖邏輯,十分復雜,這里只是展示方法的入口在哪里。

CASE(_monitorenter): {
  ...
}

查看objectMonitor.cpp,hotspot目錄/src/share/vm/runtime

最終synchronized的線程阻塞邏輯是由objectMonitor對象負責的,所以我們直接查看該對象的相應方法。找到enter方法:

跳過其中大部分邏輯,我們看到EnterI方法,正是在該方法中阻塞線程的。

void ObjectMonitor::enter(TRAPS) {
  ...
  //阻塞線程
  EnterI(THREAD);
  ...
}

查看EnterI方法

這個方法會在一個死循環中嘗試獲取鎖,如果獲取失敗則調用當前線程的ParkEventpark()方法阻塞線程,否則就退出循環

當然特別注意的是,這個方法是在一個死循環中調用的,因此在java級別來看,synchronized是不可打斷的,線程會一直阻塞直到它獲取到鎖為止。

void ObjectMonitor::EnterI(TRAPS) {
  //獲取當前線程對象
  Thread * const Self = THREAD;
  ...
  for (;;) {
    //嘗試獲取鎖
    if (TryLock(Self) > 0) break;
    ...
    //調用ParkEvent的park()方法阻塞線程
    if (_Responsible == Self || (SyncFlags & 1)) {
      Self->_ParkEvent->park((jlong) recheckInterval);
    } else {
      Self->_ParkEvent->park();
    }
    ...
  }
  ...
}

接着來看情況二:

查看objectMonitor.cpp,hotspot目錄/src/share/vm/runtime

最終Object.wait()的線程阻塞邏輯也是由objectMonitor對象負責的,所以我們直接查看該對象的相應方法。找到wait方法:

可以看到wait()方法中對線程的打斷作出了響應,並且會拋出InterruptedException,這也正是java級別的Object.wait()方法會拋出該異常的原因

線程阻塞和synchronized一樣,是由線程的ParkEvent對象的park()方法完成的

void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) {
  //獲取當前線程對象
  Thread * const Self = THREAD;
  //檢查是否可以打斷
  if (interruptible && Thread::is_interrupted(Self, true) && !HAS_PENDING_EXCEPTION) {
    ...
    //拋出InterruptedException
    THROW(vmSymbols::java_lang_InterruptedException());
  }
  if (interruptible && (Thread::is_interrupted(THREAD, false) || HAS_PENDING_EXCEPTION)) {
    //如果線程被打斷了,那就什么都不做
  } else if (node._notified == 0) {
    //調用ParkEvent的park()方法阻塞線程
    if (millis <= 0) {
      Self->_ParkEvent->park();
    } else {
      ret = Self->_ParkEvent->park(millis);
    }   
  }
}

所以對於synchronizedObject.wait來說,最終都是調用thread中ParkEvent對象的park()方法實現線程阻塞的

而在java層面上synchronized本身是不響應線程打斷的,但是Object.wait()方法卻是會響應打斷的,區別正是在於jvm級別的邏輯處理上有所不同。

4.ParkEvent和parker對象的原理

Thread.sleep、synchronized和Object.wait底層分別是利用線程SleepEventParkEvent對象的park方法實現線程阻塞的。因為這2個對象實際是一個類型的,因此我們就一起來看一下其park方法究竟做了什么

查看thread.cpp,hotspot目錄src/share/vm/runtime

找到SleepEventParkEvent的定義,從后面的注釋就可以發現,ParkEvent就是供synchronized()使用的,而SleepEvent則是供Thread.sleep使用的:

ParkEvent * _ParkEvent;    // for synchronized()
ParkEvent * _SleepEvent;   // for Thread.sleep

查看park.hpp,hotspot目錄src/share/vm/runtime

在頭文件中能找到ParkEvent類的定義,繼承自os::PlatformEvent

class ParkEvent : public os::PlatformEvent {
  ...
}

查看os_linux.hpp,hotspot目錄src/os/linux/vm

以linux系統為例,在頭文件中可以看到PlatformEvent的具體定義:

我們關注的重點首先是2個private的對象,一個pthread_mutex_t,表示操作系統級別的信號量,一個pthread_cond_t,表示操作系統級別的條件變量

其次是定義了3個方法,park()、unpark()、park(jlong millis),控制線程的阻塞和繼續執行

class PlatformEvent : public CHeapObj<mtInternal> {
 private:
  ...
  pthread_mutex_t _mutex[1];
  pthread_cond_t  _cond[1];
  ...
  void park();
  void unpark();
  int  park(jlong millis); // relative timed-wait only
  ...
};

查看os_linux.cpp,hotspot目錄src/os/linux/vm

接着我們就需要去看park方法的具體實現,這里我們主要關注3個系統底層方法的調用

pthread_mutex_lock(_mutex):鎖住信號量

status = pthread_cond_wait(_cond, _mutex):釋放信號量,並在條件變量上等待

status = pthread_mutex_unlock(_mutex):釋放信號量

void os::PlatformEvent::park() { 
    ...
    //鎖住信號量
    int status = pthread_mutex_lock(_mutex);
    while (_Event < 0) {
      //釋放信號量,並在條件變量上等待
      status = pthread_cond_wait(_cond, _mutex);
    }
    //釋放信號量
    status = pthread_mutex_unlock(_mutex);
}

可以看到ParkEvent的park()方法底層最終是調用系統函數pthread_cond_wait完成線程阻塞的操作。

而線程的parker對象的park()方法本質和ParkEvent是完全一致的,最終也是調用系統函數pthread_cond_wait完成線程阻塞的操作,區別只是在於多了一個絕對時間的判斷:

查看os_linux.cpp,hotspot目錄src/os/linux/vm

void Parker::park(bool isAbsolute, jlong time) {
  ...
  if (time == 0) {
    //這里是直接長時間等待
    _cur_index = REL_INDEX; 
    status = pthread_cond_wait(&_cond[_cur_index], _mutex);
  } else {
    //這里會根據時間是否是絕對時間,分別等待在不同的條件上
    _cur_index = isAbsolute ? ABS_INDEX : REL_INDEX;
    status = pthread_cond_timedwait(&_cond[_cur_index], _mutex, &absTime);
  }
  ...
}

5.Thread.interrupt的原理

上面看了3中線程阻塞的原理,那么接着自然是需要看一下線程打斷在jvm層面上到底做了什么。我們跳過代碼搜尋的過程,直接看最后一步的源碼

查看os_posix.cpp,hotspot目錄src/os/posix/vm

找到interrupt方法,這個方法正是打斷的重點,其中一共做了2件事情:

1.將打斷標記置為true

2.分別調用thread中的ParkEvent、SleepEvent和Parker對象的unpark()方法

void os::interrupt(Thread* thread) {
  ...
  //獲得c++線程對應的系統線程
  OSThread* osthread = thread->osthread();
  //如果系統線程的打斷標記是false,意味着還未被打斷
  if (!osthread->interrupted()) {
    //將系統線程的打斷標記設為true
    osthread->set_interrupted(true);
    //這個涉及到內存屏障,本文不展開
    OrderAccess::fence();
    //這里獲取一個_SleepEvent,並調用其unpark()方法
    ParkEvent * const slp = thread->_SleepEvent ;
    if (slp != NULL) slp->unpark() ;
  }

  //這里依據JSR166標准,即使打斷標記為true,依然要調用下面的2個unpark
  if (thread->is_Java_thread())
    //如果是一個java線程,這里獲取一個parker對象,並調用其unpark()方法
    ((JavaThread*)thread)->parker()->unpark();

  ParkEvent * ev = thread->_ParkEvent ;
  //這里獲取一個_ParkEvent,並調用其unpark()方法
  if (ev != NULL) ev->unpark() ;
}

通過對3個park對象park()方法的了解,在unpark中必然是調用系統級別的signal方法:

void os::PlatformEvent::unpark() {
  ...
  if (AnyWaiters != 0) {
    //喚醒條件變量
    status = pthread_cond_signal(_cond);
  }
  ...
}

所以對於Thread.interrupt來說,它最重要的事情其實是調用3個unpark()方法對象喚醒線程,而我們老生常談的修改打斷標記,反倒是沒那么重要。是否響應該標記、是在jvm層上響應還是在java層上響應等等邏輯,都取決於實際需要。

6.對於synchronized的擴展

在synchronized的原理部分,我們看到線程的阻塞是在一個死循環中執行的,因此在java級別上看來是不可打斷的。

如果了解synchronized的原理(不了解也沒關系,一會兒會有實際示例),可以知道當線程沒有搶到鎖時會進入一個隊列並阻塞,而線程的正常喚醒順序會按照入隊列的順序依次進行。

然而,如果我們仔細看jvm的邏輯,可以發現在循環中,每當線程被喚醒后都會去調用TryLock方法嘗試獲取鎖,那么結合我們對Thread.interrupt方法的了解

我們就可以大膽推測,雖然在java級別上synchronized不可打斷,但是如果我們不斷地調用Thread.interrupt方法就能使得線程直接插隊獲取鎖,而不必按照入隊列的順序了!

接下來我們來看示例

1.synchronized的順序性

這里我們先讓一個線程獲取到鎖,之后啟動3個線程等待在鎖上。

@Test
public void synchronizedTest() throws InterruptedException {
    int size = 3;
    Object lock = new Object();
    //讓第一個線程獲取鎖后阻塞1秒鍾
    new Thread(() -> {
        synchronized (lock) {
            System.out.println("Thread Lock");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Lock Over");
        }
    }).start();
    TimeUnit.MILLISECONDS.sleep(10);
    //啟動3個線程,並等待第一個線程釋放鎖,每個線程啟動間隔10毫秒,保證入隊列的順序性
    int count = 1;
    for (int i = 0; i < size; i++) {
        int m = count++;
        TimeUnit.MILLISECONDS.sleep(10);
        new Thread(() -> {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName());
            }
        }, "thread--" + m).start();
    }
    TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
}

最終輸出結果,可以看到synchronized的隊列遵循先入后出的原則

Thread Lock
Lock Over
thread--3
thread--2
thread--1

2.線程打斷對隊列順序的影響

在啟動3個線程入隊列之前,我們先啟動一個單獨的線程。並且在主線程的最后,我們在一個死循環中不斷調用該單獨線程的interrupt方法。

@Test
public void synchronizedTest() throws InterruptedException {
    int size = 3;
    Object lock = new Object();
    new Thread(() -> {
        synchronized (lock) {
            System.out.println("Thread Lock");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Lock Over");
        }
    }).start();
    TimeUnit.MILLISECONDS.sleep(10);
    //啟動一個單獨的線程,用來測試synchronized的打斷
    Thread interruptThread = new Thread(() -> {
        synchronized (lock) {
            System.out.println("interruptThread");
        }
    });
    interruptThread.start();
    TimeUnit.MILLISECONDS.sleep(10);
    int count = 1;
    for (int i = 0; i < size; i++) {
        int m = count++;
        TimeUnit.MILLISECONDS.sleep(10);
        new Thread(() -> {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName());
            }
        }, "thread--" + m).start();
    }
    //在主線程中不斷打斷單獨線程
    for(;;){
        interruptThread.interrupt();
    }
}

輸出如下,按照先入后出的原則,這個單獨的線程應該是最后一個獲取到鎖的。然而在主線程不斷地打斷下,它成功地完成了插隊!而其他沒有被打斷的線程依然按照約定的順序依次喚醒。有興趣的同學可以嘗試去掉最后的打斷,再運行一次。

Thread Lock
Lock Over
interruptThread
thread--3
thread--2
thread--1

最后總結一下Thread.sleep、LockSupport.park和synchronized線程阻塞方式的區別,這里我分幾個層次來總結

1.系統級別:這3種方式沒有區別,最終都是調用系統的pthread_cond_wait方法

2.c++線程級別:Thread.sleep使用的是線程的SleepEvent對象,LockSupport.park使用的是線程的Parker對象,synchronizedObject.wait使用的是線程的ParkEvent對象

3.java級別:Thread.sleep可打斷並拋出異常;LockSupport.park可打斷,且不會拋出異常;synchronized不可打斷;Object.wait可打斷並拋出異常

4.InterruptedException其實僅僅是jvm邏輯上對打斷標記的判斷而已

5.Thread.interrupt的本質在於修改打斷標記,並調用3個unpark()方法喚醒線程

4.更概括來說,無論是哪種線程阻塞的方式,在系統級別和c++線程級別來說都是可打斷的。而jvm通過代碼邏輯使得3種線程阻塞的方式在java級別上面對同一個打斷方法時會有不同的表現形式


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM