創建線程的四種方法以及區別
1.線程是什麼?
線程被稱爲輕量級進程,是程序執行的最小單位,它是指在程序執行過程中,能夠執行代碼的一個執行單位。每個程序程序都至少有一個線程,也即是程序本身。
2.線程狀態
Java語言定義了5種線程狀態,在任意一個時間點,一個線程只能有且只有其中一個狀態。,這5種狀態如下:
(1)新建(New):創建後尚未啓動的線程處於這種狀態
(2)運行(Runable):Runable包括了操作系統線程狀態的Running和Ready,也就是處於此狀態的線程有可能正在執行,也有可能正在等待着CPU爲它分配執行時間。
(3)等待(Wating):處於這種狀態的線程不會被分配CPU執行時間。等待狀態又分爲無限期等待和有限期等待,處於無限期等待的線程需要被其他線程顯示地喚醒,沒有設置Timeout參數的Object.wait()、沒有設置Timeout參數的Thread.join()方法都會使線程進入無限期等待狀態;有限期等待狀態無須等待被其他線程顯示地喚醒,在一定時間之後它們會由系統自動喚醒,Thread.sleep()、設置了Timeout參數的Object.wait()、設置了Timeout參數的Thread.join()方法都會使線程進入有限期等待狀態。
(4)阻塞(Blocked):線程被阻塞了,“阻塞狀態”與”等待狀態“的區別是:”阻塞狀態“在等待着獲取到一個排他鎖,這個時間將在另外一個線程放棄這個鎖的時候發生;而”等待狀態“則是在等待一段時間或者喚醒動作的發生。在程序等待進入同步區域的時候,線程將進入這種狀態。
(5)結束(Terminated):已終止線程的線程狀態,線程已經結束執行。
下圖是5種狀態轉換圖:
3.線程同步方法
線程有4中同步方法,分別爲wait()、sleep()、notify()和notifyAll()。
wait():使線程處於一種等待狀態,釋放所持有的對象鎖。
sleep():使一個正在運行的線程處於睡眠狀態,是一個靜態方法,調用它時要捕獲InterruptedException異常,不釋放對象鎖。
notify():喚醒一個正在等待狀態的線程。注意調用此方法時,並不能確切知道喚醒的是哪一個等待狀態的線程,是由JVM來決定喚醒哪個線程,不是由線程優先級決定的。
notifyAll():喚醒所有等待狀態的線程,注意並不是給所有喚醒線程一個對象鎖,而是讓它們競爭。
4.創建線程的方式
在JDK1.5之前,創建線程就只有兩種方式,即繼承java.lang.Thread類和實現java.lang.Runnable接口;而在JDK1.5以後,增加了兩個創建線程的方式,即實現java.util.concurrent.Callable接口和線程池。下面是這4種方式創建線程的代碼實現。
1)繼承Thread類創建線程
class MyThread extends Thread{
@Override
public void run() {
// 重寫run方法
}
}
2)實現Runnable接口創建線程
public class ThreadTest {
public static void main(String[] args) {
MyThread thread = new MyThread();
Thread tt = new Thread(thread);
}
}
class MyThread implements Runnable{
@Override
public void run() {
// 重寫run方法
}
}
3)使用Callable和Future創建線程
public class ThreadTest {
public static void main(String[] args) throws Exception {
// 創建Callable
MyCallable myCallable = new MyCallable();
// 創建FutureTask
FutureTask<Integer> futureTask = new FutureTask<Integer>(myCallable);
// 配置到Thread
Thread thread = new Thread(futureTask, "AAA");
// 啓動線程
thread.start();
// 獲取返回值
int result01 = 100;
while(!futureTask.isDone()){
// 等待計算結果
}
// 阻塞計算,如果超過時間 拋出java.util.concurrent.TimeoutException異常
int result02 = futureTask.get(2, TimeUnit.SECONDS);
System.out.println("計算結果\t" + (result01 + result02));
}
}
class MyCallable implements Callable<Integer>{
@Override
public Integer call() throws Exception {
try { TimeUnit.MILLISECONDS.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); }
return 1024;
}
}
4)使用線程池例如用Executor框架
public static void main(String[] args) {
// ExecutorService threadPool = Executors.newFixedThreadPool(5);// 一池5個處理線程
// ExecutorService threadPool = Executors.newSingleThreadExecutor();// 一池1個處理線程
ExecutorService threadPool = Executors.newCachedThreadPool();//一池N處理線程
// 模擬10個用戶來辦理業務,每個用戶就是一個來自外部的請求線程
try {
for (int i = 1; i <= 50; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName() + "\t辦理業務");
try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
爲什麼使用線程池?
線程池的工作主要是控制運行的線程的數量,處理過程中將任務放入隊列,然後在線程創建後啓動這些任務,如果線程數量超過了最大數量之後超出數量的線程排隊等候,等其他線程執行完畢,再從隊列中取出任務來執行。
線程池主要特點:線程複用、控制最大併發數、管理線程
- 降低資源消耗,通過重複利用已創建的線程降低縣城創建和銷燬造成的消耗。
- 提高響應速度。當任務到達時,任務可以不需要的等到線程創建就能立即執行。
- 提高現成的可管理型,線程是稀缺資源,如果無限的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控。
線程池的種類:
- Executors.newScheduledThreadPool(); // 帶調度的線程池
- Executors.newWorkStealingPool(int); // JDK1.8新出
- Executors.newFixedThreadPool(int);
- Executors.newSingleThreadExecutor();
- Executors.newCachedThreadPool();
線程池源碼解析:
1、定長線程池:
- 創建一個定長線程池,可控制現成最大併發數,超出的線程會在隊列中等待
- newFixedThreadPool創建的線程池corePoolSize和maximumPoolSize值是相等的,它使用LinkedBlockingQueue;
// 定長線程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
2、單個線程線程池:
- 創建一個線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序執行。
- newSingleThreadExecutor將corePoolSize和maximunPoolSize都設置爲1,它使用的LinkedBlockingQueue。
// 單個線程的線程池
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
3、可緩存線程池:
- 創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閒線程,若無可回收,則新建線程。
- newCachedThreadPool將corePoolSize設置爲0,將maximumPoolSize設置爲Integer.MAX_VALUE,它使用的SynchronousQueue,也就是說來了任務就創建線程運行,當線程空閒超過60秒,就銷燬線程。
// 緩衝池線程
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
4、線程池的七大參數
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
- int corePoolSize:線程池中的常駐核心線程數
- int maximumPoolSize:線程池能夠容納同時執行的最大線程數,此值必須大於等於1
- long keepAliveTime:多餘的空閒線程的存活時間,當前線程池數量超過corePoolSize時,當空閒時間達到keepAliveTime值時,多餘空閒線程會被銷燬直到只剩下corePoolSize個線程爲止
- TimeUnit unit:keepAliveTime的單位
- BlockingQueue<Runnable> workQueue:任務隊列,被提交但尚未被執行的任務。
- ThreadFactory threadFactory:表示生成線程池中工作線程的線程工廠,用於創建線程一般用默認的即可
- RejectedExecutionHandler handler:拒絕策略,表示當隊列滿了並且工作線程大於等於線程池的最大線程數
- 在創建了線程池後,等待提交過來的任務請求。
- 檔調用execute()方法添加一個請求任務時,線程池會做如下判斷:
- 如果正在運行的線程數量小於corePoolSize,那麼馬上創建線程運行這個任務
- 如果正在運行的線程數大於或等於corePoolSize,那麼將這個任務放入隊列
- 如果這時候隊列滿了且正在運行的線程數量還小於maximumPoolSize,那麼還是要創建非核心線程立刻運行這個任務
- 如果隊列滿了且正在運行的線程數量大於或等於maximumPoolSize,那麼線程池會自動飽和拒絕策略來執行。
- 當一個線程完成任務時,它會從隊列中取下一個任務來執行
- 當一個線程無事可做超過一定時間(KeepAliveTime)時,線程池會判斷
- 如果當前運行的線程數大於codePoolSize,那麼這個線程就被停掉
- 所以線程池的所有任務完成後它最終會收縮到corePoolSize的大小
線程池的拒絕策略
等待隊列也已經排滿了,再也塞不下新任務了,同時線程池中的max線程也達到了,無法繼續爲新任務服務。這時候我們就需要拒絕策略機制合理的處理這個問題。
- AbortPolicy(默認):直接拋出java.util.concurrent.RejectedExecutionException異常阻止系統正常運行。
- CallerRunsPolicy:"調用者運行"一種調節機制,該策略既不會拋棄任務,也不會拋出異常,而是將某些任務回退到調用者,從而降低新任務的流量。
- DiscardOldestpolicy:拋棄隊列中等待最久的任務,然後把當前任務加入隊列中嘗試再次提交任務。
- DiscardPolicy:直接丟棄任務,不予任何處理也不拋出異常。如果允許任務丟失,這是最好的一種方案。
AbortPolicy:丟棄任務,拋出異常
拋出java.util.concurrent.RejectedExecutionException異常
DiscardPolicy:丟棄任務,不拋出異常
CallerRunsPolicy:任務回退給調用者
pool-1-thread-1 辦理業務 1
main 辦理業務 2
DiscardOldestPolicy:丟棄隊列中排在首位的數據
class MyTask implements Runnable {
private int taskId;
private String taskName;
public MyTask(int taskId, String taskName) {
this.taskId = taskId;
this.taskName = taskName;
}
public int getTaskId() {
return taskId;
}
public void setTaskId(int taskId) {
this.taskId = taskId;
}
public String getTaskName() {
return taskName;
}
public void setTaskName(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
try {
System.out.println("run taskId =" + this.taskId);
Thread.sleep(5 * 1000);
//System.out.println("end taskId =" + this.taskId);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public String toString() {
return Integer.toString(this.taskId);
}
}
public class MyThreadDemo2 {
public static void main(String[] args) {
/**
* 在使用有界隊列時,若有新的任務需要執行,如果線程池實際線程數小於corePoolSize,則優先創建線程,
* 若大於corePoolSize,則會將任務加入隊列,
* 若隊列已滿,則在總線程數不大於maximumPoolSize的前提下,創建新的線程,
* 若線程數大於maximumPoolSize,則執行拒絕策略。或其他自定義方式。
*
*/
ThreadPoolExecutor pool = new ThreadPoolExecutor(
1, //coreSize
2, //MaxSize
60, //60
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(3), //指定一種隊列 (有界隊列)
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy()
);
MyTask mt1 = new MyTask(1, "任務1");
MyTask mt2 = new MyTask(2, "任務2");
MyTask mt3 = new MyTask(3, "任務3");
MyTask mt4 = new MyTask(4, "任務4");
MyTask mt5 = new MyTask(5, "任務5");
MyTask mt6 = new MyTask(6, "任務6");
pool.execute(mt1);
pool.execute(mt2);
pool.execute(mt3);
pool.execute(mt4);
pool.execute(mt5);
pool.execute(mt6);
pool.shutdown();
}
}
問題2:你在工作中單一/固定數的/可變的三種創建線程池的方法,你用哪個多?超級大坑
回答:java自帶的基本不用,高併發直接引起OOM,一般需要有一個管理者,如自己寫個類來管理,或者交給Spring來管理,主要是控制線程數量避免引起OOM。
問題3:你在工作中是如何使用線程池的,是否自定義過線程池使用
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
1,
10,
1L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
問題4:合理配置線程池是如何考慮的?
首先判斷業務密度
// 查看計算機核心數
System.out.println(Runtime.getRuntime().availableProcessors());
CPU密集型:該任務需要大量的運算,而沒有阻塞,CPU一直全力運行。 一般公式:CPU核心數+1個線程的線程池
IO密集型:任務線程並不是一直在執行任務,則應配置儘可能多的線程,如CPU核心數*2
參考公式:CPU核心數 / (1 - 阻塞係數) 阻塞係數在0.8~0.9之間
比如:8核CPU 8 / (1 -0.9) = 80 個線程數
問題5:線程池的submit和execute方法區別?
- 接收的參數不一樣
- submit有返回值,而execute沒有
- submit方便Exception處理
死鎖
是指兩個或兩個以上的進程在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力干涉那他們都將無法推進下去,如果系統資源充足,進程的資源請求能夠得到滿足,死鎖出現的可能性就很低,否則會因爭奪有限的資源而陷入死鎖。
產生死鎖的原因:
- 系統資源不足
- 進程運行推進的順序不合適
- 資源分配不當
問題排查:
- jps命令定位進程號
- jstack找到思索查看堆棧軌跡