7 多線程

多線程


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。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章