7 多線程
阿新 • • 發佈:2018-02-10
資源釋放 單線程 interrupt 線程阻塞 illegal his 等等 異常 mit 多線程
1.相比於多進程,多線程的優勢有:
(1)進程之間不能共享數據,線程可以;
(2)系統創建進程需要為該進程重新分配系統資源,故創建線程代價比較小;
2.創建線程和啟動(3種)
(1)繼承Thread類,重寫run()方法(用匿名類) Thread thread = new Thread(){ public void run(){ }; } t.start(); (2) 實現Runnable接口,重寫run方法 兩種寫法: 匿名: Runnable task = new Runnable(){ public void run() { } }; Thread t = new Thread( task ); t.start(); Lambda表達式 Runnable task = () -> { System.out.println("HelloWorld"); }; Thread t = new Thread( task ); (3)通過Callable和Future創建線程 Callable的特點: 1.可以有返回值 2.接口的方法拋出Exception,如果在任務主體裏面有異常,可以不處理,系統自動處理 使用Callable的步驟: 1.創建Callable的實例 Callable<String> call = () -> { return "xxx"; }; 2.包裝成一個FutureTask(實現了Future和Runable接口) // FutureTask的泛型參數,必須和Callable的泛型參數一樣,要求相同類型、兼容類型 FutureTask<String> task = new FutureTask<>( call ); 3.把FutureTask作為任務,傳遞給Thread的構造器 Thread t = new Thread( task ); 4.調用線程的start方法 t.start();
3.線程的生命周期
(1) 、新建狀態
用new關鍵字和Thread類或其子類建立一個線程對象後,該線程對象就處於新生狀態。通過調用start方法進入就緒狀態(runnable)。
註意:不能對已經啟動的線程再次調用start()方法,否則會出現Java.lang.IllegalThreadStateException異常。
(2)、就緒狀態
處於就緒狀態的線程已經具備了運行條件,但還沒有分配到CPU,處於線程就緒隊列(盡管是采用隊列形式,事實上,把它稱為可運行池而不是可運行隊列。因為cpu的調度不一定是按照先進先出的順序來調度的),等待系統為其分配CPU。等待狀態並不是執行狀態,當系統選定一個等待執行的Thread對象後,它就會從等待執行狀態進入執行狀態,系統挑選的動作稱之為“cpu調度”。一旦獲得CPU,線程就進入運行狀態並自動調用自己的run方法。 提示:如果希望子線程調用start()方法後立即執行,可以使用Thread.sleep()方式使主線程睡眠一會兒,轉去執行子線程。
(3)、運行狀態
處於運行狀態的線程最為復雜,它可以變為阻塞狀態、就緒狀態和死亡狀態。 處於就緒狀態的線程,如果獲得了cpu的調度,就會從就緒狀態變為運行狀態,執行run()方法中的任務。如果該線程失去了cpu資源,就會又從運行狀態變為就緒狀態。重新等待系統分配資源。也可以對在運行狀態的線程調用yield()方法,它就會讓出cpu資源,再次變為就緒狀態。 註: 當發生如下情況時,線程會從運行狀態變為阻塞狀態: ①、線程調用sleep方法主動放棄所占用的系統資源 ②、線程調用一個阻塞式IO方法,在該方法返回之前,該線程被阻塞 ③、線程試圖獲得一個同步監視器,但更改同步監視器正被其他線程所持有 ④、線程在等待某個通知(notify) ⑤、程序調用了線程的suspend方法將線程掛起。不過該方法容易導致死鎖,所以程序應該盡量避免使用該方法。 當線程的run()方法執行完,或者被強制性地終止,例如出現異常,或者調用了stop()、desyory()方法等等,就會從運行狀態轉變為死亡狀態。
(4)、阻塞狀態
處於運行狀態的線程在某些情況下,如執行了sleep(睡眠)方法,或等待I/O設備等資源,將讓出CPU並暫時停止自己的運行,進入阻塞狀態。
在阻塞狀態的線程不能進入就緒隊列。只有當引起阻塞的原因消除時,如睡眠時間已到,或等待的I/O設備空閑下來,線程便轉入就緒狀態,重新到就緒隊列中排隊等待,被系統選中後從原來停止的位置開始繼續運行。有三種方法可以暫停Threads執行:
(5)、死亡狀態
當線程的run()方法執行完,或者被強制性地終止,就認為它死去。這個線程對象也許是活的,但是,它已經不是一個單獨執行的線程。線程一旦死亡,就不能復生。 如果在一個死去的線程上調用start()方法,會拋出java.lang.IllegalThreadStateException異常。
**4.線程管理**
(1)線程睡眠--sleep
Thread.sleep(1000);
(2)線程讓步--yield
Thread.yield();
設置優先級:thread.setPriority(1);
註:關於sleep()方法和yield()方的區別如下:
①、sleep方法暫停當前線程後,會進入阻塞狀態,只有當睡眠時間到了,才會轉入就緒狀態。而yield方法調用後 ,是直接進入就緒狀態,所以有可能剛進入就緒狀態,又被調度到運行狀態。
②、sleep方法聲明拋出了InterruptedException,所以調用sleep方法的時候要捕獲該異常,或者顯示聲明拋出該異常。而yield方法則沒有聲明拋出任務異常。
③、sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法來控制並發線程的執行。
(3)線程合並 --join (thread.join() )
將幾個並行線程的線程合並為一個單線程執行,應用場景是當一個線程必須等待另一個線程執行完畢才能執行時
有三個重載方法:
void join() 當前線程等該加入該線程後面,等待該線程終止。
void join(long millis) 當前線程等待該線程終止的時間最長為 millis 毫秒。 如果在millis時間內,該線程沒有執行完,那麽當前線程進入就緒狀態,重新等待cpu調度
void join(long millis,int nanos) 等待該線程終止的時間最長為 millis 毫秒 + nanos 納秒。如果在millis時間內,該線程沒有執行完,那麽當前線程進入就緒狀態,重新等待cpu調度
(4)設置線程的優先級(thread.setPriority(1) )
優先級高的線程獲取CPU資源的概率較大,優先級低的也並非沒機會執行。
每個線程默認的優先級都與創建它的父線程具有相同的優先級,在默認情況下,main線程具有普通優先級。
註:Thread類提供了setPriority(int newPriority)和getPriority()方法來設置和返回一個指定線程的優先級,其中setPriority方法的參數是一個整數,範圍是1~·0之間,也可以使用Thread類提供的三個靜態常量:
MAX_PRIORITY =10
MIN_PRIORITY =1
NORM_PRIORITY =5
class MyThread extends Thread {
public MyThread(String name,int pro) {
super(name);//設置線程的名稱
setPriority(pro);//設置線程的優先級
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(this.getName() + "線程第" + i + "次執行!");
}
}
}
public class Test1 {
public static void main(String[] args) throws InterruptedException {
new MyThread("高級", 10).start();
new MyThread("低級", 1).start();
}
}
(5)後臺(守護)進程 --thread.setDaemon(true);
把線程對象設置為後臺線程,此方法必須在start()之前調用。
後臺線程主要用於維護、監控任務。
所有的非後臺線程結束後,表示程序要結束。此時如果還有後臺線程正在執行,那麽所有的後臺線程直接結束、中斷。
(6)正確結束線程
廢棄方法 Thread.stop(); Thread.suspend(); Thread.resume();
①正常執行完run方法,然後結束掉;
②控制循環條件和判斷條件的標識符來結束掉線程。
5.線程同步(同步鎖)
多線程並發時,多個線程同時操作一個可共享的資源時,將會導致數據不準確。
(1)同步方法
既有synchronized關鍵字修飾的方法。由於java每一個對象都有一個內置鎖,當用此關鍵字修飾方法時,
內置鎖會保護整個方法。在調用該方法前,需要獲得內置鎖,否則處於阻塞狀態。
註:synchronized關鍵字也可以修飾靜態方法,此時如果調用該靜態方法,將會鎖住整個類。
(2)同步代碼塊
既有synchronized關鍵字修飾的語句塊。被該關鍵字修飾的語句塊會自動被加上內置鎖,從而實現同步。
註:同步是一種高開銷的操作,因此應盡量減少同步的內容。
(3)使用重入鎖(Lock)實現線程同步
ReentrantLock類是可重入、互斥、實現了Lock接口的鎖。
ReentrantLock() : 創建一個ReentrantLock實例
lock() : 獲得鎖
unlock() : 釋放鎖
class Bank {
private int account = 100;
//需要聲明這個鎖
private Lock lock = new ReentrantLock();
public int getAccount() {
return account;
}
//這裏不再需要synchronized
public void save(int money) {
lock.lock();
try{
account += money;
}finally{
lock.unlock();
}
}
}
6.線程通信(生產者和消費者)
(1)、借助於Object類的wait()、notify()和notifyAll()實現通信
線程執行wait()後,就放棄了運行資格,處於凍結狀態;
線程運行時,內存中會建立一個線程池,凍結狀態的線程都存在於線程池中,notify()執行時喚醒的也是線程池中的線程,線程池中有多個線程時喚醒第一個被凍結的線程。
notifyall(), 喚醒線程池中所有線程。
註:
① wait(), notify(),notifyall()都用在同步裏面,因為這3個函數是對持有鎖的線程進行操作,而只有同步才有鎖,所以要使用在同步中;
② wait(),notify(),notifyall(), 在使用時必須標識它們所操作的線程持有的鎖,因為等待和喚醒必須是同一鎖下的線程;而鎖可以是任意對象,所以這3個方法都是Object類中的方法。
有一個字段flag來判斷生產品的數量是否為空,消費品是否為空。true表示產品有,通知消費者消費;false就是沒有商品
true:生產者等待消費,消費者通知,並設置為false
false:消費者等待生產,生產者通知,並設置為true
class Resource{
private String name;
private int count=1;
private boolean flag=false;
public synchronized void set(String name){
while(flag)
try{wait();}catch(Exception e){}
this.name=name+"---"+count++;
System.out.println(Thread.currentThread().getName()+"...生產者..."+this.name);
flag=true;
this.notifyAll();
}
public synchronized void out(){
while(!flag)
try{wait();}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"...消費者..."+this.name);
flag=false;
this.notifyAll();
}
}
public class ProducerConsumerDemo{
public static void main(String[] args){
Resource r=new Resource();
Producer pro=new Producer(r);
Consumer con=new Consumer(r);
Thread t1=new Thread(pro);
Thread t2=new Thread(con);
Thread t3=new Thread(pro);
Thread t4=new Thread(con);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
(2)、使用Condition控制線程通信
jdk1.5中,提供了多線程的升級解決方案為:
①將同步synchronized替換為顯式的Lock操作;
②將Object類中的wait(), notify(),notifyAll()替換成了Condition對象,該對象可以通過Lock鎖對象獲取;
③一個Lock對象上可以綁定多個Condition對象,這樣實現了本方線程只喚醒對方線程,而jdk1.5之前,一個同步只能有一個鎖,不同的同步只能用鎖來區分,且鎖嵌套時容易死鎖。
class Resource{
private String name;
private int count=1;
private boolean flag=false;
private Lock lock = new ReentrantLock();/Lock是一個接口,ReentrantLock是該接口的一個直接子類。/
private Condition condition_pro=lock.newCondition(); /創建代表生產者方面的Condition對象/
private Condition condition_con=lock.newCondition(); /使用同一個鎖,創建代表消費者方面的Condition對象/
public void set(String name){
lock.lock();//鎖住此語句與lock.unlock()之間的代碼
try{
while(flag)
condition_pro.await(); //生產者線程在conndition_pro對象上等待
this.name=name+"---"+count++;
System.out.println(Thread.currentThread().getName()+"...生產者..."+this.name);
flag=true;
condition_con.signalAll();
}
finally{
lock.unlock(); //unlock()要放在finally塊中。
}
}
public void out(){
lock.lock(); //鎖住此語句與lock.unlock()之間的代碼
try{
while(!flag)
condition_con.await(); //消費者線程在conndition_con對象上等待
System.out.println(Thread.currentThread().getName()+"...消費者..."+this.name);
flag=false;
condition_pro.signqlAll(); /*喚醒所有在condition_pro對象下等待的線程,也就是喚醒所有生產者線程*/
}
finally{
lock.unlock();
}
}
}
**(3)、使用阻塞隊列(BlockingQueue)控制線程通信**
BlockingQueue是一個接口,也是Queue的子接口。BlockingQueue具有一個特征:當生產者線程試圖向BlockingQueue中放入元素時,如果該隊列已滿,則線程被阻塞;但消費者線程試圖從BlockingQueue中取出元素時,如果隊列已空,則該線程阻塞。程序的兩個線程通過交替向BlockingQueue中放入元素、取出元素,即可很好地控制線程的通信。
BlockingQueue提供如下兩個支持阻塞的方法:
①put(E e):嘗試把Eu元素放如BlockingQueue中,如果該隊列的元素已滿,則阻塞該線程。
②take():嘗試從BlockingQueue的頭部取出元素,如果該隊列的元素已空,則阻塞該線程。
BlockingQueue繼承了Queue接口,當然也可以使用Queue接口中的方法,這些方法歸納起來可以分為如下三組:
①在隊列尾部插入元素,包括add(E e)、offer(E e)、put(E e)方法,當該隊列已滿時,這三個方法分別會拋出異常、返回false、阻塞隊列。
②在隊列頭部刪除並返回刪除的元素。包括remove()、poll()、和take()方法,當該隊列已空時,這三個方法分別會拋出異常、返回false、阻塞隊列。
③在隊列頭部取出但不刪除元素。包括element()和peek()方法,當隊列已空時,這兩個方法分別拋出異常、返回false。
public class BlockingQueueTest{ public static void main(String[] args)throws Exception{ //創建一個容量為1的BlockingQueue
BlockingQueue<String> b=new ArrayBlockingQueue<>(1);
//啟動3個生產者線程
new Producer(b).start();
new Producer(b).start();
new Producer(b).start();
//啟動一個消費者線程
new Consumer(b).start();
}
} class Producer extends Thread{ private BlockingQueue<String> b;
public Producer(BlockingQueue<String> b){
this.b=b;
}
public synchronized void run(){
String [] str=new String[]{
"java",
"struts",
"Spring"
};
for(int i=0;i<9999999;i++){
System.out.println(getName()+"生產者準備生產集合元素!");
try{
b.put(str[i%3]);
sleep(1000);
//嘗試放入元素,如果隊列已滿,則線程被阻塞
}catch(Exception e){System.out.println(e);}
System.out.println(getName()+"生產完成:"+b);
}
}
} class Consumer extends Thread{ private BlockingQueue<String> b; public Consumer(BlockingQueue<String> b){ this.b=b; } public synchronized void run(){
while(true){
System.out.println(getName()+"消費者準備消費集合元素!");
try{
sleep(1000);
//嘗試取出元素,如果隊列已空,則線程被阻塞
b.take();
}catch(Exception e){System.out.println(e);}
System.out.println(getName()+"消費完:"+b);
}
}
7.線程池
線程池的核心:
①.創建一堆的線程放到內存裏面備用。每個線程的run方法都不會結束。 在沒有任務的時候,wait狀態。
②如果有計算任務到達,就從線程池裏面獲取一個線程對象出來,並且把任務設置給線程對象。
設置完以後,發送notify通知線程要執行任務。
③任務執行完成以後,就會把線程放回線程池,並且進入wait狀態。
合理利用線程池能夠帶來三個好處。
降低資源消耗。通過重復利用已創建的線程降低線程創建和銷毀造成的消耗。
提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行。
提高線程的可管理性。線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控。
使用Executors工廠類產生線程池
Executor線程池框架的最大優點是把任務的提交和執行解耦。客戶端將要執行的任務封裝成Task,然後提交即可
ExecutorService(實現類 ThreadPoolExecutor,ScheduledThreadPoolExecutor)繼承了Executor接口(註意區分Executor接口和Executors工廠類),
使用Executors執行多線程任務的步驟如下:
? 調用Executors類的靜態工廠方法創建一個ExecutorService對象,該對象代表一個線程池;
? 創建Runnable實現類或Callable實現類的實例,作為線程執行任務;
? 調用ExecutorService對象的submit()方法來提交Runnable實例或Callable實例;
? 當不想提交任務時,調用ExecutorService對象的shutdown()方法來關閉線程池。
【重點】ThreadPoolExecutor
簡單的線程池,就是創建線程備用的。
創建3個Runnable對象,這個對象裏面每次執行都需要3秒鐘。
把3個任務,提交給線程池,但是線程池的大小是1,意味著最多同時執行1個任務。
ExecutorService pool = Executors.newFixedThreadPool( 大小 );
ScheduledThreadPoolExecutor
可以調度的線程池,裏面的任務可以按照一定的規則循環、重復執行。
定時任務,一般會使用spring-timer來代替,支持更加復雜的任務調度方式。
ScheduledExecutorService pool = Executors.newScheduledThreadPool( 大小 );
定時重復調用的方法:
scheduleAtFixedRate : 以固定的頻率執行任務,以任務的開始時間計算頻率。
假設間隔2秒,每次執行任務需要3秒。
頻率的間隔比任務所需要的時間要小。
此時前面的任務完成以後,馬上執行下一次任務。
*間隔以開始時間計算
scheduleWithFixedDelay : 以固定的間隔執行任務
8.死鎖
產生死鎖的四個必要條件如下。當下邊的四個條件都滿足時即產生死鎖,即任意一個條件不滿足既不會產生死鎖。
(1)死鎖的四個必要條件 互斥條件:資源不能被共享,只能被同一個進程使用 請求與保持條件:已經得到資源的進程可以申請新的資源 非剝奪條件:已經分配的資源不能從相應的進程中被強制剝奪 循環等待條件:系統中若幹進程組成環路,該環路中每個進程都在等待相鄰進程占用的資源
舉個常見的死鎖例子:進程A中包含資源A,進程B中包含資源B,A的下一步需要資源B,B的下一步需要資源A,所以它們就互相等待對方占有的資源釋放,所以也就產生了一個循環等待死鎖。
(2)處理死鎖的方法
忽略該問題,也即鴕鳥算法。當發生了什麽問題時,不管他,直接跳過,無視它;
檢測死鎖並恢復;
資源進行動態分配;
破除上面的四種死鎖條件之一。
9.線程相關類
ThreadLocal
ThreadLocal它並不是一個線程,而是一個可以在每個線程中存儲數據的數據存儲類,通過它可以在指定的線程中存儲數據,數據存儲之後,只有在指定線程中可以獲取到存儲的數據,對於其他線程來說則無法獲取到該線程的數據。 即多個線程通過同一個ThreadLocal獲取到的東西是不一樣的,就算有的時候出現的結果是一樣的(偶然性,兩個線程裏分別存了兩份相同的東西),但他們獲取的本質是不同的。使用這個工具類可以簡化多線程編程時的並發訪問,很簡潔的隔離多線程程序的競爭資源。
對於多線程資源共享的問題,同步機制采用了“以時間換空間”的方式,而ThreadLocal采用了“以空間換時間”的方式。前者僅提供一份變量,讓不同的線程排隊訪問,而後者為每一個線程都提供了一份變量,因此可以同時訪問而互不影響。
若多個線程之間需要共享資源,以達到線程間的通信時,就使用同步機制;若僅僅需要隔離多線程之間的關系資源,則可以使用ThreadLocal。
7 多線程