Java並發編程-線程基礎
1. 線程的創建
首先我們來複習我們學習 java
時接觸的線程創建,這也是面試的時候喜歡問的,有人說兩種也有人說三種四種等等,其實我們不能去死記硬背,而應該深入理解其中的原理,當我們理解後就會發現所謂的創建線程實質都是一樣的,在我們面試的過程中如果我們能從本質出發回答這樣的問題,那麼相信一定是個加分項!好了我們不多說了,開始今天的 code
之路
1.1 **繼承 Thread 類創建線程 **
**
- 這是我們最常見的創建線程的方式,通過繼承
Thread
類來重寫run
方法,
代碼如下:
/**
* 線程類
* url: www.i-code.online
* @author: anonyStar
* @time: 2020/9/24 18:55
*/
public class ThreadDemo extends Thread {
@Override
public void run() {
//線程執行內容
while (true){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ThredDemo 線程正在執行,線程名:"+ Thread.currentThread().getName());
}
}
}
測試方法:
@Test
public void thread01(){
Thread thread = new ThreadDemo();
thread.setName("線程-1 ");
thread.start();
while (true){
System.out.println("這是main主線程:" + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
結果:
繼承
Thread
的線程創建簡單,啟動時直接調用start
方法,而不是直接調用run
方法。直接調用run
等於調用普通方法,並不是啟動線程
1.2 **實現 Runnable 接口創建線程 **
**
- 上述方式我們是通過繼承來實現的,那麼在
java
中提供了Runnable
接口,我們可以直接實現該接口,實現其中的run
方法,這種方式可擴展性更高
代碼如下:
/**
* url: www.i-code.online
* @author: anonyStar
* @time: 2020/9/24 18:55
*/
public class RunnableDemo implements Runnable {
@Override
public void run() {
//線程執行內容
while (true){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("RunnableDemo 線程正在執行,線程名:"+ Thread.currentThread().getName());
}
}
}
測試代碼:
@Test
public void runnableTest(){
// 本質還是 Thread ,這裡直接 new Thread 類,傳入 Runnable 實現類
Thread thread = new Thread(new RunnableDemo(),"runnable子線程 - 1");
//啟動線程
thread.start();
while (true){
System.out.println("這是main主線程:" + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
運行結果:
1.3 實現 Callable 接口創建線程
- 這種方式是通過 實現
Callable
接口,實現其中的call
方法來實現線程,但是這種線程創建的方式是依賴於 ****FutureTask **
包裝器**來創建Thread
, 具體來看代碼
代碼如下:
/**
* url: www.i-code.online
* @author: anonyStar
* @time: 2020/9/24 18:55
*/
public class CallableDemo implements Callable<String> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
@Override
public String call() throws Exception {
//線程執行內容
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("CallableDemo 線程正在執行,線程名:"+ Thread.currentThread().getName());
return "CallableDemo 執行結束。。。。";
}
}
測試代碼:
@Test
public void callable() throws ExecutionException, InterruptedException {
//創建線程池
ExecutorService service = Executors.newFixedThreadPool(1);
//傳入Callable實現同時啟動線程
Future submit = service.submit(new CallableDemo());
//獲取線程內容的返回值,便於後續邏輯
System.out.println(submit.get());
//關閉線程池
service.shutdown();
//主線程
System.out.println("這是main主線程:" + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
結果:
有的時候,我們可能需要讓一步執行的線程在執行完成以後,提供一個返回值給到當前的主線程,主線程需要依賴這個值進行後續的邏輯處理,那麼這個時候,就需要用到帶返回值的線程了
關於線程基礎知識的如果有什麼問題的可以在網上查找資料學習學習!這裡不再闡述
2. 線程的生命周期
- Java 線程既然能夠創建,那麼也勢必會被銷毀,所以線程是存在生命周期的,那麼我們接下來從線程的生命周期開始去了解線程。
2.1 線程的狀態
2.1.1 線程六狀態認識
線程一共有 6 種狀態(NEW、RUNNABLE、BLOCKED、WAITING、TIME_WAITING、TERMINATED)
-
NEW:初始狀態,線程被構建,但是還沒有調用 start 方法
-
RUNNABLED:運行狀態,JAVA 線程把操作系統中的就緒和運行兩種狀態統一稱為「運行中」
-
BLOCKED:阻塞狀態,表示線程進入等待狀態, 也就是線程因為某種原因放棄了 CPU 使用權,阻塞也分為幾種情況
- 等待阻塞:運行的線程執行 wait 方法,jvm 會把當前線程放入到等待隊列➢ 同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被其他線程鎖佔用了,那麼 jvm 會把當前的線程放入到鎖池中
- 其他阻塞:運行的線程執行 Thread.sleep 或者 t.join 方法,或者發出了 I/O 請求時,JVM 會把當前線程設置為阻塞狀態,當 sleep 結束、join 線程終止、io 處理完畢則線程恢復
-
TIME_WAITING:超時等待狀態,超時以後自動返回
-
TERMINATED:終止狀態,表示當前線程執行完畢
2.1.2 代碼實操演示
- 代碼:
public static void main(String[] args) {
////TIME_WAITING 通過 sleep wait(time) 來進入等待超時中
new Thread(() -> {
while (true){
//線程執行內容
try {
TimeUnit.SECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"Time_Waiting").start();
//WAITING, 線程在 ThreadStatus 類鎖上通過 wait 進行等待
new Thread(() -> {
while (true){
synchronized (ThreadStatus.class){
try {
ThreadStatus.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"Thread_Waiting").start();
//synchronized 獲得鎖,則另一個進入阻塞狀態 blocked
new Thread(() -> {
while (true){
synchronized(Object.class){
try {
TimeUnit.SECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"Object_blocked_1").start();
new Thread(() -> {
while (true){
synchronized(Object.class){
try {
TimeUnit.SECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"Object_blocked_2").start();
}
啟動一個線程前,最好為這個線程設置線程名稱,因為這樣在使用 jstack 分析程序或者進行問題排查時,就會給開發人員提供一些提示
2.1.3 線程的狀態堆棧
➢ 運行該示例,打開終端或者命令提示符,鍵入「 jps
」, ( JDK1.5
提供的一個顯示當前所有 java
進程 pid
的命令)
➢ 根據上一步驟獲得的 pid
,繼續輸入 jstack pid
(jstack是 java
虛擬機自帶的一種堆棧跟蹤工具。jstack 用於打印出給定的 java
進程 ID
或 core file
或遠程調試服務的 Java
堆棧信息)
3. 線程的深入解析
3.1 線程的啟動原理
- 前面我們通過一些案例演示了線程的啟動,也就是調用
start()
方法去啟動一個線程,當run
方法中的代碼執行完畢以後,線程的生命周期也將終止。調用start
方法的語義是當前線程告訴JVM
,啟動調用start
方法的線程。 - 我們開始學習線程時很大的疑惑就是 啟動一個線程是使用
start
方法,而不是直接調用run
方法,這裡我們首先簡單看一下start
方法的定義,在Thread
類中
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
//線程調用的核心方法,這是一個本地方法,native
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 */
}
}
}
//線程調用的 native 方法
private native void start0();
- 這裡我們能看到
start
方法中調用了native
方法start0
來啟動線程,這個方法是在Thread
類中的靜態代碼塊中註冊的 , 這裡直接調用了一個native
方法registerNatives
/* Make sure registerNatives is the first thing <clinit> does. */
private static native void registerNatives();
static {
registerNatives();
}
-
由於
registerNatives
方法是本地方法,我們要看其實現源碼則必須去下載jdk
源碼,關於jdk
及虛擬機hotspot
的源碼下載可以去openJDK
官網下載 ,參考: -
我們可以本地查看源碼或者直接去 //hg.openjdk.java.net/jdk8u/jdk8u60/jdk/file/935758609767/src/share/native/java/lang/Thread.c 查看
Thread
類對應的本地方法.c
文件,
- 如上圖,我們本地下載
jdk
工程,找到src->share->native->java->lang->Thread.c
文件
- 上面是
Thread.c
中所有代碼,我們可以看到調用了RegisterNatives
同時可以看到method
集合中的映射,在調用本地方法start0
時,實際調用了JVM_StartThread
,它自身是由c/c++
實現的,這裡需要在 虛擬機源碼中去查看,我們使用的都是hostpot
虛擬機,這個可以去openJDK
官網下載,上述介紹了不再多說 - 我們看到
JVM_StartThread
的定義是在jvm.h
源碼中,而jvm.h
的實現則在虛擬機hotspot
中,我們打開hotspot
源碼,找到src -> share -> vm -> prims ->jvm.cpp
文件,在2955
行,可以直接檢索JVM_StartThread
, 方法代碼如下:
JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
JVMWrapper("JVM_StartThread");
JavaThread *native_thread = NULL;
bool throw_illegal_thread_state = false;
{
MutexLocker mu(Threads_lock);
if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) {
throw_illegal_thread_state = true;
} else {
// We could also check the stillborn flag to see if this thread was already stopped, but
// for historical reasons we let the thread detect that itself when it starts running
// <1> :獲取當前進程中線程的數量
jlong size =
java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));
size_t sz = size > 0 ? (size_t) size : 0;
// <2> :真正調用創建線程的方法
native_thread = new JavaThread(&thread_entry, sz);
if (native_thread->osthread() != NULL) {
// Note: the current thread is not being used within "prepare".
native_thread->prepare(jthread);
}
}
}
if (throw_illegal_thread_state) {
THROW(vmSymbols::java_lang_IllegalThreadStateException());
}
assert(native_thread != NULL, "Starting null thread?");
if (native_thread->osthread() == NULL) {
// No one should hold a reference to the 'native_thread'.
delete native_thread;
if (JvmtiExport::should_post_resource_exhausted()) {
JvmtiExport::post_resource_exhausted(
JVMTI_RESOURCE_EXHAUSTED_OOM_ERROR | JVMTI_RESOURCE_EXHAUSTED_THREADS,
"unable to create new native thread");
}
THROW_MSG(vmSymbols::java_lang_OutOfMemoryError(),
"unable to create new native thread");
}
// <3> 啟動線程
Thread::start(native_thread);
JVM_END
JVM_ENTRY
是用來定義JVM_StartThread
函數的,在這個函數裏面創建了一個真正和平台有關的本地線程, 上述標記 <2> 處
- 為了進一步線程創建,我們在進入
new JavaThread(&thread_entry, sz)
中查看一下具體實現過程,在thread.cpp
文件1566
行處定義了new
的方法
- 對於上述代碼我們可以看到最終調用了
os::create_thread(this, thr_type, stack_sz);
來實現線程的創建,對於這個方法不同平台有不同的實現,這裡不再贅述,
- 上面都是創建過程,之後再調用
Thread::start(native_thread);
在 JVM_StartThread 中調用,該方法的實現在Thread.cpp
中
start
方法中有一個函數調用:os::start_thread(thread);
,調用平台啟動線程的方法,最終會調用Thread.cpp
文件中的JavaThread::run()
方法
3.2 線程的終止
3.2.1 通過標記位來終止線程
- 正常我們線程內的東西都是循環執行的,那麼我們實際需求中肯定也存在想在其他線程來停止當前線程的需要,這是後我們可以通過標記位來實現,所謂的標記為其實就是
volatile
修飾的變量,着由它的可見性特性決定的,如下代碼就是依據volatile
來實現標記位停止線程
//定義標記為 使用 volatile 修飾
private static volatile boolean mark = false;
@Test
public void markTest(){
new Thread(() -> {
//判斷標記位來確定是否繼續進行
while (!mark){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("線程執行內容中...");
}
}).start();
System.out.println("這是主線程走起...");
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//10秒後將標記為設置 true 對線程可見。用volatile 修飾
mark = true;
System.out.println("標記位修改為:"+mark);
}
3.2.2 通過 stop 來終止線程
- 我們通過查看
Thread
類或者JDK API
可以看到關於線程的停止提供了stop()
,supend()
,resume()
等方法,但是我們可以看到這些方法都被標記了@Deprecated
也就是過時的, - 雖然這幾個方法都可以用來停止一個正在運行的線程,但是這些方法都是不安全的,都已經被拋棄使用,所以在我們開發中我們要避免使用這些方法,關於這些方法為什麼被拋棄以及導致的問題
JDK
文檔中較為詳細的描述 《Why Are Thread.stop, Thread.suspend, Thread.resume and Runtime.runFinalizersOnExit Deprecated?》 - 在其中有這樣的描述:
-
總的來說就是:
- 調用
stop()
方法會立刻停止run()
方法中剩餘的全部工作,包括在catch
或finally
等語句中的內容,並拋出ThreadDeath
異常(通常情況下此異常不需要顯示的捕獲),因此可能會導致一些工作的得不到完成,如文件,數據庫等的關閉。 - 調用
stop()
方法會立即釋放該線程所持有的所有的鎖,導致數據得不到同步,出現數據不一致的問題。
- 調用
3.2.3 通過 interrupt 來終止線程
- 通過上面闡述,我們知道了使用
stop
方法是不推薦的,那麼我們用什麼來更好的停止線程,這裡就引出了interrupt
方法,我們通過調用interrupt
來中斷線程 - 當其他線程通過調用當前線程的
interrupt
方法,表示向當前線程打個招呼,告訴他可以中斷線程的執行了,至於什麼時候中斷,取決於當前線程自己 - 線程通過檢查自身是否被中斷來進行相應,可以通過
isInterrupted()
來判斷是否被中斷。
我們來看下面代碼:
public static void main(String[] args) {
//創建 interrupt-1 線程
Thread thread = new Thread(() -> {
while (true) {
//判斷當前線程是否中斷,
if (Thread.currentThread().isInterrupted()) {
System.out.println("線程1 接收到中斷信息,中斷線程...");
break;
}
System.out.println(Thread.currentThread().getName() + "線程正在執行...");
}
}, "interrupt-1");
//啟動線程 1
thread.start();
//創建 interrupt-2 線程
new Thread(() -> {
int i = 0;
while (i <20){
System.out.println(Thread.currentThread().getName()+"線程正在執行...");
if (i == 8){
System.out.println("設置線程中斷....");
//通知線程1 設置中斷通知
thread.interrupt();
}
i ++;
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"interrupt-2").start();
}
打印結果如下:
上述代碼中我們可以看到,我們創建了
interrupt-1
線程,其中用interrupt
來判斷當前線程是否處於中斷狀態,如果處於中斷狀態那麼就自然結束線程,這裡的結束的具體操作由我們開發者來決定。再創建interrupt-2
線程,代碼相對簡單不闡述,當執行到某時刻時將線程interrupt-1
設置為中斷狀態,也就是通知interrupt-1
線程。
線程中斷標記複位 :
在上述
interrupt-1
代碼中如果加入sleep
方法,那麼我們會發現程序報出InterruptedException
錯誤,同時,線程interrupt-1
也不會停止,這裡就是因為中斷標記被複位了 ,下面我們來介紹一下關於中斷標記複位相關的內容
- 在線程類中提供了** **
Thread.interrupted
的靜態方法,用來對線程中斷標識的複位,在上面的代碼中,我們可以做一個小改動,對interrupt-1
線程創建的代碼修改如下:
//創建 interrupt-1 線程
Thread thread = new Thread(() -> {
while (true) {
//判斷當前線程是否中斷,
if (Thread.currentThread().isInterrupted()) {
System.out.println("線程1 接收到中斷信息,中斷線程...中斷標記:" + Thread.currentThread().isInterrupted());
Thread.interrupted(); // //對線程進行複位,由 true 變成 false
System.out.println("經過 Thread.interrupted() 複位後,中斷標記:" + Thread.currentThread().isInterrupted());
//再次判斷是否中斷,如果是則退出線程
if (Thread.currentThread().isInterrupted()) {
break;
}
}
System.out.println(Thread.currentThread().getName() + "線程正在執行...");
}
}, "interrupt-1");
上述代碼中 我們可以看到,判斷當前線程是否處於中斷標記為
true
, 如果有其他程序通知則為true
此時進入if
語句中,對其進行複位操作,之後再次判斷。執行代碼後我們發現interrupt-1
線程不會終止,而會一直執行
Thread.interrupted
進行線程中斷標記複位是一種主動的操作行為,其實還有一種被動的複位場景,那就是上面說的當程序出現InterruptedException
異常時,則會將當前線程的中斷標記狀態複位,在拋出異常前,JVM
會將中斷標記isInterrupted
設置為false
在程序中,線程中斷複位的存在實際就是當前線程對外界中斷通知信號的一種響應,但是具體響應的內容有當前線程決定,線程不會立馬停止,具體是否停止等都是由當前線程自己來決定,也就是開發者。
3.3 線程終止 interrupt 的原理
- 首先我們先來看一下在
Thread
中關於interrupt
的定義:
public void interrupt() {
if (this != Thread.currentThread()) {
checkAccess(); //校驗是否有權限來修改當前線程
// thread may be blocked in an I/O operation
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
// <1> 調用 native 方法
interrupt0(); // set interrupt status
b.interrupt(this);
return;
}
}
}
// set interrupt status
interrupt0();
}
- 上面代碼中我們可以看到,在
interrupt
方法中最終調用了Native
方法interrupt0
,這裡相關在線程啟動時說過,不再贅述,我們直接找到hotspot
中jvm.cpp
文件中JVM_Interrupt
方法
JVM_Interrupt
方法比較簡單,其中我們可以看到直接調用了Thread.cpp
的interrupt
方法,我們進入其中查看
- 我們可以看到這裡直接調用了
os::interrupt(thread)
這裡是調用了平台的方法,對於不同的平台實現是不同的,我們這裡如下所示,選擇Linux
下的實現os_linux.cpp
中,
在上面代碼中我們可以看到,在
1
處拿到OSThread
,之後判斷如果interrupt
為false
則在2
處調用OSThread
的set_interrupted
方法進行設置,我們可以進入看一下其實現,發現在osThread.hpp
中定義了一個成員變量volatile jint _interrupted;
而set_interrupted
方法其實就是將_interrupted
設置為true
,之後再通過ParkEvent
的unpark()
方法來喚醒線程。具體的過程在上面進行的簡單的注釋介紹,
本文由AnonyStar 發佈,可轉載但需聲明原文出處。
仰慕「優雅編碼的藝術」 堅信熟能生巧,努力改變人生
歡迎關注微信公賬號 :雲棲簡碼 獲取更多優質文章
更多文章關注筆者博客 :雲棲簡碼