前言
針對Java多線程,其實一直都些零散的學習,面試中多線程也是經常被問到的一塊,這次還真想認證的總結好這個,實在不想在這塊栽跟頭了。這次根據《Java 高併發編程詳解》一書進行學習和總結。
先來個簡單實例
@Slf4j
public class StepIn {
public static void main(String[] args) {
//啓動一個線程
new Thread(){
@Override
public void run() {
enjoyMusic();
}
}.start();
browseNews();
}
private static void browseNews(){
while(true){
log.info("Uh-hh,the good news.");
sleep(1);
}
}
private static void enjoyMusic(){
while(true){
log.info("Uh-hh,the nice music.");
sleep(1);
}
}
private static void sleep(int seconds){
try {
TimeUnit.SECONDS.sleep(seconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
運行起來之後,利用JConsole連接到指定的JVM進程,查看結果如下
可以很正常的看到會有一個main線程和自己啓動的Thread-0線程,還有其他的一些守護線程(什麼是守護線程後續會詳談)。每一個線程都有自己的局部變量表,程序計數器,以及生命週期。
線程的生命週期
線程執行了Thread的start方法就代表該線程已經開始執行了麼?線程的生命週期大體可以分爲如下5個主要的階段,具體如下圖所示
一個線程的生命週期大體可以分爲5個主要的階段:NEW,RUNNABLE,RUNNING,BLOCKED,TERMINATED。
NEW狀態
當通過new Thread()創建一個Thread對象的時候,並沒有做線程啓動的操作,這個時候線程並不處於執行狀態,因爲沒有調用start啓動線程,線程這個時候只能爲NEW狀態。
RUNNABLE狀態
線程對象進入RUNNABLE狀態必須調用start方法,此時纔是真正地在JVM中創建了一個線程,線程啓動之後並不能立即執行,必須得到CPU選中,如果被CPU選中,則會進入Running狀態。
順便說一下,線程無法從RUNNABLE狀態直接進入TERMINATED狀態。
RUNNING狀態
CPU選中了該線程,該線程才真正進入到了運行狀態,此時它才能真正的執行自己的代碼邏輯,一個在RUNNING狀態的線程,線程可能發送如下轉換
由RUNNING直接進入TERMINATED狀態,比如調用stop方法(雖然不建議使用)
由RUNNING狀態進入BLOCKED狀態,比如調用sleep或者wait方法,或者進行某個阻塞的IO狀態,獲取某個鎖資源,
由RUNNING狀態進入RUNNABLE狀態,比如CPU的調度器輪詢使該線程放棄執行,或由於主動調用了yield方法,放棄CPU的執行權。
BLOCKED狀態
由RUNNING進入到BLOCKED的狀態我們已經介紹過了。BLOCKED狀態切換成其他狀態的可能如下:
1、直接進入TERMINATED狀態,比如調用stop方法
2、直接進入RUNNABLE狀態,線程阻塞的操作結束,比如讀取到了想要的字節,或者完成了指定時間的休眠,或者獲取到了某個鎖資源,或者在阻塞過程中被打斷(比如其他線程調用interrupt方法)。
TERMINATED狀態
這是一個線程的最終狀態,該狀態不會有任何切換。線程運行出錯,或者JVM Crash都會導致所有線程直接進入該狀態
爲什麼要有Runnable接口
談到有幾種方式創建線程,我們的第一反應都是兩種(其實還有其他的)——繼承Thread類,實現Runnable接口。但是,爲啥會有兩種方式,爲啥不直接實現一個Runnable接口就完事了?
其實有兩個原因:其中一個是如果一個類已經繼承了某個類,再想繼承Thread類就很難了,所以有了一個Runnable接口,但是還有另外一個原因,這個原因需要從下面一段程序說起,這個我們後面會討論。
這裏先看看start方法中的源碼
/**
* Causes this thread to begin execution; the Java Virtual Machine
* calls the <code>run</code> method of this thread.
* <p>
* The result is that two threads are running concurrently: the
* current thread (which returns from the call to the
* <code>start</code> method) and the other thread (which executes its
* <code>run</code> method).
* <p>
* It is never legal to start a thread more than once.
* In particular, a thread may not be restarted once it has completed
* execution.
*
* @exception IllegalThreadStateException if the thread was already
* started.
* @see #run()
* @see #stop()
*/
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 {
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 */
}
}
}
private native void start0();
可以看出,start源碼是比較簡單的,核心無非就是調用了一個start0()的本地方法。但是我們繼承Thread的時候,複寫的是run方法,而不是start方法?其實從註釋中可以看到這一點:Causes this thread to begin execution; the Java Virtual Machine calls the run
method of this thread.這句話的翻譯就是在開始執行這個線程時,JVM將會調用該線程的run方法,換言之,run 方法是被JNI方法start0() 調用的(這一點和模板方法模式有點像),仔細閱讀start的源碼將會總結出如下幾點。
1、Thread被new之後,Thread內部的threadStatus爲0,這個時候只表示線程被初始化,並沒有啓動
2、不能兩次啓動線程,否則會出現IllegalThreadStateException。
3、group.add(this);線程啓動之後會被加入一個線程組中。
4、線程已經結束(即線程運行結束)或者啓動,threadStatus不爲0,是不能再次啓動的。
從模板方法說起
來一個簡單的模板方法的實例
有一個抽象類
@Slf4j
public abstract class AbstractTemplate {
//具體的實現交給子類
public abstract String sayHello(String name);
public void printHelloMessage(String name){
log.info("print hello message : {}",sayHello(name));
}
}
兩個實現類
public class TemplateOne extends AbstractTemplate {
public String sayHello(String name) {
return "中文的問候方法:你好"+name;
}
}
public class TemplateTwo extends AbstractTemplate {
public String sayHello(String name) {
return "English say Hello : "+name;
}
}
運行類
public class TemplateTest {
public static void main(String[] args) {
AbstractTemplate template = new TemplateOne();
template.printHelloMessage("liman");
AbstractTemplate templateTwo = new TemplateTwo();
templateTwo.printHelloMessage("liman");
}
}
運行結果:
其實這個比較簡單,無法就是真正的邏輯交給子類去是實現,然後在通過父類調用具體的方法的時候,會自動調用到子類的實現邏輯,這其實充分利用了多態的特點。
繼承Thread與實現Runnable接口
現在回到我們之前的問題,這兩者有何區別,除了方便繼承之外還有沒有其他值得考量的地方?這個我們先從Thread類中的run方法來看看
@Override
public void run() {
//如果構造Thread的時候傳遞了Runnable接口,則會調用Runnable接口對應的run方法
if (target != null) {
target.run();
}
//如果沒有傳遞一個實現了Runnable接口的實例,則需要自己複寫run方法
}
在看看Thread其中的一個構造函數
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
可以看到這裏的Thread的構造方法接受一個Runnable參數。
之前流傳着一種說法——問:創建線程有幾種方式,有些人答:兩種,一種是實現Runnable接口,另一種是繼承Thread類。至少現在看來,這句話還是有些膚淺的。前面的start方法中我們看到,run只是一個模板方法,本地方法start0中回去調用這個對應子類的run方法。其實這兩種方式都是殊途同歸。從實質上看,創建線程只有一種方式就是構造Thread類,創建線程執行單元的方式有兩種,一種是重寫Thread的run方法,另一種是實現Runnable接口的run方法,第二種方法將Runnab實例作爲Thread的構造參數。
繼承Thread
@Slf4j
public class TicketWindow extends Thread {
private final String name;
private static final int MAX = 500;
private static int index = 1; //用static修飾
public TicketWindow(String name){
this.name = name;
}
@Override
public void run() {
while(index<=MAX){
log.info("櫃檯名稱:{},當前編號:{}",name,index++);
}
}
public static void main(String[] args) {
TicketWindow ticketWindow0 = new TicketWindow("一號叫號機");
TicketWindow ticketWindow1 = new TicketWindow("二號叫號機");
TicketWindow ticketWindow2 = new TicketWindow("三號叫號機");
TicketWindow ticketWindow3 = new TicketWindow("四號叫號機");
TicketWindow ticketWindow4 = new TicketWindow("五號叫號機");
ticketWindow0.start();
ticketWindow1.start();
ticketWindow2.start();
ticketWindow3.start();
ticketWindow4.start();
}
}
這種情況下會存在差別,如果index用static修飾,纔在一定程度上避免了一個號被多個TicketWindow消費的情況,如果去掉了static修飾,則會出現多個TicketWindow打印同一個index值的情況。
實現runnable接口
@Slf4j
public class TicketWindowRunnable implements Runnable {
private final String name;
private static final int MAX = 500;
private int index = 1;//未用static修飾
public TicketWindowRunnable(String name){
this.name = name;
}
public void run() {
while(index<=MAX){
log.info("櫃檯名稱:{},當前編號:{}",name,index++);
}
}
}
實現Runnable接口之後就會好得多,index不需要static修飾就能在一定程度上做到index共享,這做到了業務數據和業務操作的分離。但是index的值如果過大同樣還是會存在線程安全的問題。