

相信你經過集合篇的成長,已經對JDK原始碼的學習輕車熟路了,接下來你將一起和我進入后半篇的學習,讓我們開始吧!
在接下來10分鐘,你將學習到thread 的原始碼原理、執行緒的狀態變化、執行緒的常用場景,
Thread基礎回顧
Thread基礎回顧
什么是Thread?
Thread顧名思義,是執行緒,你應該知道,一個java程式在作業系統上運行,會啟動一個JVM行程,具有一個行程ID,這個行程可以創建很多個執行緒,作業系統、程式、行程、執行緒關系如下圖所示:

運行一個執行緒其實就是啟動一個執行分支,執行不同的事情,執行分支時可以是阻塞的也可以是異步的,舉一個例子,假如你需要燒開水,還想玩手機,異步就是你燒開水的時候,可以玩手機,阻塞就是你就一直等著水燒開了,再開始玩手機,
創建執行緒Thread
創建執行緒的方式一般是2種,一種是繼承Thread重寫run方法,一種是實作Runnable或Callable介面之后,創建Thread,
當然通過執行緒池也能說算是一種方式,但是底層還是上面的兩種方式之一,沒什么區別,
這里帶你回顧下,代碼如下:
代碼清單: LinkedListDemo創建LinkedList
public static void main(String[] args) {
//創建執行緒1
new Thread(()-> System.out.println(Thread.currentThread().getName()),"demo1").start();
//創建執行緒2
new Thread(new MyThread(),"demo2").start();
//創建執行緒3
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
.setNameFormat("demo-pool-%d")
.build();
ExecutorService singleThreadPool = new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(1024),
namedThreadFactory,
new ThreadPoolExecutor.AbortPolicy());
singleThreadPool.execute(()-> System.out.println(Thread.currentThread().getName()));
singleThreadPool.shutdown();
}
static class MyThread extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
執行緒Thread的常用方法
相信這些方法你一定很熟悉了,這里就不過多贅述了,我想讓你掌握的重點是剖析執行緒如何在開源專案中的使用,從而更好的理解原理,更好的運用執行緒,這個在成長記后面會給大家講解的,
-
start() 啟動一個執行緒
-
join() 加入一個執行緒
-
sleep() 執行緒休眠一陣子
-
isAlive() 執行緒是否存活
-
interupted() 標記執行緒為中斷
-
sinterupted() 執行緒是否中斷
另外還有以下這些方法不經常用,也不建議使用
destroy() 、stop()、 suspend()、 resume()、 yeild()
在開源專案和實際業務系統或者基礎架構系統也是很少使用到的,
執行緒的狀態
執行緒中其實最重要的就是它的狀態流轉,這里直接給大家看一個圖,這個圖非常重要,對分析后面的原始碼有很大幫助,大家一定要牢記于心,執行緒狀態圖如下所示:

執行緒應用場景舉例
執行緒應用場景舉例
當你回顧了執行緒的基本知識,這來舉幾個例子,
在各個開源框架中,執行緒的應用場景非常廣泛,這里給大家舉幾個例子,主要是為了,讓你明白下熟練掌握執行緒的重要性,之后的相關的成長記你會學到具體是怎么使用執行緒的細節的,
執行緒應用舉例1 心跳和監控時使用執行緒
在微服務系統中,經常使用到的一個服務就是注冊中心,注冊中心簡單的講就是讓一個服務訪問另一個服務的時候,可以知道對方所有實體的地址,可以用來在呼叫的時候選擇,這就需要每個服務的實體要注冊自己的資訊到一個公共的地方,這個地方就是注冊中心,
每個實體有一個客戶端用來和服務端通信,在Spring Cloud實作的微服務技術堆疊中,Eureka這個組件就充當了注冊中心的作用,它的服務端Eureka-Server大量使用了一些Thread,這里我舉兩個場景,讓大家感受下Thread的應用,
一個是場景,每個服務的實體需要發送心跳,告訴注冊中心自己還在線,服務還存活著,服務端通過一個執行緒來判斷,某個服務如果在一定時間內不發送心跳了,就認為它故障了,就會把它注冊中心剔除掉,如圖中藍色部分的圓圈所示,

