您好,登錄后才能下訂單哦!
本篇內容介紹了“java線程的原理和實現方式”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
我們都知道,實現多線程的方式是繼承Thread類和實現Runable接口,那么除了java中不允許多繼承的這個特性,他們之間還有什么區別呢?我們看下面這個例子:
//繼承Thread類 public class ThreadTest extends Thread { private AtomicInteger count = new AtomicInteger(0); @Override public void run() { for(int i=0;i<5;i++){ System.out.println(Thread.currentThread().getName() + " " + count.incrementAndGet()); } } public static void main(String[] args) { new ThreadTest().start(); new ThreadTest().start(); } }
//實現Runable接口 public class RunnableTest implements Runnable { private AtomicInteger count = new AtomicInteger(0); @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + " " + count.incrementAndGet()); } } public static void main(String[] args) { RunnableTest rab=new RunnableTest(); new Thread(rab).start(); new Thread(rab).start(); } }
通過觀察console信息我們可以看到,繼承Thread類,無法共享對象資源,而實現Runable則可以;這讓我們可以針對不同的業務情況作出不同的選擇。
Thread.State枚舉中定義了線程的六種狀態
狀態 | 說明 |
---|---|
NEW | new 對象后線程狀態 |
RUNNABLE | start方法調用后;或者waiting狀態下的線程調用了notify、LockSupport.Unpark等方法 |
BLOCKED | 等待synchronized代碼塊時的狀態 |
WAITING | 調用wait()、join(),LockSupport.park()方法后 |
TIMED_WAITING | 調用wait(long)、sleep(long),join(long)方法后 |
TERMINATED | 線程運行完畢,或者調用terminate方法成功 |
關于線程狀態流轉,看下面這幅圖;左邊是正常狀態流轉,右邊是存在block和waiting的情況:
網上有些文章說線程還分為ready和running狀態,這里需要注意的是,這兩種執行狀態都屬于Runable狀態;ready和running狀態其實是描述VM/OS是否線程分配CPU資源,JVM并不能決定這一事情。當然運行中的線程通過調用yield方法是可以將running狀態的線程切換到ready狀態,但這一過程是OS層面的事情而非JVM層面。
在實現多線程的過程中,我們需要用到相關方法來進行線程狀態的切換,已達到不同的目的。
Thread類的相關方法
方法 | 說明 |
---|---|
start | 啟動線程,使線程進入運行狀態 |
setPriority | 給線程設置優先級,有三個可選項:Thread.MIN_PRIORITY 最低優先級、Thread.NORM_PRIORITY 普通、Thread.MAX_PRIORITY 最高 |
sleep | 使線程進入睡眠狀態,需要指定時間,線程進入等待狀態,此時線程仍然持有鎖 |
yield | 使線程交出CPU資源給優先級是同級及以上的線程,不可以指定時間,線程仍然是RUNABLE狀態,此時線程仍然持有鎖 |
interrupt | 終止未執行的線程,正在執行的線程不受影響,被終止的線程進入TERMINATED終止狀態 |
stop | 強行終止線程,不管線程是否在執行中,此方法為廢棄方法,不是線程安全方法,因為會釋放線程持有的鎖導致數據不一致的問題產生 |
join | 等待線程執行完畢,一般是在主線程中等待子線程執行結束,此方法會阻塞當前線程 |
Object類的相關方法
Obj類主要有wait()和notify()、notifyAll()三個方法來控制多線程讀共享數據的訪問;注意這三個方法不是Thread類的,故只能在同步代碼塊中才會產生效果。
方法 | 說明 |
---|---|
wait | 使線程暫停,并釋放持有的鎖,需要手動調用notify方法喚醒;線程暫停后會放入等待隊列 |
wait(long) | 同wait,但是可以指定暫停的時間 |
notify | 喚醒暫停的線程,將線程移出等待隊列;線程喚醒后并不立即執行,而是放入獲取鎖的隊列中等待獲取鎖 |
notifyAll | 喚醒所有暫停的線程,將所有線程沖等待隊列中移出,并放入獲取鎖的隊列中 |
我們在系統中直接new線程是可以實現多線程,但是存在以下弊端:
線程不能重用,過多的線程創建和銷毀會降低系統性能
不能控制線程并發數量
不能指定線程的定時執行等
ThreadPoolExecutor讓我們可以自定義線程池,它有三個構造函數,它的參數含義如下 :
參數 | 說明 |
---|---|
int corePoolSize | 核心線程數量,當線程池內的線程數小于corePoolSize,會新建線程馬上運行任務;這些線程創建后不會進行回收操作,即便是閑置狀態 |
int maximumPoolSize | 最大線程數量,此參數需大于corePoolSize;當添加任務時,如果當前線程池線程數量大于corePoolSize,會提交給等待隊列,當隊列滿了后,會新建線程(運行線程數不超過maximumPoolSize情況下),這一部分線程我們稱之為非核心線程數,它和核心線程沒有區別,只是系統會定時回收線程,最終線程數會保持在corePoolSize數量 |
long keepAliveTime | 非核心線程的回收時間,默認60 |
TimeUnit unit | keepAliveTime的單位,默認秒 |
BlockingQueue<Runnable> workQueue | 線程池中的任務隊列,提交任務時如果核心線程數達到,則會提交到這里排隊 |
ThreadFactory threadFactory | 創建線程的工廠,可以給每個創建出來的線程設置名字。一般情況下無須設置該參數 |
RejectedExecutionHandler handler | 拒絕策略,這是當任務隊列和線程池都滿了時所采取的應對策略,默認是AbordPolicy,表示直接拋出RejectedExecutionException 異常 |
任務隊列
workQueue指明了當核心線程數達到最大時,任務的排隊策略;有三種類型:
參數 | 說明 | 缺點 |
---|---|---|
直接提交 | 例如:SynchronousQueue(默認),這是一個沒有數據緩沖的阻塞隊列,隊列中只能存放一個元素,超出之后后續線程提交會阻塞(maximumPoolSize達到閾值情況下) | 線程阻塞 |
無界隊列 | 例如:不設定容量的LinkedBlockingQueue,當核心線程數滿了之后,任務提交后可以一直存放在隊列中 | maximumPoolSize失效,且有OOM風險 |
有界隊列 | 例如:ArrayBlockingQueue,當核心線程數滿了之后,一定量的任務提交可以存放在隊列中,但是后續如果添加新的任務,需要有拒絕策略 | 需要指定拒絕策略 |
拒絕策略
在使用有界隊列的前提下,如果工作隊列已滿,我們需要設定拒絕策略
參數 | 說明 |
---|---|
ThreadPoolExecutor.AbortPolicy | 默認策略;直接拋出RejectedExecutionException異常 |
ThreadPoolExecutor.CallerRunsPolicy | 將任務交給主線程執行,通過阻塞主線程達到減緩提交的作用 |
ThreadPoolExecutor.DiscardPolicy | 直接丟棄當前任務 |
ThreadPoolExecutor.DiscardOldestPolicy | 丟棄最老的任務 |
以上拒絕策略中,除了CallerRunsPolicy其他的都好理解,下面我們使用CallerRunsPolicy策略來和DiscardPolicy進行一個比對:
我們定義了最大線程是3個,排隊兩個,線程睡眠0.5s以達到效果;使用DiscardPolicy策略我們可以看到10個任務丟棄了5個。
但是CallerRunsPolicy策略會讓主線程來執行任務,同時將主線程阻塞,已達到延緩提交任務的效果;當主線程執行完畢后,線程池內任務也執行完畢了,這時線程池會繼續接受后續任務。
我們可以通過submit、execute方法提交任務給線程池執行,其中submit方法有三個重載,他們有以下區別
方法 | 說明 | 是否關心結果 |
---|---|---|
void execute | 無返回值,提交后任務和主線程再無瓜葛 | 否 |
<T> Future<T> submit(Callable<T> task) | 返回一個代表執行結果的Future對象,當調用Future的get方法時會獲取到執行結果,如果線程發生異常會獲取到異常信息 | 是 |
Future<?> submit(Runnable task) | 返回一個代表執行結果的Future對象,,當調用Future的get方法時,成功返回null,如果線程發生異常會獲取到異常信息 | 是 |
<T> Future<T> submit(Runnable task, T result) | 當線程正常結束的時候調用Future的get方法會返回result對象,當線程拋出異常的時候會獲取到對應的異常的信息 | 是 |
CPU密集型任務,就需要盡量壓榨CPU,參考值可以設為 NCPU+1
IO密集型任務,參考值可以設置為2*NCPU
上面ThreadExecutorPool提供給我們手動實現線程池的方式,同時J.U.C包下面提供了線程池Executors類,可以讓我們方便的創建線程池。
Executors提供了5種線程池的實現方式,以針對不同的業務場景:
從上圖中我們可以看到,除了newWorkStealingPool以外,其他的線程池都只是通過ThreadExecutorPool構造函數,傳遞不同的參數而實現不同的效果,這也是本篇博客為什么先介紹ThreadExecutorPool的原因;雖然這幾種線程池區別已經一目了然,我們還是列舉一下它們的特點:
方法 | 說明 | 缺點 |
---|---|---|
newCachedThreadPool | 初始不指定線程池大小,提交任務后就創建線程,每隔60s回收一下空閑線程 | 最大并發數不可控制:導致系統資源耗盡;不拒絕任務:存在OOM風險 |
newFixedThreadPool | 指定固定大小線程池,不存在非核心線程,使用無界隊列 | 無界隊列:存在OOM風險 |
newScheduledThreadPool | 在手動創建ThreadExecutorPool的基礎上加了一個定期任務,例如給線程池提交了兩個任務,設置10s運行一次這兩個任務 | 需要注意ThreadExecutorPool參數 |
newSingleThreadExecutor | 只有一個線程的線程池,使用無界隊列 | 單線程略顯單薄;無界隊列:存在OOM風險 |
newWorkStealingPool | 返回一個ForkJoinPool而不是ThreadExecutorPool對象,可以指定線程數,詳情見下面ForkJoinPool描述 | 適用于大任務情況 |
ForkJoinPool
ForkJoinPool適用于大型任務,其核心是Fork和Join;Fork可以將一個大任務拆分為多個小的任務,Join會將多個小的任務的結果匯總達到最終運行效果。
舉個例子:假設一個線程池中并發數控制在兩個,但是每個任務都要運行1分鐘;假設我們當前服務器有4個CPU,兩個在處理任務,其他的則為空閑狀態,這時剩下的兩個空閑CPU資源是浪費的。
試想一下:如果們能使剩下的兩個空閑的CPU也能利用起來處理上面兩個任務,任務運行肯定會加快,這就是ForkJoinPool的設計出發點。
多數情況下,不推薦使用Executors,而推薦手動創建ThreadExecutorPool,因為Executors的五種實現方式有一下缺點:
要么不能控制并發:資源耗盡、OOM
要么不能設置等待隊列大小:OOM
不能指定拒絕策略
方法 | 說明 |
---|---|
shutdown | 關閉線程池,執行以前提交的任務,但是不接受新提交的任務 |
isTerminated | 如果調用了shutdown,并且所有任務已經完成,則返回true,否則永遠false |
getActiveCount | 線程池存貨的數量 |
getQueue().size | 等待隊列大小 |
getPoolSize | 線程池中當前線程數量 |
getLargestPoolSize | 曾經有過的最大線程數量 |
getTaskCount | 未完成的任務數量 |
getCompletedTaskCount | 完成的任務數量 |
“java線程的原理和實現方式”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。