這周趕項目,暫停了一下微博。結果今天看到簡書app的圖標竟然有負罪感!趁著周末,更新一波。。。
從本文開始,我們開始分析一個新的Java的知識點--多線程。要研究這個,首先我們要知道,什么是線程。
線程的定義
進程是指一個內存中運行的應用程序,每個進程都有自己獨立的一塊內存空間,擁有自己的數據和代碼。
線程是指進程中的一個任務,一個進程中可以運行多個線程。線程是屬于某個進程,進程中的多個線程共享此進程的內存。
拿手機舉個例子,我們拿著手機可以一邊聽歌一邊看微信,此時,微信和聽歌軟件就構成了多進程。即同一時刻,有不同的進程在工作。而多線程就是指在同一個程序中,同時執行多個任務,通常,每個任務稱為一個線程。線程跟進程的區別是,每個進程都有自己獨立的數據空間,而同一類的線程共享數據。多進程是為了提高CPU的使用率,而多線程是為了提高應用程序的使用率。
線程的狀態
線程的狀態如下:
線程的狀態分為五種:
新建狀態:當使用某種方式創建一個線程對象后,該線程就是新建狀態
就緒狀態:當已創建的線程的start()方法被調用后,就進入就緒狀態。這種狀態也叫做"可執行狀態"。在這種狀態下,該線程隨時等待被CPU調度執行(注意,這個時候不是立即執行,而是等待CPU的調度)。
可運行狀態:在Java虛擬機中執行的線程處于此狀態。即線程取得CPU的權限,開始執行。線程只能從就緒狀態進入運行狀態。(在任何給定時刻,一個可運行的線程可能正在運行也可能沒有運行)
阻塞狀態:當線程因為某種關系,無法獲得CPU的使用權限時,就處于這種狀態。比如調用的wait()方法(等待)、有同步鎖(阻塞)及調用了sleep()方法(計時等待)等。
死亡狀態:以退出的線程處于此狀態。退出可能是線程執行完畢,或者是發生了異常等。
當一個線程開始運行的時候,它并不是始終運行的。因為Java中多線程是搶占式調度,即多個線程搶占時間片來執行任務。當一個線程的時間片用完后,它就會被系統剝奪其運行權限,并與其他線程共同爭奪下一個時間片的使用權。
線程的創建
創建線程的方式有:繼承Thread類、實現Runnable接口及通過Callable和Future新建一個線程。
繼承Thread類
創建的步驟為:
1、繼承Thread類
2、重寫run方法
3、實例化我們寫的Thread類的子類,并調用start方法啟動線程
具體代碼如下:
//繼承Thread類
public class MyThread extends Thread{
//重寫run方法(線程的執行部分)
@Override
public void run() {
//自己的代碼邏輯
........
}
}
public class Main{
public static void main(String [] args){
//實例化一個MyThread類的子類
MyThread myThread = new MyThread();
//調用start方法啟動線程
myThread.start();
}
}
實現Runnable接口
創建步驟為:
1、定義一個類,實現Runnable接口,重寫run方法;
2、創建一個Runnable的實現類的實例,并以此為參數創建一個Thread類
3、調用Thread類的start方法,啟動線程
還可以創建一個實現Runnable接口的匿名類,或者創建一個實現Runnable接口的Java Lambda表達式(JDK8之后)。
具體代碼如下:
//定義一個類,實現Runnable接口
public class MyRunnable implements Runnable {
//重寫run方法
@Override
public void run() {
//自己的代碼邏輯
.......
}
}
public class Main{
public static void main(String [] args){
//實例化一個Runnable實現類的實例
MyRunnable myRunnable = new MyRunnable();
//以myRunnable為參數實例化一個Thread類
Thread thread = new Thread(myRunnable);
//調用start方法啟動線程
thread();
/**
*--------分割線------
*/
//創建Runnable的匿名實現
Runnable myRunnable =new Runnable(){
public void run(){
//自己的代碼邏輯
.......
}
}
//Runnable的Lambda實現
Runnable runnable =() -> { //自己的代碼邏輯};
}
注意:
1、使用"myThread.run();"時,run()方法并非是由剛創建的新線程執行,而是被創建新線程的當前線程所執行了。想要讓創建的新線程執行run()方法,必須調用新線程的start()方法。且start()方法不可以多次調用。
2、調用start()方法后,并不是讓線程立刻執行,而是將線程變為可執行狀態,等待CPU的調度。
問題來了,當用此方式創建線程后,線程執行的run()方法是Runnable接口中的還是Thread類中的?
我們看一下Thread類的定義及其run()方法:
public class Thread implements Runnable {
private Runnable target;
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
......
this.target = target;
......
}
@Override
public void run() {
if (target != null) {
target.run();
}
}
}
可以看到,在run()方法中,會判斷target是否為空,不為空則執行Runnable的run()方法,否則此方法不執行任何操作并返回。也是因為如此,Java提示Thread的子類要重寫此方法。
實現Runnable接口比繼承Thread類的優勢:
1、避免了Java中的單繼承限制
2、適合多個相同的程序代碼的線程去處理同一個共享資源
3、代碼可以被多個線程共享且代碼和數據獨立,增加了程序的健壯性
通過Callable和FutureTask
創建步驟為:
1、創建一個Callable接口的實現類,并實現call()方法
2、使用FutureTask類包裝Callable實現類的對象,封裝了Callable的call()方法的返回值
3、以FutureTask對象為Thread的參數創建線程,并啟動線程
4、調用FutureTask對象的get()方法,獲取線程執行結束后的返回值
代碼如下:
//創建一個Callable接口的實現類,并實現call()方法
public class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
//自己的代碼邏輯
return value;
}
}
public class Main{
public static void main(String [] args){
//使用FutureTask類包裝Callable實現類的對象
MyThread myThread = new MyThread();
FutureTask futureTask = new FutureTask<Integer>(myThread);
//以FutureTask對象為Thread的參數創建線程
Thread thread = new Thread(futureTask);
thread.start();
//獲取值
try {
//get()方法會阻塞,直到子線程執行結束才返回
int x = (int) futureTask.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
其中,Callable的類型參數為返回值的類型,Future保存異步計算的結果。
在計算過程中,可以使用isDone方法判斷Future任務是否結束(包括正常結束或者中途退出),返回true表示完成,返回false表示未完成。可以用cancal方法取消計算。
一般推薦使用實現Runable或Callable接口的方式來創建多線程。因為這樣既可以繼承其他類,而且多線程可以共享一個target,即多線程可以共同處理同一份資源。
線程的優先級及守護線程
線程優先級
每個線程都有優先級,且默認情況下,線程繼承其父類的優先級。我們可以使用setPriority方法為線程設置優先級。可以將線程的優先級設置在MIN_PRIORITY(1)和MAX_PRIORITY(10)之間。NORM_PRIORITY表示線程優先級為5,為默認優先級。數字越大,表示優先級越高。高優先級的線程被CPU調用的概率大于低優先級的線程。不過要注意的是線程優先級無法保證線程的執行順序,它是依賴于平臺的。比如在Linux下,線程優先級僅適用于Java6之后,在這之前線程優先級沒有作用。
守護線程
在Java中,線程可以分為用戶線程和守護線程,可以使用Thread類的setDaemon方法將一個線程設置為守護線程。守護線程在后臺運行,當JVM中沒有其他非守護線程時,守護線程會和JVM一起結束。守護線程的作用是為其他線程提供服務,比如我們熟悉的GC就是這樣。要注意的是,不要用守護線程訪問文件或數據庫等資源。因為守護線程可能在任何時候發生中斷,而這個時候,我們對資源文件的讀寫有可能還沒有完成。
有時候主線程都結束了,守護線程還在執行,這是因為線程結束是需要時間的。
Thread類的部分方法
start()方法
start方法為將線程由新建狀態轉變為可運行狀態,其源碼如下:
//表示Java線程狀態的工具,初始化為線程未啟動
private volatile int threadStatus = 0;
//線程是否運行的標志
boolean started = false;
//此線程的線程組
private ThreadGroup group;
public synchronized void start() {
//判斷線程是否未啟動或者已經運行,滿足一項則拋出異常
if (threadStatus != 0 || started)
throw new IllegalThreadStateException();
//添加次線程到其線程組
group.add(this);
//將運行狀態置為false
started = false;
try {
//調用本地方法啟動線程
nativeCreate(this, stackSize, daemon);
//將線程的運行狀態置為true
started = true;
} finally {
try {
//如果啟動失敗,從其線程組中移除此線程
//此線程組的狀態將回滾,就像從未嘗試啟動線程一樣。該線程再次被視為線程組的未啟動成員,允許隨后嘗試啟動該線程。
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
可以看到,當一個線程是未啟動或者已運行時,調用start方法將拋出異常。當線程啟動失敗后,會將其從線程組中移除,以讓其有機會重新啟動。
sleep()方法
使當前正在執行的線程休眠(暫時停止執行),該線程不會失去任何監視器的所有權。源碼如下:
private static final int NANOS_PER_MILLI = 1000000;
public static void sleep(long millis) throws InterruptedException {
Thread.sleep(millis, 0);
}
//實際調用的方法
public static void sleep(long millis, int nanos)
throws InterruptedException {
//判斷傳入的毫秒和納秒是否有錯誤
if (millis < 0) {
throw new IllegalArgumentException("millis < 0: " + millis);
}
if (nanos < 0) {
throw new IllegalArgumentException("nanos < 0: " + nanos);
}
if (nanos > 999999) {
throw new IllegalArgumentException("nanos > 999999: " + nanos);
}
//零睡眠
if (millis == 0 && nanos == 0) {
//如果線程為中斷狀態,則拋出異常并返回
if (Thread.interrupted()) {
throw new InterruptedException();
}
return;
}
//返回運行的Java虛擬機的高分辨率時間源的當前值,以納秒計。
long start = System.nanoTime();
//將傳入的時間轉為納秒級
long duration = (millis * NANOS_PER_MILLI) + nanos;
//獲取鎖
Object lock = currentThread().lock;
//等待可能會提前返回,所以循環直到睡眠時間結束。
synchronized (lock) {
while (true) {
//調用本地方法
sleep(lock, millis, nanos);
long now = System.nanoTime();
long elapsed = now - start;
if (elapsed >= duration) {
break;
}
duration -= elapsed;
start = now;
millis = duration / NANOS_PER_MILLI;
nanos = (int) (duration % NANOS_PER_MILLI);
}
}
}
由上面的源碼可以看出,我們最終調用的是sleep(long millis, int nanos)方法。在sleep方法中,是通過循環不斷判斷當前時間跟起始時間的差值,直到這個值大于等于我們傳入的休眠時間,則線程可以繼續工作。在此期間,當前線程為阻塞狀態。
yield()方法
線程執行此方法的作用是暫停當前正在執行的線程,使其他具有相同優先級的線程獲得運行的機會。但是在實際中,我們不能保證其功能可以完全實現,因為yield是將線程從運行狀態變為可運行狀態,在這種情況下,當前線程可能會被CPU再次選中。此方法為本地方法,jdk中源碼如下:
public static native void yield();
join()方法
此方法為讓一個線程加入到另一個線程的后面,在前面的線程沒有結束的時候,后面的線程不被執行。調用此方法會導致線程棧發生變化,當然,這些變化都是瞬時的。
//負責此線程的join / sleep / park操作的同步對象
private final Object lock = new Object();
public final void join() throws InterruptedException {
//調用join(long millis)方法
join(0);
}
public final void join(long millis, int nanos)
throws InterruptedException {
synchronized(lock) {
//判斷傳入的參數是否正確
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
//根據條件判斷millis是否要加1
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
//調用join(long millis)方法
join(millis);
}
}
//真正被調用的方法
public final void join(long millis) throws InterruptedException {
synchronized(lock) {
//返回當前時間
long base = System.currentTimeMillis();
long now = 0;
//判斷傳入的參數是否正確
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
//以下就是根據isAlive(線程是否存活),調用wait方法的循環
if (millis == 0) {
while (isAlive()) {
lock.wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
lock.wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
}
wait()方法的作用:導致當前線程等待,直到另一個線程調用此對象的notify()方法或notifyAll()方法或指定的等待時間已經過去。
由源碼可知,我們可以自己設置等待時間。但是如果我們不設置等待時間或者設置的等待時間為0,則線程會永遠等待。直到被其join的線程結束后,會調用this.notifyAll方法,使其結束等待。
未捕獲異常處理器
UncaughtExceptionHandler:是在Java Thread類中定義的,當Thread由于未捕獲的異常而突然終止時調用的處理程序接口。這個接口只有一個方法:
//當給定線程由于給定的未捕獲異常而終止時調用的方法。Java虛擬機將忽略此方法拋出的任何異常
void uncaughtException(Thread t, Throwable e);
當一個線程由于未捕獲的異常而即將終止時,Java虛擬機將使用getUncaughtExceptionHandler向線程查詢其UncaughtExceptionHandler并將調用處理程序的uncaughtException方法,將線程和異常作為參數傳遞。如果某個線程沒有顯式設置其UncaughtExceptionHandler,則其ThreadGroup對象將充當其UncaughtExceptionHandler。如果ThreadGroup對象沒有處理異常的特殊要求,它可以將調用轉發到getDefaultUncaughtExceptionHandler默認的未捕獲異常處理程序。我們可以用setUncaughtExceptionHandler方法為任何線程設置一個處理器。也可以用Thread類的靜態方法setDefaultUncaughtExceptionHandler為所有線程設置一個默認的處理器。我們可以通過實現Thread.UncaughtExceptionHandler接口并重寫其uncaughtException方法來自定義一個未捕獲異常處理器。
小結
本文主要是簡單的介紹一下線程的相關概念,使大家對線程有基本的了解。本文中涉及到的鎖及介紹的相關方法,會在后期的分析中一一講解。