概述
- 線程就是進程中的一個獨立單元,線程控制著進程的執行,一個進程中至少有一個線程。
什么是進程呢?
- 進程是程序運行的最小單元,表示正在運行中的程序,每個進程都有一個執行順序,也就是執行路徑,或者叫控制單元。不同的進程使用不同的內存空間,而所有的線程共享一片相同的內存空間。
多線程的創建
- 繼承Thread類
public class MyThread extends Thread {
@Override
public void run() {
}
}
- 實現Runnable接口
public class MyThread implements Runnable{
@Override
public void run() {
}
}
- 實現Callable接口
public class MyCallable {
Callable<String> callable = new Callable<String>() {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
@Override
public String call() throws Exception {
return "hi callable";
}
};
public void test(String[] args) {
FutureTask futureTask = new FutureTask(callable);
Thread thread = new Thread(futureTask);
thread.start();//啟動線程
//取消線程
futureTask.cancel(true);
}
}
Thread類中的start()方法和run()方法有什么區別?
- 調用start()方法會創建一個新的線程去實質性我們的代碼,如果直接使用run()方法,它的行為就和普通的方法一樣,還是在當前線程中執行我們的代碼。
用Runnable還是Thread?
- 我們知道繼承Thread類和實現Runnable接口都可以創建一個線程,那么使用哪種方式更好呢?因為Java不支持多繼承,但是支持多實現,所以當然是實現Runnable接口更好
Runnable和Callable有什么不同?
- Runnable是重寫run方法,Callable是重寫call方法,另外call有返回值,并且支持拋出異常,Runnable沒有這些功能。但是在新的線程中new Thread(Runnable r)類只支持Runnable,不支持直接使用Callable。我們可以把Callable存儲在FutureTask()中,然后將task傳入Thread()。是因為FutureTask實現了RunnableFuture這個接口。它等價于可以攜帶結果的Runnable,并且有三個狀態:等待、運行和完成。完成包括所有計算以任意方式結束,包括正常結束,取消和異常結束。
volatile關鍵字
- volatile是一個特殊的修飾符,只有成員變量才能使用它。在Java并發程缺少同步類的情況下,多線程對同步變量操作對其他線程是透明的,volatile變量可以保證下一個讀取操作會在前一個寫操作之后發生。線程都會直接從內存中讀取該變量,并且不再緩存它。這就確保了線程讀到的變量是同內存一致的。所以volatile是用來解決可見性問題的。
ThreadLocal
- 什么是ThreadLocal?
- ThreadLocal類提供了線程本地變量,這些變量不同于普通變量,訪問線程本地變量的每個線程都有其自己獨立初始化的變量副本,因此ThreadLocal沒有多線程競爭的問題,不需要單獨進行加鎖
- 使用場景
- 每個線程都需要有數據自己的實例數據(線程隔離)
- 框架跨層數據的傳遞
- 需要參數全局傳遞的復雜調用鏈路的場景
- 數據庫連接的管理,在AOP的各種嵌套中調用保證事務的一致性
多線程的優缺點
優點
- 可以提高程序的執行效率
- 提高了CPU,內存等資源利用率
缺點
- 占用內存空間
- CPU開銷增大
- 程序復雜度上升
正是因為多線程提高了CPU和內存等資源的利用率,但是資源是有限的,所以我們也不能隨意創建線程,才引入了線程池的概念。
線程池的實現
// 重寫ThreadPoolExecutor的構造方法
public class MyThread extends ThreadPoolExecutor {
//當前線程號
static int poolNum=0;
/**
*
* @param corePoolSize:核心線程數
* @param maximumPoolSize:最大線程數
* @param keepAliveTime:線程閑置時的超時時間
* @param workQueueSize:阻塞隊列
* TimeUnit.MILLISECONDS:keepAliveTime的時間單位
* threadFactory:線程工廠
* new ThreadPoolExecutor.AbortPolicy():拒絕策略
*/
public MyThread(int corePoolSize, int maximumPoolSize, long keepAliveTime,
int workQueueSize) {
super(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(workQueueSize),
new ThreadFactory(){
@Override
public Thread newThread(Runnable runnable){
return new Thread(runnable, "FlowThread"+"-"+poolNum++);
}
},
new ThreadPoolExecutor.AbortPolicy()
);
}
}
線程池的關閉
- ThreadPoolExecutor提供了兩個方法,用于線程池的關閉,分別是shutdown()和showdownNow()。
- showdown():不會立即終止線程池,而是要等待所有任務緩存隊列都執行完畢再終止,但是再也不會接受新的任務
- shutdownNow():立即終止線程池,并嘗試打斷正在執行的任務,并清空任務緩存隊列,返回尚未執行的任務。
Executors提供四種線程池
- 一般這四種線程池我們都不會使用,所以這里我們就先不聊了
線程調度策略
搶占式調度策略
Java運行時系統的線程調度算法是搶占式的。Java運行時系統支持一種簡單地固定的優先級的調度算法。
如果一個優先級比其他線程優先級高的線程進入就緒狀態,那么系統運行時就會選擇該線程運行。
優先級高的線程搶占了其他線程的資源。但是Java運行時系統并不搶占同優先級的線程,
對于處于就緒狀態的同優先級的線程,Java采用一種簡單的,非搶占式的調度順序。
時間片輪轉調度策略
有些系統的線程調度采用把時間片輪轉調度策略。這種調度策略是對于處于就緒狀態的線程選擇優先級最高的線程,
分配一定的CPU時間。該時間過了之后,在選擇其他線程運行。只有當線程運行結束,
放棄CPU或者由于某種原因進入阻塞狀態,低優先級的線程才會有機會執行。對于優先級相同的線程都在等待CPU,
則以輪轉的方式選擇運行的線程。
線程的流轉狀態
線程的狀態可以分為7種
new:新建狀態
ready:就緒狀態
Running:可運行狀態
Terminated:終止狀態
Waiting:等待狀態
TimedWaiting:超時等待狀態
Blocked:阻塞狀態
新建狀態
- new 一個線程實例后,線程就進入了新建狀態 (new Thread)
就緒狀態(Ready)
- 線程對象創建成功后,調用該線程的start()函數,線程進入就緒狀態
運行狀態(Running)
- 線程調度程序從可運行線程池中選擇一個線程,是該線程獲得CPU時間片,該線程就進入運行狀態。
- 當線程CPU時間片執行完或者調用yield()函數,該線程就回到就緒狀態。
終止狀態(Terminated)
- 線程運行,直到執行結束或者執行過程中因意外終止,就是使該線程進入終止狀態
阻塞狀態(Blocked)
- 運行狀態的線程獲取同步鎖失敗或發出I/O請求,該線程就進入阻塞狀態。如果是獲取同步鎖失敗,JVM還會吧該線程放入鎖的同步隊列
等待狀態(Waiting)
- 運行狀態的線程執行wait(),join(),LockSupport.park()任意函數,該線程進入等待狀態。其中wait(),join()函數會讓線程釋放掉鎖,進入等鎖池。處于這種狀態的線程不會被分配CPU時間片,它們要等待被主動喚醒,否則會一直處于等待狀態。
- 執行LockSupport.unpark(t)函數喚醒指定線程,該線程就回到了就緒狀態,等待分配CPU時間片。
- 而通過notify()、notifyAll()、join()線程執行完畢方式,會喚醒等鎖池線程,從等鎖池回到就緒狀態
超時等待狀態(Timed waiting)
- 超時等待狀態和等待狀態一樣,唯一的區別就是多了超時等待機制,不會一直等待被其他線程喚醒,而是到達指定時間,主動喚醒。
- 使用wait(long),join(long),LockSupport.parkNanos(long),LockSupport.parkUtil(long),sleep(long)會觸發線程進入超時等待狀態
sychronized
- 通過加sychronized的方法和代碼塊,在多線程訪問的時候,同一時刻只能有一個線程訪問。
相關面試題
sychronized和Lock的區別?
Lock是一個接口,sychronized是一個關鍵字
Lock加鎖的時候,必須使用unlock釋放鎖,否則會造成死鎖。sychronized不需要手動釋放鎖。
lock的使用更加靈活,可以有中斷響應,有超時時間等等,sychronized會一直等待獲取鎖,直到獲取鎖成功。
Lock lock = new ReentrantLock(); lock.lock(); try { } catch (Exception e) { } finally { lock.unlock(); }
什么是死鎖
- 死鎖指的是兩個以上的線程在執行的過程中,在資源的競爭而造成的一種阻塞現象。若無外力作用,它們都無法進行下去,這時候就產生了死鎖。
怎么預防死鎖
如果獲取一個鎖獲取不到,直接return中斷當前線程執行,簡單粗暴。
為什么要使用線程池
- 如果我們在一個方法中選擇去new一個線程,這個方法被調用很多次的時候,我們就會創建很多線程,這樣不僅會消耗大量的系統資源,比如CPU資源和內存資源,還會降低系統穩定性。所以我們選擇使用線程池。因為線程池可以合理的使用線程,可以帶來一下好處:
- 1.降低資源消耗,通過重復利用創建好的線程,降低創建線程和銷毀線程帶來的資源消耗
- 2.提供響應速度,通過使用創建好的線程,減少了線程狀態的轉換,提升了響應速度
- 3.增加了線程的可管理性,線程是稀缺資源,,使用線程池可以統一分配,調優和監控。
線程池的核心屬性有哪些?
- ThreadFactory(線程工廠):用于創建線程的工廠
- corePoolSize(核心線程數):當線程池運行的線程數小于核心線程數,不管其他線程處不處于空閑狀態,都將創建一個新的線程來處理請求
- workQueue(阻塞隊列):用于保留任務,如果當前線程數已經達到了核心線程數,則判斷阻塞隊列是否已滿,如果未滿,將新的任務移交到阻塞隊列
- maxumunPoolSize(最大線程數):如果阻塞隊列滿了,當再有新的任務進來的時候,判斷是否達到最大線程數,如果未達到最大線程數,則創建一個新的線程來處理這個任務
- handler(拒絕策略):當阻塞隊列已經滿了,并且已經達到最大線程數上限,則會采取拒絕策略。注意:在線程池的線程數已經超過corePoolSize,并且有的線程的空閑時間超過了keppAliveTime(存活時間),這個線程也會觸發拒絕策略,被終止掉。直到線程池中的線程數量不大于corePoolSize的時候。
sychronized的加鎖場景范圍
//作用于非靜態方法,鎖住的對象實例是this,每一個對象實例有一個鎖
public sychronized void method() {}
//作用于靜態方法,鎖住的是類的Class對象。
public static sychronized void method() {}
//作用于Lock.class,鎖住的是Lock的Class對象
sychronized (Lock.class) {}
//作用于this,鎖住的是對象實例
sychronized(this) {}
//作用于靜態成員變量,鎖住的是該靜態成員變量
public static Object monitor = new Object();
sychronized (monitor) {};
線程池的狀態有哪些?
- running:能夠接受新的任務,并且也能處理阻塞隊列中的任務
- shutdown:不會立即終止線程池,不再接受新的任務,但是會繼續處理各種緩存隊列中的任務。調用showdown()方法進入該狀態
- stop:不接受新的任務,也不處理該隊列中的任務,中斷正在執行的任務。當線程池處于Running狀態或者shutdown狀態的時候,調用shutdownNow()方法使線程池進入此狀態
- tidying:如果所有任務都已經終止了,有效線程數為0,線程池進入該狀態后會調用terminated()方法進入terminated狀態。
- terminated(終止):在terminated()方法執行之后進入該狀態。
Java中如何保證線程的安全
- 使用sychronized加鎖
- 使用Lock加鎖
- 使用concurrent下邊的類
wait()和sleep()的區別
- sleep()屬于Thread類,wait()屬于Object類
- sleep不會讓當前線程釋放鎖,wait()會使當前線程釋放鎖
- sleep可以使用在任何地方,wait()只能使用在同步方法或者代碼塊里
- sleep()時間到了,會自動喚醒,wait()需要其他線程調用同一對象的notify()或者notifyAll()才可以喚醒
線程的sleep()方法和yield()方法的區別
- sleep()是當前線程進入超時等待狀態,yield()使當前線程進入就緒狀態。
- sleep()給其他線程執行機會的時候不考慮優先級,yield()給其他線程執行機會的時候只會給與自己優先級相同或者優先級比自己高的線程機會
線程的join()方法時干啥用的
- 等待當前線程終止,如果一個線程A執行了threadB.join();則A線程就會等待線程B執行完畢之后繼續往下執行。
什么是Executor框架?
- 無限制的創建線程會以前你程序內存移除,所以我們通常使用線程池的方式來管理線程。Executor框架就是一個創建線程池的框架
這里有篇阿里的文章,有興趣可以看一下!!