Categories
程式開發

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

}

結果:

Java並發編程-線程基礎 1

繼承 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();
}
}
}

運行結果:

Java並發編程-線程基礎 2

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 {
/**
* 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();
}
}

結果:

Java並發編程-線程基礎 3

有的時候,我們可能需要讓一步執行的線程在執行完成以後,提供一個返回值給到當前的主線程,主線程需要依賴這個值進行後續的邏輯處理,那麼這個時候,就需要用到帶返回值的線程了

關於線程基礎知識的如果有什麼問題的可以在網上查找資料學習學習!這裡不再闡述

2. 線程的生命週期

Java 線程既然能夠創建,那麼也勢必會被銷毀,所以線程是存在生命週期的,那麼我們接下來從線程的生命週期開始去了解線程。

2.1 線程的狀態

2.1.1 線程六狀態認識

線程一共有6 種狀態(NEW、RUNNABLE、BLOCKED、WAITING、TIME_WAITING、TERMINATED)

NEW:初始狀態,線程被構建,但是還沒有調用start 方法RUNNABLED:運行狀態,JAVA 線程把操作系統中的就緒和運行兩種狀態統一稱為“運行中”BLOCKED:阻塞狀態,表示線程進入等待狀態, 也就是線程因為某種原因放棄了CPU 使用權,阻塞也分為幾種情況TIME_WAITING:超時等待狀態,超時以後自動返回TERMINATED:終止狀態,表示當前線程執行完畢

Java並發編程-線程基礎 4

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 堆棧信息)

Java並發編程-線程基礎 5

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 does. */
private static native void registerNatives();
static {
registerNatives();
}

由於 registerNatives 方法是本地方法,我們要看其實現源碼則必須去下載 jdk 源碼,關於 jdk 及虛擬機 hotspot 的源碼下載可以去 openJDK 官網下載,參考:我們可以本地查看源碼或者直接去 http://hg.openjdk.java.net/jdk8u/jdk8u60/jdk/file/935758609767/src/share/native/java/lang/Thread.c” 查看 Thread 類對應的本地方法 .c 文件,

Java並發編程-線程基礎 6

如上圖,我們本地下載 jdk 工程,找到 src->share->native->java->lang->Thread.c 文件

Java並發編程-線程基礎 7

上面是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
// :获取当前进程中线程的数量
jlong size =
java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));

size_t sz = size > 0 ? (size_t) size : 0;

// :真正调用创建线程的方法
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");
}

// 启动线程
Thread::start(native_thread);

JVM_END

JVM_ENTRY 是用來定義 JVM_StartThread 函數的,在這個函數里面創建了一個真正和平台有關的本地線程, 上述標記處

為了進一步線程創建,我們在進入 new JavaThread(&thread_entry, sz) 中查看一下具體實現過程,在 thread.cpp 文件 1566 行處定義了 new 的方法

Java並發編程-線程基礎 8

對於上述代碼我們可以看到最終調用了 os::create_thread(this, thr_type, stack_sz); 來實現線程的創建,對於這個方法不同平台有不同的實現,這裡不再贅述,

Java並發編程-線程基礎 9

上面都是創建過程,之後再調用 Thread::start(native_thread); 在JVM_StartThread 中調用,該方法的實現在 Thread.cpp 中

Java並發編程-線程基礎 10

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 文檔中較為詳細的描述 《為什麼不贊成使用Thread.stop,Thread.suspend,Thread.resume和Runtime.runFinalizersOnExit?》“在其中有這樣的描述:

Java並發編程-線程基礎 11

總的來說就是:

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

Java並發編程-線程基礎 12

上述代碼中我們可以看到,我們創建了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) {
// 调用 native 方法
interrupt0(); // set interrupt status
b.interrupt(this);
return;
}
}
}

// set interrupt status
interrupt0();
}

上面代碼中我們可以看到,在 interrupt 方法中最終調用了 Native 方法 interrupt0 ,這里相關在線程啟動時說過,不再贅述,我們直接找到 hotspot 中 jvm.cpp 文件中 JVM_Interrupt 方法

Java並發編程-線程基礎 13

JVM_Interrupt 方法比較簡單,其中我們可以看到直接調用了 Thread.cpp 的 interrupt 方法,我們進入其中查看

Java並發編程-線程基礎 14

我們可以看到這裡直接調用了 os::interrupt(thread) 這裡是調用了平台的方法,對於不同的平台實現是不同的,我們這裡如下所示,選擇 Linux 下的實現 os_linux.cpp 中,

Java並發編程-線程基礎 15

在上面代碼中我們可以看到,在1 處拿到OSThread ,之後判斷如果interrupt 為false 則在2 處調用OSThread 的set_interrupted 方法進行設置,我們可以進入看一下其實現,發現在osThread.hpp 中定義了一個成員變量volatile jint _interrupted; 而set_interrupted 方法其實就是將_interrupted 設置為true ,之後再通過ParkEvent 的unpark() 方法來喚醒線程。具體的過程在上面進行的簡單的註釋介紹,