Java多線程
- Java中線程的3種建立方式:
- 多線程三種同步方式
- 線程間通信
- 線程池
-
- 線程池的建立
- 向線程池送出任務
- 關閉線程池
Java中線程的3種建立方式:
線程啟動start() 和 中斷線程interrupt()
1.繼承Thread類
建立自定義線程類并繼承Thread類–>重寫run()–>執行個體化自定義線程類–>調用自定義實作類的start()啟動線程。
public class ThreadDemo extends Thread {
@Override//重寫run()
public void run() {
System.out.println(Thread.currentThread().getName()+" subthread");
}
}
public class App {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+" main");//列印目前線程(主線程)的線程名
ThreadDemo threadDemo=new ThreadDemo();//執行個體化自定義的線程類
threadDemo.start();//開啟線程
}
}
2.實作Runnable接口
建立自定義線程類并實作Runnable接口–>實作Runnable中的run()–>執行個體化自定義的線程類–>将建立的執行個體當作Thread類構造方法的參數來執行個體化Thread類–>調用Thread類執行個體的start()方法啟動線程。
public class RunnableDemo implements Runnable {//建立自定義線程類
@Override//實作run()
public void run() {//一個線程所有的操作都要在run()中
System.out.println(Thread.currentThread().getName()+" subthread");//列印目前線程線程名
}
}
public class App {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+" main");//列印目前線程(主線程)的線程名
RunnableDemo runnable = new RunnableDemo();//執行個體化自定義線程類
Thread thread=new Thread(runnable);//執行個體化Thread類
thread.start();//開啟一個線程
}
}
3.實作Callable接口
建立自定義線程類并實作Callable類–>實作call()方法–>執行個體化自定義線程類–>将自定義線程類的執行個體當作參數來執行個體化FutureTask類–>将FutureTask類的執行個體當作Thread類構造方法的參數來執行個體化Thread類–>調用Thread類執行個體的start()方法啟動線程。
import java.util.concurrent.Callable;
public class CallableDemo implements Callable<Integer> {
@Override//實作call()
public Integer call() throws Exception {
int i=100;
System.out.println(Thread.currentThread().getName()+" subthread");
return i;//傳回目前線程的執行結果
}
}
import java.util.concurrent.*;
public class App {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+" main");//列印目前線程(主線程)的線程名
CallableDemo callable=new CallableDemo();//執行個體化自定義線程類
FutureTask<Integer> result = new FutureTask<>(callable);
Thread thread = new Thread(result);
thread.start();
try {
System.out.println(result.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
三種方法對比
1.繼承Thread類為單繼承,而實作接口為多實作,是以實作接口方式在使用上比較靈活。
2.實作接口建立的線程可以放入線程池來管理,而繼承Thread類建立的線程不可以放入線程池。
3.Thread類底層實作了Runnable接口,底層也實作了run()方法,我們繼承Thread類需要重寫該方法;而實作Runnable接口需要實作run()方法。可以試一下,當我們繼承Thread類時,不重寫run()方法不會報錯,而實作Runnable接口,不實作run()會報錯。
4.繼承Thread類的線程類的執行個體直接調用start()方法去啟動該線程;
實作Runnable接口的線程類的執行個體需要作為參數來執行個體化Thread類,然後再由Thread類執行個體調用start()去啟動線程;
5.實作Callable接口的線程類的執行個體需要作為參數來執行個體化FutureTask類,然後再把FutureTask類執行個體作為參數來執行個體化Thread類,然後再由Thread類執行個體調用start()去啟動線程。
繼承Thread類和實作runnable接口建立的線程的操作都在run()方法中;
實作Callable接口建立的線程操作都在call()方法中,而且在實作接口時可以指定一個參數類型作為call()傳回值的類型,call()方法是一個有傳回值而且抛異常的方法。傳回值由FutureTask負責擷取。
總結:繼承Thread類這種方法使用起來比較友善,但底層還是實作Runnable接口;實作Runnable接口的方法啟動線程需要用Thread類的start()方法或放線上程池中啟動;實作Callable接口的方法也需要用Thread類的start()方法或放線上程池中啟動,而且需要先執行個體化FutureTask,但可以得到線程執行的傳回值。
多線程三種同步方式
-
synchronized
synchronized關鍵字修飾的方法,由于java的每個對象都有一個内置鎖,當用此關鍵字修飾方法時,内置鎖會保護整個方法。在調用該方法前,需要獲得内置鎖,否則就處于阻塞狀态。
synchronized關鍵字也可以修飾靜态方法,此時如果調用該靜态方法,将會鎖住整個類。
synchronized關鍵字修飾的語句塊。被該關鍵字修飾的語句塊會自動被加上内置鎖,進而實作同步。
注:同步是一種高開銷的操作,是以應該盡量減少同步的内容。通常沒有必要同步整個方法,使用synchronized代碼塊同步關鍵代碼即可。
-
volatile 隻保證可見性,不保證原子性
a.volatile關鍵字為域變量的通路提供了一種免鎖機制
b.使用volatile修飾域相當于告訴虛拟機該域可能會被其他線程更新
c.是以每次使用該域就要重新計算,而不是使用寄存器中的值
d.volatile不會提供任何原子操作,它也不能用來修飾final類型的變量
-
使用重入鎖實作線程同步
在JavaSE5.0中新增了一個java.util.concurrent包來支援同步。ReentrantLock類是可重入、互斥、實作了Lock接口的鎖, 它與使用synchronized方法和快具有相同的基本行為和語義,并且擴充了其能力。
ReenreantLock類的常用方法有:
ReentrantLock() : 建立一個ReentrantLock執行個體
lock() : 獲得鎖
unlock() : 釋放鎖
如果synchronized關鍵字能滿足使用者的需求,就用synchronized,因為它能簡化代碼 。如果需要更進階的功能,就用ReentrantLock類,此時要注意及時釋放鎖,否則會出現死鎖,通常在finally代碼釋放鎖。
線程間通信
-
wait與notify
wait():使一個線程處于等待狀态,并且釋放所持有的對象的lock。
sleep():使一個正在運作的線程處于睡眠狀态,是一個靜态方法,調用此方法要捕捉InterruptedException異常。
notify():喚醒一個處于等待狀态的線程,注意的是在調用此方法的時候,并不能确切的喚醒某一個等待狀态的線程,而是由JVM确定喚醒哪個線程,而且不是按優先級。
Allnotity():喚醒所有處入等待狀态的線程,注意并不是給所有喚醒線程一個對象的鎖,而是讓它們競争。
線程池
在開發過程中,合理地使用線程池能夠帶來3個好處。
- 降低資源消耗。通過重複利用已建立的線程降低線程建立和銷毀造成的消耗。
- 提高響應速度。當任務到達時,任務可以不需要等到線程建立就能立即執行。
- 提高線程的可管理性。線程是稀缺資源,如果無限制地建立,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一配置設定、調優和監控。但是,要做到合理利用線程池,必須對其實作原理了如指掌。 從圖中可以看出,當送出一個新任務到線程池時,線程池的處理流程如下。
Java多線程Java中線程的3種建立方式:多線程三種同步方式線程間通信線程池 - 線程池判斷核心線程池裡的線程是否都在執行任務。如果不是,則建立一個新的工作線程來執行任務。如果核心線程池裡的線程都在執行任務,則進入下個流程。
- 線程池判斷工作隊列是否已經滿。如果工作隊列沒有滿,則将新送出的任務存儲在這個工作隊列裡。如果工作隊列滿了,則進入下個流程。
- 線程池判斷線程池的線程是否都處于工作狀态。如果沒有,則建立一個新的工作線程來執行任務。如果已經滿了,則交給飽和政策來處理這個任務。
線程池的建立
我們可以通過ThreadPoolExecutor來建立一個線程池。
ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime,
unit, workQueue, threadFactory, handler);
建立一個線程池時需要輸入幾個參數:
-
corePoolSize(線程池的基本大小):當送出一個任務到線程池時,線程池會建立一個線程來執行任務,即使其他空閑的基本線程能夠執行新任務也會建立線程,等到需要執行的任務數大于線程池基本大小時就不再建立。
如果調用了線程池的prestartAllCoreThreads()方法,線程池會提前建立并啟動所有基本線程。
- maximumPoolSize(線程池最大數量):線程池允許建立的最大線程數。如果隊列滿了,并且已建立的線程數小于最大線程數,則線程池會再建立新的線程執行任務。值得注意的是,如果使用了無界的任務隊列這個參數就沒什麼效果。
- keepAliveTime(線程活動保持時間):線程池的工作線程空閑後,保持存活的時間。是以,如果任務很多,并且每個任務執行的時間比較短,可以調大時間,提高線程的使用率。
-
TimeUnit(線程活動保持時間的機關),可選的機關有:
天(DAYS)
小時(HOURS)
分鐘(MINUTES)
毫秒(MILLISECONDS)
微秒(MICROSECONDS,千分之一毫秒)
納秒(NANOSECONDS,千分之一微秒)
-
workQueue(任務隊列):用于儲存等待執行的任務的阻塞隊列。
可以選擇以下幾個阻塞隊列:
ArrayBlockingQueue:是一個基于數組結構的有界阻塞隊列,此隊列按FIFO(先進先出)原則對元素進行排序。
LinkedBlockingQueue:一個基于連結清單結構的阻塞隊列,此隊列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。靜态工廠方法Executors.newFixedThreadPool()使用了這個隊列。
SynchronousQueue:一個不存儲元素的阻塞隊列。每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處于阻塞狀态,吞吐量通常要高于LinkedBlockingQueue,靜态工廠方法Executors.newCachedThreadPool使用了這個隊列。
PriorityBlockingQueue:一個具有優先級的無限阻塞隊列。
- ThreadFactory:用于設定建立線程的工廠,可以通過線程工廠給每個建立出來的線程設定更有意義的名字。使用開源架構guava提供的ThreadFactoryBuilder可以快速給線程池裡的線程設定有意義的名字,代碼如下:
-
RejectedExecutionHandler(飽和政策):當隊列和線程池都滿了,說明線程池處于飽和狀态,那麼必須采取一種政策處理送出的新任務。這個政策預設情況下是AbortPolicy,表示無法處理新任務時抛出異常。在JDK 1.5中Java線程池架構提供了以下4種政策。
AbortPolicy:直接抛出異常。
CallerRunsPolicy:隻用調用者所線上程來運作任務。
DiscardOldestPolicy:丢棄隊列裡最近的一個任務,并執行目前任務。
DiscardPolicy:不處理,丢棄掉。
當然,也可以根據應用場景需要來實作RejectedExecutionHandler接口自定義政策。如記錄日志或持久化存儲不能處理的任務。
向線程池送出任務
可以使用兩個方法向線程池送出任務,分别為execute()和submit()方法。
- execute()方法用于送出不需要傳回值的任務,是以無法判斷任務是否被線程池執行成功。通過以下代碼可知execute()方法輸入的任務是一個Runnable類的執行個體。
threadsPool.execute(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
}
});
- submit()方法用于送出需要傳回值的任務。線程池會傳回一個future類型的對象,通過這個future對象可以判斷任務是否執行成功,并且可以通過future的get()方法來擷取傳回值,get()方法會阻塞目前線程直到任務完成,而使用get(long timeout,TimeUnit unit)方法則會阻塞目前線程一段時間後立即傳回,這時候有可能任務沒有執行完。
Future<Object> future = executor.submit(harReturnValuetask);
try {
Object s = future.get();
} catch (InterruptedException e) {
// 進行中斷異常
} catch (ExecutionException e) {
// 處理無法執行任務異常
} finally {
// 關閉線程池
executor.shutdown();
}
關閉線程池
可以通過調用線程池的shutdown或shutdownNow方法來關閉線程池。
它們的原理是周遊線程池中的工作線程,然後逐個調用線程的interrupt方法來中斷線程,是以無法響應中斷的任務可能永遠無法終止。
但是它們存在一定的差別:
- shutdownNow首先将線程池的狀态設定成STOP,然後嘗試停止所有的正在執行或暫停任務的線程,并傳回等待執行任務的清單,
- shutdown隻是将線程池的狀态設定成SHUTDOWN狀态,然後中斷所有沒有正在執行任務的線程。
隻要調用了這兩個關閉方法中的任意一個,isShutdown方法就會傳回true。當所有的任務都已關閉後,才表示線程池關閉成功,這時調用isTerminaed方法會傳回true。
至于應該調用哪一種方法來關閉線程池,應該由送出到線程池的任務特性決定:
- 通常調用shutdown方法來關閉線程池。
- 如果任務不一定要執行完,則可以調用shutdownNow方法。