java多線程
[TOC]
創建線程
class MyRunnable implements Runnable{
public void run(){
//線程執行代碼
}
}
Runnable r = new MyRunnable();
//創建線程并啟動
Thread t = new Thread(r);
t.start();
直接調用Thread類或Runnable類的run方法并不會 創建線程,只會在本線程內執行
中斷線程
- 當線程的run方法執行完畢后或者出現了沒有捕獲的異常時,線程將終止。
- 當對一個線程調用interrupt方法時,線程中的中斷標志將被置位,這是每個線程都有的boolean標志,每個線程都應不時檢查這個標志,已判斷線程是否中斷。
- 使用Thread.currentThread()方法獲取當前線程,Thread.currentThread().isInterrupted方法返回當前線程被置位的狀態。但是當線程被阻塞(sleep或wait)時將無法檢測中斷狀態,當對阻塞狀態的線程調用interrupt方法時線程會拋出interrupted Exception異常。
- interrupted測試當前線程是否中斷,執行方法會將線程的中斷狀態置為false。
線程狀態
- New(新創建)
- Runnable(可運行)
- Blocked(被阻塞)
- Waiting(等待)
- Timed Waiting(計時等待)
- Terminated(被終止)
-
新創建線程
使用new操作剛剛創建的線程,此時線程還未開始運行。****
-
可運行線程
一旦調用了Thread的start方法線程就處于runable狀態,一個可運行的線程可能正在運行也可能沒有運行,取決于操作系統的上下文切換。一旦一個線程開始運行,可能隨時被中斷,目的是為了讓其他線程獲得運行機會,操作系統搶占式調度系統給每一個可運行線程一個時間片來執行任務,時間片用完時,系統剝奪線程的運行權,并給其他線程運行機會。
-
被阻塞或等待的線程
當線程處于被阻塞或等待狀態時,它暫時不活動,不運行任何代碼切消耗資源,直到線程調度器重新激活它。
- 當一個線程試圖獲取一個內部對象鎖,而該鎖被其他線程持有,則該線程進入阻塞狀態。當其他線程釋放鎖,并且調度去允許本線程持有時,該線程恢復為非阻塞狀態。
- 當線程等待另一個線程通知調度器一個條件時,它自己進入等待狀態。
- Thread.sleep、Object.wait、Thread.join、Lock.tryLock、Condition.await方法將線程進入等待狀態。
當一個線程被阻塞或等待時,另一個線程被調度為可運行狀態,當一個線程重新被激活時,調度器檢查是否比當前運行線程具有更高優先級,如果優先級更高則從當前運行線程中選一個剝奪當前線程優先級,選擇一個新的線程運行。
-
線程有倆種原因被終止:
- run方法正常退出而自然死亡。
- 因為沒有捕獲的異常終止運行而意外死亡。
線程屬性
線程優先級
- 每一個線程有一個優先級,默認情況,一個線程繼承它的父線程的優先級,可以用setPriority方法提高或降低任何一個線程的優先級。可以將優先級設置為在MIN_PRIORITY(1) 至MAX_PRIORITY(10)之間,NORM_PRIORITY=5。
- 線程優先級高度依賴于操作系統調度,java優先級被映射到操作系統的優先級。優先個數也許更多,也許更少。在linux上線程優先級被忽略,所有線程具有相同優先級。
守護線程
可以通過調用t.setDaemon(true);將線程轉換為守護線程。守護線程的唯一用途就是為其他線程提供服務。當只剩下守護線城時虛擬機就退出了,因為沒必要繼續運行程序了。
未捕獲異常處理器
- 線程的run方法不能拋出任何被檢測的異常,不被檢測的異常將導致線程終止。但是不需要任何catch子句來處理可以被傳播的異常,相反在線程死亡前異常被傳遞到一個用于未捕獲異常的處理器。該處理器必須屬于一個實現Thread.UncaughtExceptionHandler接口的類。這個接口只有一個方法:
void uncaughtException(Thread t,Throwable e)
可以用setUncaughtExceptionHandler方法為任何線程安裝一個處理器。也可以用Thread類的靜態方法setDefaultUncaughtExceptionHandler為所有線程安裝一個默認處理器,替換處理器可以使用日志API發送未捕獲異常的報告到日志文件。
-
默認處理器為空,但是如果不為獨立的線程安裝處理器,此時的處理器就是該線程的ThreadGroup對象。
線程組(ThreadGroup)是一個可以統一管理的線程集合。默認情況所有線程屬于相同的線程組,但是也可以建立其他的組,現在引入了更好的特性用于線程集合的操作,不建議在自己的程序中使用線程組。
-
ThreadGroup類實現了Thread.UncaughtExceptionHandler接口,它的uncaughtException方法做如下操作:
- 如果該線程組有父線程組,那么父線程組的uncaughtException方法被調用。
- 否則如果Thread.getDefaultExceptionHandler方法返回一個非空的處理器,則調用該處理器。
- 否則如果Throwable是ThreadDeath的一個實例,什么都不做。
- 否則,線程的名字以及Throwable的棧蹤跡被輸出。
同步
鎖對象
如果使用鎖就不能使用帶資源的try語句。
- 有兩種機制防止代碼塊受并發訪問的干擾,java提供一個synchronized關鍵字達到這個目的,jdk1.5引入了ReentrantLock類,用于顯式鎖定。
//ReentrantLock顯式鎖定
myLock.lock();
try{
}finally{
myLock.unlock();
}
- 當一個對象有自己的ReentrantLock對象時,如果倆個線程試圖訪問同一個對象,那么將觸發鎖機制,如果倆個線程訪問倆個不同的對象則都不會發生阻塞。
可重入鎖
- synchronized和ReentrantLock都是可重入鎖,比如一個方法是synchronized,遞歸調用自己,第一個獲得鎖后第二次依然可以獲得鎖,在鎖的內部對象有一個計數器用來跟蹤鎖的數量,unlock或synchronized方法執行完畢后對計數器-1。
public class Widget {
public synchronized void doSomething() {
...
}
}
public class LoggingWidget extends Widget {
public synchronized void doSomething() {
System.out.println(toString() + ": calling doSomething");
super.doSomething();//若內置鎖是不可重入的,則發生死鎖
}
}
//當執行子類方法時,先獲取了一次Widget的鎖,在執行super的時候,又要獲取一次如果是,如果不可重入線程就會死亡。
- 為每個鎖關聯一個獲取計數器和一個所有者線程,當計數值為0的時候,這個所就沒有被任何線程只有.當線程請求一個未被持有的鎖時,JVM將記下鎖的持有者,并且將獲取計數值置為1,如果同一個線程再次獲取這個鎖,技術值將遞增,退出一次同步代碼塊,計算值遞減,當計數值為0時,這個鎖就被釋放.
條件對象
-
一個鎖對象可以有一個或多個相關的條件對象,可以使用newCondition方法獲得一個條件對象。
例如:private Condition sufficientFunds;
-
如果條件對象發現條件不滿足時,它調用await方法,當前線程現在被阻塞了,并放棄了鎖。等待或者鎖的線程和調用await方法的線程存在本質上的不同,一個線程調用await方式,它進入該條件的等待集。當鎖可用時,該線程不能馬上解除阻塞。相反,它處于阻塞狀態,直到另一個線程調用同一條件上的signalAll方法為止。
當另一個線程轉賬時,它應該調用sufficientFunds.signalAll();
這一調用重新激活因為這一條件而等待的所有線程,當這些線程從等待集中移出時,它們再次成為可運行的,調度器將再次激活它們。同時它們試圖重新進入該對象,一旦鎖成為可用的,它們中的某個將從await調用返回,獲得該鎖并從被阻塞的地方繼續執行。此時應該重新檢測條件對象,由于無法確保該條件被滿足,signalAll方法僅僅是通知正在等待的線程:此時有可能已經滿足條件,值得再去檢測該條件。
至關重要的是最終需要某個線程調用signalAll方法,當一個線程調用await時,它沒有辦法重新激活自身,只能靠其他線程來重新激活,否則將陷入死鎖狀態中。
另一個方法signal則是隨機解除等待集中某個線程的阻塞狀態,這比解除所有線程阻塞更加有效,但是隨機選擇的線程如果發現仍然不能運行,那么它再次被阻塞,如果沒有其他線程再次調用signal,系統就死鎖了。
while(account[from] < amount){
condition.await();
}
synchronized 關鍵字
- 鎖用來保護代碼片段,任何時刻只能有一個線程執行被保護的代碼。
- 鎖可以管理試圖進入被保護代碼段的線程。
- 鎖可以擁有一個或多個相關的條件對象
- 每個條件對象管理那些已經進入被保護的代碼段但還不能運行的線程。
- java中每一個對象都有一個內部鎖,如果一個方法用synchronized關鍵字聲明,那么對象鎖就保護整個方法。
- 內部對象鎖只有一個相關條件,wait方法添加一個線程到等待集中,notifyALL/notify方法解除等待線程的阻塞狀態。等價于await與signalAll。
class Bank{
private double[] accounts;
public synchronized void transfer(int from,int to,int amount) throws InterruptedException{
while(accounts[from] < amount){//條件
wait();//如果條件不滿足會將當前線程添加到等待集中
accounts[from] -= amount;
account[to] += amount;
notifyAll();//轉賬完成后通知其他等待中的線程可以檢查條件了
}
}
}
- 如果Bank類有一個靜態同步方法,那么當該方法被調用時,Bank.class對象會被鎖住,因此沒有其他線程可以調用同一個類的這個或任何其他的同步靜態方法。
內部鎖和條件的局限性
- 不能中斷一個正在試圖獲得鎖的線程。
- 試圖獲得鎖時不能設定超時。
- 每個鎖僅有單一的條件,可能是不夠的。
- 最好既不適用lock/Condition也不適用synchronized,許多情況下可以用java.util.concurrent包中的一種機制。
監視器概念
- 監視器是只包含私有域的類。
- 每個監視器類的對象有一個相關鎖。
- 使用該鎖對所有的方法進行加鎖。
- 該鎖可以有任意多個相關條件。
- java中每一個對象有一個內部鎖和內部的條件。
- 如果一個方法用synchronized聲明,表現的就像是一個監視器方法,通過調用wait/notifyAll/notify來訪問條件變量。
Volatile域
Volatile關鍵字為實例域的同步訪問提供了一種免鎖機制。如果聲明一個域為volatile,那么編譯器和虛擬機就知道該域是可能被另一個線程并發更新的。
volatile變量不能提供原子性,例如:public void flipDone(){ done = !done },不能確保翻轉域中的值。
線程局部變量
使用ThreadLocal輔助類為各個線程提供各自的實例。
鎖測試與超時
- tryLock方法嘗試獲取鎖,成功后返回true,否則返回false,而且線程可以立刻離開,同時可以為tryLock設置超時參數。
- 在等待一個條件時也可以提供一個超時: condition.await(100,TimeUnit.MILLISECONDS),如果一個線程被另一個線程通過調用signalAll或signal激活,或者已超時,或者線程被中斷,await方法將返回。
讀寫鎖
- ReentrantLock 通用鎖,讀寫都阻塞。
- ReentrantReadWriteLock 讀寫鎖
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();
阻塞隊列
- 當試圖向隊列添加元素而隊列已滿,或是香蔥隊列移出元素而隊列為空的時候,阻塞隊列導致線程阻塞。
- 工作者線程可以周期性地將結果存儲在阻塞隊列中,其他工作者線程移出中間結果處理,隊列會自動地平衡負載,如果第一個線程集運行的比第二個慢,第二個線程集在等待結果時會阻塞,如果第一個線程集運行的快,他將等待第二個隊列集趕上來。
阻塞隊列方法
方法 | 正常動作 | 特殊情況動作 |
---|---|---|
add | 添加元素 | 隊列滿拋出IllegalStateException |
element | 返回頭元素 | 隊列空拋出NoSuchElementException |
offer | 添加元素返回true | 隊列滿了返回false |
peed | 返回隊列頭元素 | 隊列空返回null |
poll | 移出并返回頭元素 | 隊列空返回null |
put | 添加一個元素 | 隊列滿則阻塞 |
remove | 移出并返回頭元素 | 隊列空拋出NoSuchElementException |
take | 移出并返回頭元素 | 如果隊列空則阻塞 |
java.util.concurrent包提供了阻塞隊列的幾個變種:
- ArrayBlockingQueue(int capacity,boolean fair) 帶有容量和公平性設置的阻塞隊列,用循環數組實現。
- LinkedBlockingQueue 單項鏈表隊列
- LinkedBlockingDeque,容量沒有上邊界,但可指定最大容量,并有可選參數指定是否需要公平性,若設置了公平性,則等待時間最長的線程會有限處理,公平性會降低性能。
- PriorityBlockingQueue<E>,優先級隊列,而不是先進先出,元素會按照優先級順序移出,該隊列沒有容量上限,但是如果隊列是空的取元素操作會阻塞,堆實現。
- LinkedTransferQueue類實現了TransferQueue接口,允許生產者線程等待,直到消費者準備就緒可以接受一個元素,如果生產者調用qtransfer(item); 這個調用會阻塞,直到另一個線程將元素刪除。
- delayQueue 包含delayed元素無邊界有阻塞時間隊列,只有那些延遲已經超過時間的元素可以從隊列中移出。
- blockingQueue<E> 普通阻塞隊列
- BlockingDeque<E> 雙向阻塞隊列
線程安全的集合
java.util.concurrent包提供了映射表、有序集合和隊列的高效實現:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet和ConcurrentLinkedQueue。
與大多數集合不同,size方法不必在常量時間內操作。確定這樣的集合當前的大小通常需要遍歷。
集合返回弱一致性的迭代器。這意味著迭代器不一定能反映出被構造之后的所有的修改,但是它們不會將一個值返回倆次。
普通集合如果在迭代器構造之后發生改變,java.util包中的迭代器將拋出一個ConcurrentModificationException。
- ConcurrentLinkedQueue<E> 線程安全無邊界非阻塞隊列。
- ConcurrentSkipListSet<E> 線程安全有序集合。
- ConcurrentHashMap
- ConcurrentSkipListMap 線程安全的hash table。具有類似redis setnx這種方法。
Callable與Future
Runnable:沒有參數沒有返回值。
Callable:有參數有返回值,只有一個方法call。
Future:保存異步計算的結果,可以啟動一個計算,將Future對象交給某個線程,然后忘掉它,Future對象的持有者在結果計算好之后就可以獲得它。
如果運行該計算的線程被中斷,get與get帶有超時的方法都將拋出InterruptedException。
Future的方法:
方法名 | 描述 |
---|---|
V get() | 調用被阻塞直到返回結果 |
V get(long timeouot,TimeUnit unit) | 阻塞調用直到超時,超時拋出InterruptedException |
boolean isDone() | 是否完成 |
boolean isCancelled() | 是否取消 |
void cancel(boolean mayInterrupt) | 取消,如果還沒有開始將不再開始。 |
FutureTask包裝器可以Callable轉換成Future和Runnable,它同時實現二者的接口。
執行器
構建一個新的線程是有一定代價的,如果程序中創建大量臨時線程,應該使用線程池,一個線程池包含許多準備運行的空閑線程,將Runnable對象交給線程池,就會有一個線程調用run方法。當run方法退出時,線程不會死亡,二十在池中準備為下一個請求提供服務。
執行器類有許多靜態工廠方法用來構建線程池
- newCachedThreadPool:必要時創建新線程;空閑線程只會被保留60秒。對于每個任務,如果有空閑線程可用立即執行任務,如果沒有則創建一個線程。
- newFixedThreadPool:該池包含固定數量的線程;空閑線程會一直被保留。如果任務數超過線程數,將任務放到隊列中。
- newSingleThreadExecutor:只有一個線程的“池”,該線程順序執行每一個提交的任務。
- newScheduledThreadPool:用于預定執行而構建的固定線程池,替代java.util.Timer
- newSingleThreadScheduledExecutor:用于預定執行而構建的單線程“池”
線程池
使用submit方法將Runnable對象或Callable對象提交給線程池。
添加任務至線程池
Future<?> submit(Runnable task),返回結果Future<?>可以調用isDone、cancel、isCancelled方法,但是get方法在完成的時候只是簡單地返回null。
Furure<T> submit(Runnable task,T result) get方法在完成的時候返回result對象。
Future<T> submit(Callable<T> task) 返回的Future對象將在計算結果準備好的時候得到它。
線程池會在方便的時候今早執行提交的任務,submit方法會返回一個Future對象,可用來查詢該任務狀態。
關閉線程池
當用完一個線程池的時候調用shutdown,該方法啟動該池的關閉序列,被關閉的線程池不在接受新的任務,所有任務都完成后,線程池中的線程死亡。
或者調用shutdownNow,取消尚未開始的任務,并中斷正在運行的線程。
獲取線程池線程數量
必須將pool對象強制轉換為ThreadPoolExecutor類對象,調用getLargestPoolSize方法。
預定執行
ScheduledExecutorService接口具有為預定執行或重復執行任務而設計的方法,可以預定Runnablehuocallable在初始的延遲之后只運行一次,也可以預定一個Runnable對象周期性地運行。
控制任務組
invokeAny方法提交所有對象到一個Callable對象的集合中,并返回某個已經完成了的任務的結果。無法找到返回的究竟是哪個任務的結果。當其中一個任務完成時,運行流程就停止了。
invokeAll方法提交所有對象到一個Callable對象的集合中,并返回一個Future對象的列表,包含所有任務的返回結果。