另一個是場景,Eureka server將每個服務實體注冊的資訊放到了一個記憶體map中,為了提高速度和并發能力,它設定了多個快取,有寫快取和讀快取,Eureka client客戶端讀取的是讀快取,什么時候將寫快取資料刷入到讀快取?就是通過一個后臺執行緒,每隔30秒從寫快取重繪到讀快取中,
客戶端也有一個后臺執行緒,每隔30秒會發起http請求,從服務端讀取注冊中心的資料,

總之,在這個場景中,執行緒用于了心跳和監控,是非常常見的場景,因為在很多開源專案中都是這樣做的,比如hdfs,zookeeper等等,
執行緒應用舉例2 定時更新、保存、洗掉資料
在MySQL、Redis、ES、HDFS、Zookeeper等等的開源專案中,都會有執行緒的概念,來定時保存資料,或者重繪磁盤,清理磁盤等等操作,
它們一般其實都很類似,都是保存到磁盤的一個就是操作日志,一個就是快照資料,
比如hdfs中的checkpoint執行緒,用來自動合并edit_log為fsImage、自動清除edit_log執行緒、比如mysql定時刷入bufferPool的資料到磁盤中的執行緒,比如Zookeeper保存WAL日志、或者mysql定時保存dump檔案,redis的快照等等,
執行緒應用舉例3 多執行緒提高處理速度和性能
比較典型的就是Storm、Flink、Spark分布式計算就是使用多執行緒計算提高性能,當然還有MQ的多執行緒消費,也是典型的多執行緒提高了處理性能,
這里不就不一一舉例了,有興趣的同學可以在評論區里寫下自己知道的開源專案中如何使用執行緒,
Thread原始碼剖析
Thread原始碼剖析
回顧了執行緒的基本知識使用場景,接下來你需要了解下Thread的原始碼,才能更好的理解它,使用它,
下面主要通過創建執行緒,啟動執行緒,執行緒狀態變化操作這幾個場景簡單摸一下Thread的原始碼,
第一個場景:創建執行緒
newThread(
()-> System.out.println(Thread.currentThread().getName()),"demo1"
).start();
讓我們分析一下這行代碼,首先是new Thread,會進入建構式,
public Thread(Runnabletarget) {
init(null, target,"Thread-"+ nextThreadNum(), 0);
}
呼叫了init方法前,呼叫了nextThreadNum方法,生成了一個自增id,和Thread-+num 作為執行緒名字,
/* For autonumbering anonymous threads. */
private static intthreadInitNumber;
private static synchronized int nextThreadNum() {
return threadInitNumber++;
}
上面原始碼總結為下圖:

