1. 線程概念
- 進程
進程是指運行中的應用程序,每個進程都有自己獨立的地址空間(內存空間)。是操作系統分配資源的最小單位。比如打開一個瀏覽器它就是一個進程。 - 線程
線程是進程中的一個實體,是被系統獨立調度和分派的基本單位,線程自己不擁有系統資源,只擁有一點在運行中必不可少的資源,但它可與同屬一個進程的其它線程共享進程所擁有的全部資源。一個進程內部可以有多個線程。 - 線程特點
1)線程是輕量級的進程
2)線程沒有獨立的地址空間(內存空間)
3)線程是由進程創建的(寄生在進程)
4)一個進程可以擁有多個線程,交替佔用同一個CPU資源(單核情況下是交替執行的,多核情況下可以多個CPU並行執行)
2. 線程狀態及常用操作
線程狀態
- 創建(New)
線程對象被創建後,就進入了新創建狀態。 - 就緒(Runnable)
線程對象調用start方法後進入就緒狀態,此時該線程隨時可能被CPU調度執行 - 運行(Running)
線程獲取CPU權限進行執行。線程只能從就緒狀態進入到運行狀態。 - 阻塞(Blocked)
線程因爲某種原因放棄CPU使用權,暫時停止運行。阻塞狀態分爲以下三種情況
1)等待阻塞:通過調用線程的wait()方法,讓線程等待其他工作先完成
2)同步鎖阻塞:線程未搶佔到同步鎖將進入同步阻塞狀態
3)其他阻塞:過調用線程的sleep()或join()或發出了I/O請求時,線程會進入到阻塞狀態 - 死亡(Dead)
線程運行完或異常退出,該線程結束生命週期。
線程操作方法
基本原則(高能)
:線程(Thread)方法分爲靜態方法和非靜態方法,非靜態方法作用在調用者上,靜態方法作用在當前運行的線程上。有了這個基本原則理解以下方法將會事半功倍。
1)start
:Thread非靜態方法。啓動一個新線程,線程狀態從創建狀態變爲就緒狀態。當線程有機會獲取CPU資源時,開始執行內部run方法中的代碼。
2)sleep
:Thread靜態方法。讓當前正在運行的線程
休眠(掛起)一段指定的時間,線程狀態變爲阻塞狀態。當指定的休眠時間完成後被喚醒進行就緒狀態(注意不是運行狀態,需要搶佔CPU資源後才能被運行)。此方法爲Thread類的靜態方法,提供的方法參數有毫秒+納秒的方式,在實際使用中可以使用TimeUnit
工具類中的方法,在參數傳遞時更加的方便。可以思考sleep(0)
的意義。
3)join
:Thread非靜態方法。讓當前正在運行的線程
阻塞,等待調用join方法的線程執行完畢。簡單的可以理解爲讓調用join方法的線程進行插隊先運行完畢。這個方法也可以直接傳入一個時間值,表示允許插隊線程
執行的超時時間。此方法內部是通過對象的wait方法實現的,一般用來控制多線程的順序執行
。比如,在線程A中調用線程B對象的join方法,線程A將被阻塞直到線程B執行完畢,然後將線程A喚醒。
public static void main(String[] args) {
Thread b = new Thread(()->{
System.out.println("thread "+Thread.currentThread().getName()+" start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread "+Thread.currentThread().getName()+" over");
}, "threadB");
Thread a = new Thread(()->{
System.out.println("thread "+Thread.currentThread().getName()+" start");
try {
b.start();
b.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread "+Thread.currentThread().getName()+" over");
}, "threadA");
a.start();
}
如上面代碼示例,main線程中啓動線程a。在運行線程a時,當前運行線程爲線程a。在線程a內部啓動線程b並調用線程b對象的join方法,線程b插入運行,線程a被阻塞,線程a等待線程b執行完畢後再被喚醒繼續執行。
代碼運行結果爲:
thread threadA start
thread threadB start
thread threadB over
thread threadA over
4)wait
:Object非靜態方法。通過講解join方法,我們知道join是通過wait方法來實現的。join方法是調用join方法的線程循環判斷該線程是否存活,存活則一直wait,線程死亡則退出。wait方法是屬於Object對象的方法,如對象a通過a.wait()來調用,當前正在運行的線程(執行a.wait()代碼的線程)將被阻塞等待,並且將會釋放持有對象a的鎖。當其他線程調用對象a的notify或者notifyAll方法時,纔會將等待在對象a上的線程喚醒到就緒狀態。該方法必須在同步代碼(同步方法或者同步代碼塊)中調用,不然會拋出IllegalMonitorStateException
異常。
5)notify/notifyAll
:Object非靜態方法。喚醒當前對象上的等待線程;notify()是隨機喚醒單個線程,而notifyAll()是喚醒所有的線程。和wait方法一樣,notify和notifyAll方法都是屬於Object對象的方法,必須在同步代碼中調用。
6)interrupt/interrupted/isInterrupted
: interrupt()爲Thread非靜態方法,該方法設置調用者的線程狀態爲中斷狀態,但並不是真正停止了該線程了運行,業務代碼可以根據線程中斷狀態來做業務處理;isInterrupted()Thread非靜態方法,檢測線程中斷狀態,返回對應的布爾值;interrupted()爲Thread靜態方法
,測試當前線程(currentThread)
的中斷狀態並清除該狀態,所以如果當第一次調用interrupted()返回true時,第二次調用返回false。
7)yield
: yield()方法爲Thread靜態方法
,作用是將當前運行的線程
設置爲就緒狀態,讓該線程重新去競爭CPU的調度,可以同時setPriority()方法來設置線程的優先級,優先級高的線程將有更大的機率搶佔CPU的時間片。
3. 創建線程的幾種方式
-
Runnable接口
將實現了Runnable接口的對象傳入Thread構造方法,創建線程,代碼示例如下
public class ThreadCreationTest { public static void main(String[] args) { new Thread(new ThreadImplementRunnable()).start(); } static class ThreadImplementRunnable implements Runnable { @Override public void run() { System.out.println("Create thread by implement Runnable"); } } }
-
繼承Thread類
通過繼承Thread類創建線程,示例代碼如下
public class ThreadCreationTest { public static void main(String[] args) { new ThreadExtend().start(); } static class ThreadExtend extends Thread { public void run() { System.out.println("Create thread by extends Thread"); } } }
-
Callable接口
通過將實現了Callable接口的對象構建FutureTask對象,再將FutureTask對象通過Thread的構造方法創建線程。這種方式可以實現異步請求,示例代碼如下
import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; public class ThreadCreationTest { public static void main(String[] args) { FutureTask<String> task = new FutureTask<>(new CallableImplement()); new Thread(task).start(); try { TimeUnit.MILLISECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } if(task.isDone()){ try { System.out.println(task.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } } static class CallableImplement implements Callable<String> { public String call(){ return "result"; } } }
-
線程池
jdk1.5以後可以通過java的原生API先創建線程池,然後通過線程池來執行實現了Runnable接口的對象的方式來創建和執行線程了。JUC(java.util.concurrent)包中提供了線程池的工具類Executors來幫助開發者創建不同的線程池。
1)通過Executors.newSingleThreadExecutor()的方式創建具有單一線程的線程池
2)通過Executors.newFixedThreadPool(n)的方式來創建具有固定數量的線程的線程池
3)通過Executors.newCachedThreadPool(n)的方式來創建具有可變數量的線程的線程池
4)上述幾種工具類Executors提供的創建線程池的方式分別都有些缺陷,比如最大線程數過大,等候隊列太大導致一些問題的出現。實際開發中我們可以通過new ThreadPoolExecutor()的方式創建線程池(通過源碼我們可以發現Executors提供的幾種創建線程池的方式底層還是通過ThreadPoolExecutor來創建的),根據實際環境來指定具體的參數。示例代碼如下
//ExecutorService executorService = Executors.newSingleThreadExecutor(); //方式1 //ExecutorService executorService = Executors.newFixedThreadPool(2); //方式2 //ExecutorService executorService = Executors.newCachedThreadPool(); //方式3 ExecutorService executorService = new ThreadPoolExecutor( 2, //最小核心數 5, //最大線程數 1, //最大-最小 空閒保持時間 TimeUnit.SECONDS, //時間單位 new LinkedBlockingQueue<>(3), //等候隊列 Executors.defaultThreadFactory(), //默認線程創建工廠 new ThreadPoolExecutor.CallerRunsPolicy() //當加入線程池的線程數大於線程池最大線程數加等候隊列時採用的拒絕策略 ); //方式4 try { for (int i = 0; i < 10; i++) { executorService.execute(()->{ try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()); }); } } finally { executorService.shutdown(); }
四種方式可以通過輸出的線程名字來看看具體參數的實際意義。實際開發中多用方式4來創建線程池, 最大線程數可以根據服務器的CPU核數和業務類型進行設置,其他參數也是根據業務情況進行優化設置。
以上代碼測試結果如下
pool-1-thread-4 main pool-1-thread-3 pool-1-thread-5 pool-1-thread-1 pool-1-thread-2 pool-1-thread-4 pool-1-thread-5 main pool-1-thread-3
代碼中設置的最大線程數是5,等候隊列爲3,拒絕策略爲扔回調用者(這裏是main線程)執行。我們加入了10個線程,所以當最大線程數加上等候隊列數爲8不夠10個線程執行的時候,有兩個線程被main線程執行了。當然這個執行結果並不一定是這樣的,有可能只有一個線程被丟回main線程執行了,也有可能全部被線程池執行完畢,和CPU的運算速度有關。