掌握java多執行緒進階
一、執行緒的實現
繼承thread類重寫run()方法和實現Runnable介面實現run()方法
注意點:1、new一個執行緒例項時建議都要加個執行緒名方便監控和排查問題;
如new Thread("thread name")或thread.setName("thread name");
2、要處理執行緒的中斷異常(InterruptedException);
如if (Thread.interrupted()) {
//do someting
};
或
Try{}
catch (InterruptedException e) {
//do someting
}
二、ThreadLocal
ThreadLocal原始碼詳見文件ThreadLocal.java;
顧名思義它是local variable(執行緒區域性變數)。它的功用非常簡單,就是為每一個使用該變數的執行緒都提供一個變數值的副本,是每一個執行緒都可以獨立地改變自己的副本,而不會和其它執行緒的副本衝突。從執行緒的角度看,就好像每一個執行緒都完全擁有該變數。
注意:使用ThreadLocal,一般都是宣告在靜態變數中,如果不斷的建立ThreadLocal而且沒有呼叫其remove方法,將會導致記憶體洩露。如果是static的ThreadLocal,一般不需要呼叫remove。
三、執行緒的同步與鎖
1、synchronized
public synchronized void xxx() {
System.out.println(“處理業務”);
}
等同於
public void xxx() {
synchronized (this) {
System.out.println(“處理業務”);
}
}
一般建議使用獨立的物件鎖,不要直接用當前物件的鎖,如
public classSharedData{
private int a = 0;
private int b = 0;
public synchronized void setA(int a) { this.a = a; }
public synchronized void setB(int b) { this.b = b; }
}
若同步整個方法,則setA()的時候無法setB(),setB()時無法setA()。為了提高效能,可以使用不同物件的鎖:
public classSharedData{
private int a = 0;
private int b = 0;
private Object syncA= new Object();
private Object syncB= new Object();
public void setA(int a) {
synchronized(syncA) {
this.a = a;
}
}
public void setB(int b) {
synchronized(syncB) {
this.b = b;
}
}
}
如果將synchronized關鍵字標記在靜態方法上,由於靜態方法不可能訪問this例項,那麼,鎖住的是哪個物件呢?是當前類的Class物件,原因是每個物件的Class例項是唯一且不可變的。比如:
public synchronized static void sync() { ... }
事實上完全等同於下面的寫法:
public static void sync() {
synchronized(SharedData.class) {
...
}
}
Synchronized的同步方法和程式碼塊在多執行緒中各執行緒才會相互競爭物件鎖,非同步方法不會競爭物件鎖。
2、讀寫鎖ReadWriteLock
為了提高效能,Java5提供了讀寫鎖,在讀的地方使用讀鎖,在寫的地方使用寫鎖,靈活控制,在一定程度上提高了程式的執行效率。
Java中讀寫鎖有個介面java.util.concurrent.locks.ReadWriteLock,也有具體的實現ReentrantReadWriteLock,詳細的API可以檢視JavaAPI文件。
一般建議使用讀寫鎖ReadWriteLock。
四、執行緒的排程
1、wait/(notify/notifyAll())機制
通常,多執行緒之間需要協調工作。例如,瀏覽器的一個顯示圖片的執行緒displayThread想要執行顯示圖片的任務,必須等待下載執行緒 downloadThread將該圖片下載完畢。如果圖片還沒有下載完,displayThread可以暫停,當downloadThread完成了任務後,再通知displayThread“圖片準備完畢,可以顯示了”,這時,displayThread繼續執行。
以上邏輯簡單的說就是:如果條件不滿足,則等待。當條件滿足時,等待該條件的執行緒將被喚醒。在Java中,這個機制的實現依賴於wait/notify。等待機制與鎖機制是密切關聯的。例如:
synchronized(obj) {
while(!condition) {
obj.wait();
}
obj.doSomething();
}
當執行緒A獲得了obj鎖後,發現條件condition不滿足,無法繼續下一處理,於是執行緒A就wait()。
在另一執行緒B中,如果B更改了某些條件,使得執行緒A的condition條件滿足了,就可以喚醒執行緒A:
synchronized(obj) {
condition = true;
obj.notify();
}
需要注意:
1、呼叫obj的wait(), notify()方法前,必須獲得obj鎖,也就是必須寫在synchronized(obj) {...} 程式碼段內。
2、呼叫obj.wait()後,執行緒A就釋放了obj的鎖,否則執行緒B無法獲得obj鎖,也就無法在synchronized(obj) {...} 程式碼段內喚醒A。
3、當obj.wait()方法返回後,執行緒A需要再次獲得obj鎖,才能繼續執行。
4、如果A1,A2,A3都在obj.wait(),則B呼叫obj.notify()只能喚醒A1,A2,A3中的一個(具體哪一個由JVM決定)。
5、obj.notifyAll()則能全部喚醒A1,A2,A3,但是要繼續執行obj.wait()的下一條語句,必須獲得obj鎖,因此,A1,A2,A3只有一個有機會獲得鎖繼續執行,例如A1,其餘的需要等待A1釋放obj鎖之後才能繼續執行。
6、當B呼叫obj.notify/notifyAll的時候,B正持有obj鎖,因此,A1,A2,A3雖被喚醒,但是仍無法獲得obj鎖。直到B退出synchronized塊,釋放obj鎖後,A1,A2,A3中的一個才有機會獲得鎖繼續執行。
2、wait/sleep的區別
Thread還有一個sleep()靜態方法也能使執行緒暫停一段時間。sleep與wait的不同點是:sleep並不釋放鎖,並且sleep的暫停和wait暫停是不一樣的。obj.wait會使執行緒進入obj物件的等待集合中並等待喚醒。但是wait()和sleep()都可以通過interrupt()方法打斷執行緒的暫停狀態,從而使執行緒立刻丟擲InterruptedException。
如果執行緒A希望立即結束執行緒B,則可以對執行緒B對應的Thread例項呼叫interrupt方法。如果此刻執行緒B正在 wait/sleep/join,則執行緒B會立刻丟擲InterruptedException,在catch() {} 中直接return即可安全地結束執行緒。需要注意的是,InterruptedException是執行緒自己從內部丟擲的,並不是interrupt()方法丟擲的。對某一執行緒呼叫 interrupt()時,如果該執行緒正在執行普通的程式碼,那麼該執行緒根本就不會丟擲InterruptedException。但是,一旦該執行緒進入到 wait()/sleep()/join()後,就會立刻丟擲InterruptedException。
3、執行緒的讓步yield()和合並join()
執行緒的讓步含義就是使當前執行著執行緒讓出CPU資源,但是然給誰不知道,僅僅是讓出,執行緒狀態回到可執行狀態。
執行緒的讓步使用Thread.yield()方法,yield() 為靜態方法,功能是暫停當前正在執行的執行緒物件,並執行其他執行緒。
執行緒的合併的含義就是將幾個並行執行緒的執行緒合併為一個單執行緒執行,應用場景是當一個執行緒必須等待另一個執行緒執行完畢才能執行時可以使用join方法。
五、阻塞佇列
阻塞佇列是Java5執行緒新特徵中的內容,Java定義了阻塞佇列的介面java.util.concurrent.BlockingQueue,阻塞佇列的概念是,一個指定長度的佇列,如果佇列滿了,新增新元素的操作會被阻塞等待,直到有空位為止。同樣,當佇列為空時候,請求佇列元素的操作同樣會阻塞等待,直到有可用元素為止。
有了這樣的功能為多執行緒的排隊等候的業務場景開闢了便捷通道,非常有用。
java.util.concurrent.BlockingQueue繼承了java.util.Queue介面,可以參看API文件。
下面給出一個簡單應用的例子:
publicclassTest {
publicstaticvoidmain(String[] args) throws InterruptedException {
BlockingQueue bqueue = new ArrayBlockingQueue(20);
for(int i = 0; i < 30; i++) {
//將指定元素新增到此佇列中,如果沒有可用空間,將一直等待(如果有必要)。
bqueue.put(i);
System.out.println("向阻塞佇列中添加了元素:" + i);
}
System.out.println("程式到此執行結束,即將退出----");
}
}
可以看出,輸出到元素19時候,就一直處於等待狀態,因為佇列滿了,程式阻塞了。
另外,阻塞佇列還有更多實現類,用來滿足各種複雜的需求:ArrayBlockingQueue, DelayQueue, LinkedBlockingQueue, PriorityBlockingQueue, SynchronousQueue ,具體的API差別也很小。
六、執行緒池和有返回值的執行緒
Executors:ExecutorService和Future
1、執行緒池
//建立一個可重用固定執行緒數的執行緒池
ExecutorService pool =Executors.newFixedThreadPool(2);
//建立一個使用單個 worker 執行緒的 Executor,以無界佇列方式來執行該執行緒。
ExecutorService pool =Executors.newSingleThreadExecutor();
//建立一個可根據需要建立新執行緒的執行緒池,但是在以前構造的執行緒可用時將重用它們。
ExecutorService pool = Executors.newCachedThreadPool();
//建立一個執行緒池,它可安排在給定延遲後執行命令或者定期地執行。
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
//建立一個單執行緒執行程式,它可安排在給定延遲後執行命令或者定期地執行。
ScheduledExecutorService pool = Executors.newSingleThreadScheduledExecutor();
//建立等待佇列
BlockingQueue bqueue = new ArrayBlockingQueue(20);
//建立一個單執行緒執行程式,它可安排在給定延遲後執行命令或者定期地執行。
ThreadPoolExecutor pool = new ThreadPoolExecutor(2,3,2,TimeUnit.MILLISECONDS,bqueue);
2、有返回值的執行緒(Future)
Future的主要方法:
boolean cancel (boolean mayInterruptIfRunning) 取消任務的執行。引數指定是否立即中斷任務執行,或者等等任務結束
boolean isCancelled () 任務是否已經取消,任務正常完成前將其取消,則返回 true
boolean isDone () 任務是否已經完成。需要注意的是如果任務正常終止、異常或取消,都將返回true
V get () throws InterruptedException, ExecutionException 等待任務執行結束,然後獲得V型別的結果。InterruptedException 執行緒被中斷異常, ExecutionException任務執行異常,如果任務被取消,還會丟擲CancellationException
V get (long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException 同上面的get功能一樣,多了設定超時時間。引數timeout指定超時時間,uint指定時間的單位,在列舉類TimeUnit中有相關的定義。如果計算超時,將丟擲TimeoutException