默認生成執行緒名字之后,呼叫4個引數的init方法,接著又繼續呼叫了6個引數的init方法,
private void init(ThreadGroupg, Runnable target,String name,
long stackSize) {
init(g, target, name, stackSize,null, true);
}
這個init是創建執行緒核心的步驟我們來看下它的脈絡,
private void init(ThreadGroupg, Runnable target,String name,
long stackSize, AccessControlContextacc,
booleaninheritThreadLocals) {
if(name ==null) {
throw newNullPointerException("name cannot be null");
}
this.name = name;
Thread parent= currentThread();
SecurityManager security= System.getSecurityManager();
if(g ==null) {
if(security !=null) {
g = security.getThreadGroup();
}
if(g ==null) {
g = parent.getThreadGroup();
}
}
g.checkAccess();
if(security !=null) {
if(isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
g.addUnstarted();
this.group = g;
this.daemon =parent.isDaemon();
this.priority =parent.getPriority();
if(security ==null || isCCLOverridden(parent.getClass()))
this.contextClassLoader= parent.getContextClassLoader();
else
this.contextClassLoader=parent.contextClassLoader;
this.inheritedAccessControlContext=
acc !=null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
if(inheritThreadLocals && parent.inheritableThreadLocals!= null)
this.inheritableThreadLocals=
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
tid= nextThreadID();
}
上面的代碼從脈絡可以分為4點:
- 設定執行緒名稱,默認是Thread+全域自增id
- 設定執行緒組 父執行緒默認是當前執行緒,默認執行緒所屬組為父執行緒組
- 優先級和是否是后臺執行緒 默認和父執行緒相同
- 保存傳入的Runnable或Callable的run方法
如下圖所示:

這個執行緒創建的程序,你需要記住的核心點就是以下幾點:
創建你的執行緒,就是你的父執行緒
如果你沒有指定ThreadGroup,你的ThreadGroup就是父執行緒的ThreadGroup
你的daemon狀態默認是父執行緒的daemon狀態
你的優先級默認是父執行緒的優先級
如果你沒有指定執行緒的名稱,那么默認就是Thread-0格式的名稱
你的執行緒id是全域遞增的,從1開始
第二個場景:啟動執行緒
當創建了執行緒后,接著就是啟動執行緒了,你一定知道,thread啟動一個執行緒使用通過start方法,它的底層邏輯很簡單,代碼如下:
public synchronized void start() {
if(threadStatus !=0)
throw newIllegalThreadStateException();
group.add(this);
boolean started =false;
try{
start0();
started =true;
} finally{
try{
if(!started) {
group.threadStartFailed(this);
}
} catch(Throwable ignore) {
}
}
}
還是來看下核心脈絡:
1、判斷執行緒狀態,不能重復啟動
2、放入執行緒組
3、呼叫一個native方法啟動執行緒,如果失敗從執行緒組中移除,
這個navive方法是C++實作的,具體我們不去研究了,你只要知道本質是將執行緒交給CPU執行緒執行調度器執行,之后CPU會通過時間片的演算法來執行各個執行緒,就可以了,如下圖所示:

啟動完成后,肯定會呼叫run方法,這里的run方法就是之前建構式保存的Runnable動的run方法,如果是繼承Thread方式創建的執行緒,這個run方法被重寫了,所以下面這段邏輯只是適用于Runnalbe或Callable方式創建的Thread,
@Override
public void run() {
if(target !=null) {
target.run();
}
}
第三個場景:執行緒狀態變化操作
了解了創建執行緒和不知道你還記得之前的執行緒狀態圖么?怎么進入到其他狀態呢?讓我們一起來看看狀態變化的一些執行緒操作,
一個執行緒創建時候是NEW啟動后變為Runnable,之后怎么變為WAITING呢?其實有很多種方法,我們這里講一個常用的方法join(),當然呼叫Obejct.wait()或LockSupport.part()操作也會實作同樣的效果,這個我們的成長記后面會講到,大家不要著急,
顧名思義,join表示加入的意思,意思就是另一執行緒加入到執行程序中,當前執行緒需要等待加入的執行緒執行完成才能繼續執行,如下圖所示:

接著我們看下如何進入TimeWaiting狀態呢?其實也很簡單,你可以通過使用sleep方法來進入這個狀態,sleep方法支持傳入等待時間,一般有兩種方式一個是直接傳入毫秒值比如 60 * 1000表示60秒,一個是可以通過TimeUnit這個工具傳遞時間,但是大多數開源專案為了靈活,使用的都是第一種方式,因為第二種方式限制了單位,如果需要修改的話,使用起來就不太靈活了,使用sleep的流程如下:

如果你看sleep的原始碼,發現就是一個native方法,底層肯定是通過C++代碼,通知CPU執行緒休眠一陣子,無論在Java執行緒模型還是CPU的執行緒模型,其實執行緒狀態的變化都是一樣的,其實執行緒的狀態的模型是一個抽象概念,
最后執行緒如何進入Block狀態?其實是通過syncronized加鎖后,被阻塞的執行緒就會進入block狀態,后面講到syncronized的時候詳細分析,這里就不過多說明了,
好了到這里,我們通過執行緒的核心的三個場景分析了thread的原始碼原理,相信你對thread有了進一步的認識了,但是更重要的是,你要不斷的積累使用thread的場景,在合適的使用thread的方法,理解執行緒的狀態變化就顯得更為重要,
本文由博客群發一文多發等運營工具平臺 OpenWrite 發布
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/325230.html
標籤:Java
