Java多線程詳解

線程對象是能夠產生線程的對象。好比在Java平臺中Thread對象,Runnable對象。線程,是指正在執行的一個指點令序列。在java平臺上是指從一個線程對象的start()開始,運行run方法體中的那一段相對獨立的過程。相比於多進程,多線程的優點有:html

    (1)進程之間不能共享數據,線程能夠;java

    (2)系統建立進程須要爲該進程從新分配系統資源,故建立線程代價比較小;程序員

    (3)Java語言內置了多線程功能支持,簡化了java多線程編程。算法

1、建立線程和啓動數據庫

  (1)繼承Thread類建立線程類編程

經過繼承Thread類建立線程類的具體步驟和具體代碼以下:數組

   • 定義一個繼承Thread類的子類,並重寫該類的run()方法;安全

   • 建立Thread子類的實例,即建立了線程對象;多線程

   • 調用該線程對象的start()方法啓動線程。併發

複製代碼
 class SomeThead extends Thraad   { 
    public void run()   { 
     //do something here  
    }  
 } 
 
public static void main(String[] args){
 SomeThread oneThread = new SomeThread();   
  步驟3:啓動線程:   
 oneThread.start(); 
}
複製代碼

    (2)實現Runnable接口建立線程類

經過實現Runnable接口建立線程類的具體步驟和具體代碼以下:

   • 定義Runnable接口的實現類,並重寫該接口的run()方法;

   • 建立Runnable實現類的實例,並以此實例做爲Thread的target對象,即該Thread對象纔是真正的線程對象。

複製代碼
class SomeRunnable implements Runnable   { 
  public void run()   { 
  //do something here  
  }  
} 
Runnable oneRunnable = new SomeRunnable();   
Thread oneThread = new Thread(oneRunnable);   
oneThread.start(); 
複製代碼

    (3)經過Callable和Future建立線程

經過Callable和Future建立線程的具體步驟和具體代碼以下:

   • 建立Callable接口的實現類,並實現call()方法,該call()方法將做爲線程執行體,而且有返回值。
   • 建立Callable實現類的實例,使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的call()方法的返回值。
   • 使用FutureTask對象做爲Thread對象的target建立並啓動新線程。
   • 調用FutureTask對象的get()方法來得到子線程執行結束後的返回值其中,Callable接口(也只有一個方法)定義以下: 

複製代碼
public interface Callable   { 
  V call() throws Exception;  
 } 
  步驟1:建立實現Callable接口的類SomeCallable(略);   
  步驟2:建立一個類對象: 
      Callable oneCallable = new SomeCallable(); 
  步驟3:由Callable建立一個FutureTask對象:   
    FutureTask oneTask = new FutureTask(oneCallable); 
  註釋: FutureTask是一個包裝器,它經過接受Callable來建立,它同時實現了 Future和Runnable接口。 
  步驟4:由FutureTask建立一個Thread對象:   
    Thread oneThread = new Thread(oneTask);   
  步驟5:啓動線程:  
    oneThread.start(); 
複製代碼

2、線程的生命週期

                        

一、新建狀態

       用new關鍵字和Thread類或其子類創建一個線程對象後,該線程對象就處於新生狀態。處於新生狀態的線程有本身的內存空間,經過調用start方法進入就緒狀態(runnable)。

注意:不能對已經啓動的線程再次調用start()方法,不然會出現Java.lang.IllegalThreadStateException異常。

二、就緒狀態

       處於就緒狀態的線程已經具有了運行條件,但尚未分配到CPU,處於線程就緒隊列(儘管是採用隊列形式,事實上,把它稱爲可運行池而不是可運行隊列。由於cpu的調度不必定是按照先進先出的順序來調度的),等待系統爲其分配CPU。等待狀態並非執行狀態,當系統選定一個等待執行的Thread對象後,它就會從等待執行狀態進入執行狀態,系統挑選的動做稱之爲「cpu調度」。一旦得到CPU,線程就進入運行狀態並自動調用本身的run方法。

提示:若是但願子線程調用start()方法後當即執行,可使用Thread.sleep()方式使主線程睡眠一夥兒,轉去執行子線程。

三、運行狀態

      處於運行狀態的線程最爲複雜,它能夠變爲阻塞狀態、就緒狀態和死亡狀態。

處於就緒狀態的線程,若是得到了cpu的調度,就會從就緒狀態變爲運行狀態,執行run()方法中的任務。若是該線程失去了cpu資源,就會又從運行狀態變爲就緒狀態。從新等待系統分配資源。也能夠對在運行狀態的線程調用yield()方法,它就會讓出cpu資源,再次變爲就緒狀態。

注: 當發生以下狀況是,線程會從運行狀態變爲阻塞狀態:

     ①、線程調用sleep方法主動放棄所佔用的系統資源

     ②、線程調用一個阻塞式IO方法,在該方法返回以前,該線程被阻塞

     ③、線程試圖得到一個同步監視器,但更改同步監視器正被其餘線程所持有

     ④、線程在等待某個通知(notify)

     ⑤、程序調用了線程的suspend方法將線程掛起。不過該方法容易致使死鎖,因此程序應該儘可能避免使用該方法。

當線程的run()方法執行完,或者被強制性地終止,例如出現異常,或者調用了stop()、desyory()方法等等,就會從運行狀態轉變爲死亡狀態。

四、阻塞狀態

      處於運行狀態的線程在某些狀況下,如執行了sleep(睡眠)方法,或等待I/O設備等資源,將讓出CPU並暫時中止本身的運行,進入阻塞狀態。 

在阻塞狀態的線程不能進入就緒隊列。只有當引發阻塞的緣由消除時,如睡眠時間已到,或等待的I/O設備空閒下來,線程便轉入就緒狀態,從新到就緒隊列中排隊等待,被系統選中後從原來中止的位置開始繼續運行。有三種方法能夠暫停Threads執行:

五、死亡狀態

      當線程的run()方法執行完,或者被強制性地終止,就認爲它死去。這個線程對象也許是活的,可是,它已經不是一個單獨執行的線程。線程一旦死亡,就不能復生。 若是在一個死去的線程上調用start()方法,會拋出java.lang.IllegalThreadStateException異常。

3、線程管理

     Java提供了一些便捷的方法用於會線程狀態的控制。具體以下:

一、線程睡眠——sleep

      若是咱們須要讓當前正在執行的線程暫停一段時間,並進入阻塞狀態,則能夠經過調用Thread的sleep方法。

注:

   (1)sleep是靜態方法,最好不要用Thread的實例對象調用它,由於它睡眠的始終是當前正在運行的線程,而不是調用它的線程對象,它只對正在運行狀態的線程對象有效。以下面的例子:

複製代碼
public class Test1 {  
    public static void main(String[] args) throws InterruptedException {  
        System.out.println(Thread.currentThread().getName());  
        MyThread myThread=new MyThread();  
        myThread.start();  
        myThread.sleep(1000);//這裏sleep的就是main線程,而非myThread線程  
        Thread.sleep(10);  
        for(int i=0;i<100;i++){  
            System.out.println("main"+i);  
        }  
    }  
} 
複製代碼

     (2)Java線程調度是Java多線程的核心,只有良好的調度,才能充分發揮系統的性能,提升程序的執行效率。可是無論程序員怎麼編寫調度,只能最大限度的影響線程執行的次序,而不能作到精準控制。由於使用sleep方法以後,線程是進入阻塞狀態的,只有當睡眠的時間結束,纔會從新進入到就緒狀態,而就緒狀態進入到運行狀態,是由系統控制的,咱們不可能精準的去幹涉它,因此若是調用Thread.sleep(1000)使得線程睡眠1秒,可能結果會大於1秒。

二、線程讓步——yield

      yield()方法和sleep()方法有點類似,它也是Thread類提供的一個靜態的方法,它也可讓當前正在執行的線程暫停,讓出cpu資源給其餘的線程。可是和sleep()方法不一樣的是,它不會進入到阻塞狀態,而是進入到就緒狀態。yield()方法只是讓當前線程暫停一下,從新進入就緒的線程池中,讓系統的線程調度器從新調度器從新調度一次,徹底可能出現這樣的狀況:當某個線程調用yield()方法以後,線程調度器又將其調度出來從新進入到運行狀態執行。

實際上,當某個線程調用了yield()方法暫停以後,優先級與當前線程相同,或者優先級比當前線程更高的就緒狀態的線程更有可能得到執行的機會,固然,只是有可能,由於咱們不可能精確的干涉cpu調度線程。用法以下:

複製代碼
public class Test1 {  
    public static void main(String[] args) throws InterruptedException {  
        new MyThread("低級", 1).start();  
        new MyThread("中級", 5).start();  
        new MyThread("高級", 10).start();  
    }  
}  
  
class MyThread extends Thread {  
    public MyThread(String name, int pro) {  
        super(name);// 設置線程的名稱  
        this.setPriority(pro);// 設置優先級  
    }  
  
    @Override  
    public void run() {  
        for (int i = 0; i < 30; i++) {  
            System.out.println(this.getName() + "線程第" + i + "次執行!");  
            if (i % 5 == 0)  
                Thread.yield();  
        }  
    }  
} 
複製代碼

注:關於sleep()方法和yield()方的區別以下:

①、sleep方法暫停當前線程後,會進入阻塞狀態,只有當睡眠時間到了,纔會轉入就緒狀態。而yield方法調用後 ,是直接進入就緒狀態,因此有可能剛進入就緒狀態,又被調度到運行狀態。

②、sleep方法聲明拋出了InterruptedException,因此調用sleep方法的時候要捕獲該異常,或者顯示聲明拋出該異常。而yield方法則沒有聲明拋出任務異常。

③、sleep方法比yield方法有更好的可移植性,一般不要依靠yield方法來控制併發線程的執行。

三、線程合併——join

線程的合併的含義就是將幾個並行線程的線程合併爲一個單線程執行,應用場景是當一個線程必須等待另外一個線程執行完畢才能執行時,Thread類提供了join方法來完成這個功能,注意,它不是靜態方法。
從上面的方法的列表能夠看到,它有3個重載的方法:

void join()      
當前線程等該加入該線程後面,等待該線程終止。 void join(long millis)
當前線程等待該線程終止的時間最長爲 millis 毫秒。 若是在millis時間內,該線程沒有執行完,那麼當前線程進入就緒狀態,從新等待cpu調度 void join(long millis,int nanos)
等待該線程終止的時間最長爲 millis 毫秒 + nanos 納秒。若是在millis時間內,該線程沒有執行完,那麼當前線程進入就緒狀態,從新等待cpu調度

四、設置線程的優先級

     每一個線程執行時都有一個優先級的屬性,優先級高的線程能夠得到較多的執行機會,而優先級低的線程則得到較少的執行機會。與線程休眠相似,線程的優先級仍然沒法保障線程的執行次序。只不過,優先級高的線程獲取CPU資源的機率較大,優先級低的也並不是沒機會執行。

每一個線程默認的優先級都與建立它的父線程具備相同的優先級,在默認狀況下,main線程具備普通優先級。

注:Thread類提供了setPriority(int newPriority)和getPriority()方法來設置和返回一個指定線程的優先級,其中setPriority方法的參數是一個整數,範圍是1~·0之間,也可使用Thread類提供的三個靜態常量:

MAX_PRIORITY   =10

MIN_PRIORITY   =1

NORM_PRIORITY   =5
複製代碼
    public class Test1 {  
        public static void main(String[] args) throws InterruptedException {  
            new MyThread("高級", 10).start();  
            new MyThread("低級", 1).start();  
        }  
    }  
      
    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 + "次執行!");  
            }  
        }  
    }  
複製代碼

注:雖然Java提供了10個優先級別,但這些優先級別須要操做系統的支持。不一樣的操做系統的優先級並不相同,並且也不能很好的和Java的10個優先級別對應。因此咱們應該使用MAX_PRIORITY、MIN_PRIORITY和NORM_PRIORITY三個靜態常量來設定優先級,這樣才能保證程序最好的可移植性。

五、後臺(守護)線程

     守護線程使用的狀況較少,但並不是無用,舉例來講,JVM的垃圾回收、內存管理等線程都是守護線程。還有就是在作數據庫應用時候,使用的數據庫鏈接池,鏈接池自己也包含着不少後臺線程,監控鏈接個數、超時時間、狀態等等。調用線程對象的方法setDaemon(true),則能夠將其設置爲守護線程。守護線程的用途爲:

     • 守護線程一般用於執行一些後臺做業,例如在你的應用程序運行時播放背景音樂,在文字編輯器裏作自動語法檢查、自動保存等功能。

     • Java的垃圾回收也是一個守護線程。守護線的好處就是你不須要關心它的結束問題。例如你在你的應用程序運行的時候但願播放背景音樂,若是將這個播放背景音樂的線程設定爲非守護線程,那麼在用戶請求退出的時候,不只要退出主線程,還要通知播放背景音樂的線程退出;若是設定爲守護線程則不須要了。

setDaemon方法的詳細說明:

複製代碼
public final void setDaemon(boolean on)        將該線程標記爲守護線程或用戶線程。當正在運行的線程都是守護線程時,Java 虛擬機退出。    
         該方法必須在啓動線程前調用。 該方法首先調用該線程的 checkAccess 方法,且不帶任何參數。這可能拋出 SecurityException(在當前線程中)。   
  參數:
     on - 若是爲 true,則將該線程標記爲守護線程。    
  拋出:    
    IllegalThreadStateException - 若是該線程處於活動狀態。    
    SecurityException - 若是當前線程沒法修改該線程。
複製代碼

注:JRE判斷程序是否執行結束的標準是全部的前臺執線程行完畢了,而無論後臺線程的狀態,所以,在使用後臺縣城時候必定要注意這個問題

六、正確結束線程

Thread.stop()、Thread.suspend、Thread.resume、Runtime.runFinalizersOnExit這些終止線程運行的方法已經被廢棄了,使用它們是極端不安全的!想要安全有效的結束一個線程,可使用下面的方法:

    • 正常執行完run方法,而後結束掉;

    • 控制循環條件和判斷條件的標識符來結束掉線程。

複製代碼
class MyThread extends Thread {  
    int i=0;  
    boolean next=true;  
    @Override  
    public void run() {  
        while (next) {  
            if(i==10)  
                next=false;  
            i++;  
            System.out.println(i);  
        }  
    }  
}
複製代碼

4、線程同步

     java容許多線程併發控制,當多個線程同時操做一個可共享的資源變量時(如數據的增刪改查),將會致使數據不許確,相互之間產生衝突,所以加入同步鎖以免在該線程沒有完成操做以前,被其餘線程的調用,從而保證了該變量的惟一性和準確性。

一、同步方法     

      即有synchronized關鍵字修飾的方法。因爲java的每一個對象都有一個內置鎖,當用此關鍵字修飾方法時,內置鎖會保護整個方法。在調用該方法前,須要得到內置鎖,不然就處於阻塞狀態。

1
public  synchronized  void  save(){}

 注: synchronized關鍵字也能夠修飾靜態方法,此時若是調用該靜態方法,將會鎖住整個類

 二、同步代碼塊     

     即有synchronized關鍵字修飾的語句塊。被該關鍵字修飾的語句塊會自動被加上內置鎖,從而實現同步。

複製代碼
    public class Bank {  
     
        private int count =0;//帳戶餘額  
     
        //存錢  
        public   void addMoney(int money){  
     
            synchronized (this) {  
                count +=money;  
            }  
            System.out.println(System.currentTimeMillis()+"存進:"+money);  
        }  
     
        //取錢  
        public   void subMoney(int money){  
     
            synchronized (this) {  
                if(count-money < 0){  
                    System.out.println("餘額不足");  
                    return;  
                }  
                count -=money;  
            }  
            System.out.println(+System.currentTimeMillis()+"取出:"+money);  
        }  
     
        //查詢  
        public void lookMoney(){  
            System.out.println("帳戶餘額:"+count);  
        } 
    }
 
 
複製代碼

注:同步是一種高開銷的操做,所以應該儘可能減小同步的內容。一般沒有必要同步整個方法,使用synchronized代碼塊同步關鍵代碼便可。

 三、使用特殊域變量(volatile)實現線程同步      

   • volatile關鍵字爲域變量的訪問提供了一種免鎖機制;

   • 使用volatile修飾域至關於告訴虛擬機該域可能會被其餘線程更新;

   • 所以每次使用該域就要從新計算,而不是使用寄存器中的值;

   • volatile不會提供任何原子操做,它也不能用來修飾final類型的變量。

複製代碼
 public class SynchronizedThread {
 
        class Bank {
 
            private volatile int account = 100;
 
            public int getAccount() {
                return account;
            }
 
            /**
             * 用同步方法實現
             * 
             * @param money
             */
            public synchronized void save(int money) {
                account += money;
            }
 
            /**
             * 用同步代碼塊實現
             * 
             * @param money
             */
            public void save1(int money) {
                synchronized (this) {
                    account += money;
                }
            }
        }
 
        class NewThread implements Runnable {
            private Bank bank;
 
            public NewThread(Bank bank) {
                this.bank = bank;
            }
 
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    // bank.save1(10);
                    bank.save(10);
                    System.out.println(i + "帳戶餘額爲:" +bank.getAccount());
                }
            }
 
        }
 
        /**
         * 創建線程,調用內部類
         */
        public void useThread() {
            Bank bank = new Bank();
            NewThread new_thread = new NewThread(bank);
            System.out.println("線程1");
            Thread thread1 = new Thread(new_thread);
            thread1.start();
            System.out.println("線程2");
            Thread thread2 = new Thread(new_thread);
            thread2.start();
        }
 
        public static void main(String[] args) {
            SynchronizedThread st = new SynchronizedThread();
            st.useThread();
        }
 
複製代碼

注:多線程中的非同步問題主要出如今對域的讀寫上,若是讓域自身避免這個問題,則就不須要修改操做該域的方法。用final域,有鎖保護的域和volatile域能夠避免非同步的問題。

四、使用重入鎖(Lock)實現線程同步

      在JavaSE5.0中新增了一個java.util.concurrent包來支持同步。ReentrantLock類是可重入、互斥、實現了Lock接口的鎖,它與使用synchronized方法和快具備相同的基本行爲和語義,而且擴展了其能力。ReenreantLock類的經常使用方法有:    

 ReentrantLock() : 建立一個ReentrantLock實例         
 lock() : 得到鎖        
 unlock() : 釋放鎖

注:ReentrantLock()還有一個能夠建立公平鎖的構造方法,但因爲能大幅度下降程序運行效率,不推薦使用

複製代碼
//只給出要修改的代碼,其他代碼與上同
        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();
                }
                
            }
        } 
複製代碼

5、線程通訊

一、藉助於Object類的wait()、notify()和notifyAll()實現通訊

     線程執行wait()後,就放棄了運行資格,處於凍結狀態;

     線程運行時,內存中會創建一個線程池,凍結狀態的線程都存在於線程池中,notify()執行時喚醒的也是線程池中的線程,線程池中有多個線程時喚醒第一個被凍結的線程。
      notifyall(), 喚醒線程池中全部線程。
注: (1) wait(), notify(),notifyall()都用在同步裏面,由於這3個函數是對持有鎖的線程進行操做,而只有同步纔有鎖,因此要使用在同步中;
       (2) wait(),notify(),notifyall(),  在使用時必須標識它們所操做的線程持有的鎖,由於等待和喚醒必須是同一鎖下的線程;而鎖能夠是任意對象,因此這3個方法都是Object類中的方法。

單個消費者生產者例子以下:

複製代碼
 class Resource{  //生產者和消費者都要操做的資源  
    private String name;  
    private int count=1;  
    private boolean flag=false;  
    public synchronized void set(String name){  
        if(flag)  
            try{wait();}catch(Exception e){}  
        this.name=name+"---"+count++;  
        System.out.println(Thread.currentThread().getName()+"...生產者..."+this.name);  
        flag=true;  
        this.notify();  
    }  
    public synchronized void out(){  
        if(!flag)  
            try{wait();}catch(Exception e){}  
        System.out.println(Thread.currentThread().getName()+"...消費者..."+this.name);  
        flag=false;  
        this.notify();  
    }  
}  
class Producer implements Runnable{  
    private Resource res;  
    Producer(Resource res){  
        this.res=res;  
    }  
    public void run(){  
        while(true){  
            res.set("商品");  
        }  
    }  
}  
class Consumer implements Runnable{  
    private Resource res;  
    Consumer(Resource res){  
        this.res=res;  
    }  
    public void run(){  
        while(true){  
            res.out();  
        }  
    }  
}  
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);  
        t1.start();  
        t2.start();  
    }  
}//運行結果正常,生產者生產一個商品,緊接着消費者消費一個商品。
複製代碼

      可是若是有多個生產者和多個消費者,上面的代碼是有問題,好比2個生產者,2個消費者,運行結果就可能出現生產的1個商品生產了一次而被消費了2次,或者連續生產2個商品而只有1個被消費,這是由於此時共有4個線程在操做Resource對象r,  而notify()喚醒的是線程池中第1個wait()的線程,因此生產者執行notify()時,喚醒的線程有多是另1個生產者線程,這個生產者線程從wait()中醒來後不會再判斷flag,而是直接向下運行打印出一個新的商品,這樣就出現了連續生產2個商品。
爲了不這種狀況,修改代碼以下:

複製代碼
    class Resource{  
        private String name;  
        private int count=1;  
        private boolean flag=false;  
        public synchronized void set(String name){  
            while(flag) /*原先是if,如今改爲while,這樣生產者線程從凍結狀態醒來時,還會再判斷flag.*/  
                try{wait();}catch(Exception e){}  
            this.name=name+"---"+count++;  
            System.out.println(Thread.currentThread().getName()+"...生產者..."+this.name);  
            flag=true;  
            this.notifyAll();/*原先是notity(), 如今改爲notifyAll(),這樣生產者線程生產完一個商品後能夠將等待中的消費者線程喚醒,不然只將上面改爲while後,可能出現全部生產者和消費者都在wait()的狀況。*/  
        }  
        public synchronized void out(){  
            while(!flag) /*原先是if,如今改爲while,這樣消費者線程從凍結狀態醒來時,還會再判斷flag.*/  
                try{wait();}catch(Exception e){}  
            System.out.println(Thread.currentThread().getName()+"...消費者..."+this.name);  
            flag=false;  
            this.notifyAll(); /*原先是notity(), 如今改爲notifyAll(),這樣消費者線程消費完一個商品後能夠將等待中的生產者線程喚醒,不然只將上面改爲while後,可能出現全部生產者和消費者都在wait()的狀況。*/  
        }  
    }  
    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();  
        }  
    }  
複製代碼

二、使用Condition控制線程通訊

      jdk1.5中,提供了多線程的升級解決方案爲:

     (1)將同步synchronized替換爲顯式的Lock操做;

     (2)將Object類中的wait(), notify(),notifyAll()替換成了Condition對象,該對象能夠經過Lock鎖對象獲取;

     (3)一個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();  
            }  
        }  
    }  
複製代碼

三、使用阻塞隊列(BlockingQueue)控制線程通訊

       BlockingQueue是一個接口,也是Queue的子接口。BlockingQueue具備一個特徵:當生產者線程試圖向BlockingQueue中放入元素時,若是該隊列已滿,則線程被阻塞;但消費者線程試圖從BlockingQueue中取出元素時,若是隊列已空,則該線程阻塞。程序的兩個線程經過交替向BlockingQueue中放入元素、取出元素,便可很好地控制線程的通訊。

BlockingQueue提供以下兩個支持阻塞的方法:

  (1)put(E e):嘗試把Eu元素放如BlockingQueue中,若是該隊列的元素已滿,則阻塞該線程。

  (2)take():嘗試從BlockingQueue的頭部取出元素,若是該隊列的元素已空,則阻塞該線程。

BlockingQueue繼承了Queue接口,固然也可使用Queue接口中的方法,這些方法概括起來能夠分爲以下三組:

  (1)在隊列尾部插入元素,包括add(E e)、offer(E e)、put(E e)方法,當該隊列已滿時,這三個方法分別會拋出異常、返回false、阻塞隊列。

  (2)在隊列頭部刪除並返回刪除的元素。包括remove()、poll()、和take()方法,當該隊列已空時,這三個方法分別會拋出異常、返回false、阻塞隊列。

  (3)在隊列頭部取出但不刪除元素。包括element()和peek()方法,當隊列已空時,這兩個方法分別拋出異常、返回false。

BlockingQueue接口包含以下5個實現類:

 

複製代碼
ArrayBlockingQueue :基於數組實現的BlockingQueue隊列。

LinkedBlockingQueue:基於鏈表實現的BlockingQueue隊列。

PriorityBlockingQueue:它並非保準的阻塞隊列,該隊列調用remove()、poll()、take()等方法提取出元素時,並非取出隊列中存在時間最長的元素,而是隊列中最小的元素。
它判斷元素的大小便可根據元素(實現Comparable接口)的自己大小來天然排序,也可以使用Comparator進行定製排序。 SynchronousQueue:同步隊列。對該隊列的存、取操做必須交替進行。 DelayQueue:它是一個特殊的BlockingQueue,底層基於PriorityBlockingQueue實現,不過,DelayQueue要求集合元素都實現Delay接口(該接口裏只有一個long getDelay()方法),
DelayQueue根據集合元素的getDalay()方法的返回值進行排序。
複製代碼

copy的一個示例:

複製代碼
 1 import java.util.concurrent.ArrayBlockingQueue;
 2 import java.util.concurrent.BlockingQueue;
 3 public class BlockingQueueTest{
 4     public static void main(String[] args)throws Exception{
 5         //建立一個容量爲1的BlockingQueue
 6         
 7         BlockingQueue<String> b=new ArrayBlockingQueue<>(1);
 8         //啓動3個生產者線程
 9         new Producer(b).start();
10         new Producer(b).start();
11         new Producer(b).start();
12         //啓動一個消費者線程
13         new Consumer(b).start();
14         
15     }
16 }
17 class Producer extends Thread{
18     private BlockingQueue<String> b;
19     
20     public Producer(BlockingQueue<String> b){
21         this.b=b;
22         
23     }
24     public synchronized void run(){
25         String [] str=new String[]{
26             "java",
27             "struts",
28             "Spring"
29         };
30         for(int i=0;i<9999999;i++){
31             System.out.println(getName()+"生產者準備生產集合元素!");
32             try{
33             
34                 b.put(str[i%3]);
35                 sleep(1000);
36                 //嘗試放入元素,若是隊列已滿,則線程被阻塞
37                 
38             }catch(Exception e){System.out.println(e);}
39             System.out.println(getName()+"生產完成:"+b);
40         }
41         
42     }
43 }
44 class Consumer extends Thread{
45     private BlockingQueue<String> b;
46     public Consumer(BlockingQueue<String> b){
47         this.b=b;
48     }
49     public  synchronized  void run(){
50     
51         while(true){
52             System.out.println(getName()+"消費者準備消費集合元素!");
53             try{
54                 sleep(1000);
55                 //嘗試取出元素,若是隊列已空,則線程被阻塞
56                 b.take();
57             }catch(Exception e){System.out.println(e);}
58             System.out.println(getName()+"消費完:"+b);
59         }
60     
61     }
62 }
複製代碼

6、線程池

   合理利用線程池可以帶來三個好處。

  1. 下降資源消耗。經過重複利用已建立的線程下降線程建立和銷燬形成的消耗。
  2. 提升響應速度。當任務到達時,任務能夠不須要等到線程建立就能當即執行。
  3. 提升線程的可管理性。線程是稀缺資源,若是無限制的建立,不只會消耗系統資源,還會下降系統的穩定性,使用線程池能夠進行統一的分配,調優和監控。

一、使用Executors工廠類產生線程池

      Executor線程池框架的最大優勢是把任務的提交和執行解耦。客戶端將要執行的任務封裝成Task,而後提交便可。而Task如何執行客戶端則是透明的。具體點講,提交一個Callable對象給ExecutorService(如最經常使用的線程池ThreadPoolExecutor),將獲得一個Future對象,調用Future對象的get方法等待執行結果。線程池實現原理類結構圖以下:

               

      上圖中涉及到的線程池內部實現原理的全部類,不利於咱們理解線程池如何使用。咱們先從客戶端的角度出發,看看客戶端使用線程池所涉及到的類結構圖:

                               

    由上圖可知,ExecutorServiceJava中對線程池定義的一個接口,它java.util.concurrent包中。  Java API對ExecutorService接口的實現有兩個,因此這兩個便是Java線程池具體實現類以下:

 ThreadPoolExecutor
 ScheduledThreadPoolExecutor

    除此以外,ExecutorService還繼承了Executor接口(注意區分Executor接口和Executors工廠類),這個接口只有一個execute()方法,最後咱們看一下整個繼承樹:

    使用Executors執行多線程任務的步驟以下:

  • 調用Executors類的靜態工廠方法建立一個ExecutorService對象,該對象表明一個線程池;

  • 建立Runnable實現類或Callable實現類的實例,做爲線程執行任務;

  • 調用ExecutorService對象的submit()方法來提交Runnable實例或Callable實例;

  • 當不想提交任務時,調用ExecutorService對象的shutdown()方法來關閉線程池。                                                                 

(1)使用Executors的靜態工廠類建立線程池的方法以下:

一、newFixedThreadPool() : 
     做用:該方法返回一個固定線程數量的線程池,該線程池中的線程數量始終不變,即不會再建立新的線程,也不會銷燬已經建立好的線程,自始自終都是那幾個固定的線程在工做,因此該線程池能夠控制線程的最大併發數。 
栗子:假若有一個新任務提交時,線程池中若是有空閒的線程則當即使用空閒線程來處理任務,若是沒有,則會把這個新任務存在一個任務隊列中,一旦有線程空閒了,則按FIFO方式處理任務隊列中的任務。
二、newCachedThreadPool() : 
     做用:該方法返回一個能夠根據實際狀況調整線程池中線程的數量的線程池。即該線程池中的線程數量不肯定,是根據實際狀況動態調整的。 
栗子:假如該線程池中的全部線程都正在工做,而此時有新任務提交,那麼將會建立新的線程去處理該任務,而此時假如以前有一些線程完成了任務,如今又有新任務提交,那麼將不會建立新線程去處理,而是複用空閒的線程去處理新任務。那麼此時有人有疑問了,那這樣來講該線程池的線程豈不是會越集越多?其實並不會,由於線程池中的線程都有一個「保持活動時間」的參數,經過配置它,若是線程池中的空閒線程的空閒時間超過該「保存活動時間」則馬上中止該線程,而該線程池默認的「保持活動時間」爲60s。
三、newSingleThreadExecutor() : 
     做用:該方法返回一個只有一個線程的線程池,即每次只能執行一個線程任務,多餘的任務會保存到一個任務隊列中,等待這一個線程空閒,當這個線程空閒了再按FIFO方式順序執行任務隊列中的任務。
四、newScheduledThreadPool() : 
     做用:該方法返回一個能夠控制線程池內線程定時或週期性執行某任務的線程池。
五、newSingleThreadScheduledExecutor() : 
     做用:該方法返回一個能夠控制線程池內線程定時或週期性執行某任務的線程池。只不過和上面的區別是該線程池大小爲1,而上面的能夠指定線程池的大小。

注:Executors只是一個工廠類,它全部的方法返回的都是ThreadPoolExecutorScheduledThreadPoolExecutor這兩個類的實例。

(2) ExecutorService有以下幾個執行方法:

- execute(Runnable)
- submit(Runnable)
- submit(Callable)
- invokeAny(...)
- invokeAll(...)

execute(Runnable)

     這個方法接收一個Runnable實例,而且異步的執行,請看下面的實例:

複製代碼
ExecutorService executorService = Executors.newSingleThreadExecutor();

executorService.execute(new Runnable() {
public void run() {
    System.out.println("Asynchronous task");
}
});

executorService.shutdown();
複製代碼

submit(Runnable)

   submit(Runnable)execute(Runnable)區別是前者能夠返回一個Future對象,經過返回的Future對象,咱們能夠檢查提交的任務是否執行完畢,請看下面執行的例子:

複製代碼
Future future = executorService.submit(new Runnable() {
public void run() {
    System.out.println("Asynchronous task");
}
});

future.get();  //returns null if the task has finished correctly.
複製代碼

注:若是任務執行完成,future.get()方法會返回一個null。注意,future.get()方法會產生阻塞。

submit(Callable)

   submit(Callable)submit(Runnable)相似,也會返回一個Future對象,可是除此以外,submit(Callable)接收的是一個Callable的實現,Callable接口中的call()方法有一個返回值,能夠返回任務的執行結果,而Runnable接口中的run()方法是void的,沒有返回值。請看下面實例:

複製代碼
Future future = executorService.submit(new Callable(){
public Object call() throws Exception {
    System.out.println("Asynchronous Callable");
    return "Callable Result";
}
});

System.out.println("future.get() = " + future.get());
複製代碼

    若是任務執行完成,future.get()方法會返回Callable任務的執行結果。另外,future.get()方法會產生阻塞。

invokeAny(…)

invokeAny(...)方法接收的是一個Callable的集合,執行這個方法不會返回Future,可是會返回全部Callable任務中其中一個任務的執行結果。這個方法也沒法保證返回的是哪一個任務的執行結果,反正是其中的某一個。請看下面實例:

複製代碼
ExecutorService executorService = Executors.newSingleThreadExecutor();

Set<Callable<String>> callables = new HashSet<Callable<String>>();

callables.add(new Callable<String>() {
public String call() throws Exception {
    return "Task 1";
}
});
callables.add(new Callable<String>() {
public String call() throws Exception {
    return "Task 2";
}
});
callables.add(new Callable<String>() {
    public String call() throws Exception {
    return "Task 3";
}
});
String result = executorService.invokeAny(callables);
System.out.println("result = " + result);
executorService.shutdown();
複製代碼

     你們能夠嘗試執行上面代碼,每次執行都會返回一個結果,而且返回的結果是變化的,可能會返回「Task2」也但是「Task1」或者其它。

invokeAll(…)

   invokeAll(...)與 invokeAny(...)相似也是接收一個Callable集合,可是前者執行以後會返回一個Future的List,其中對應着每一個Callable任務執行後的Future對象。狀況下面這個實例:

   

複製代碼
ExecutorService executorService = Executors.newSingleThreadExecutor();

Set<Callable<String>> callables = new HashSet<Callable<String>>();

callables.add(new Callable<String>() {
public String call() throws Exception {
    return "Task 1";
}
});
callables.add(new Callable<String>() {
    public String call() throws Exception {
    return "Task 2";
}
});
callables.add(new Callable<String>() {
public String call() throws Exception {
    return "Task 3";
}
});

List<Future<String>> futures = executorService.invokeAll(callables);
for(Future<String> future : futures){
    System.out.println("future.get = " + future.get());
}
executorService.shutdown();
複製代碼

(3) ExecutorService關閉方法

     當咱們使用完成ExecutorService以後應該關閉它,不然它裏面的線程會一直處於運行狀態。舉個例子,若是的應用程序是經過main()方法啓動的,在這個main()退出以後,若是應用程序中的ExecutorService沒有關閉,這個應用將一直運行。之因此會出現這種狀況,是由於ExecutorService中運行的線程會阻止JVM關閉。

     要關閉ExecutorService中執行的線程,咱們能夠調用ExecutorService.shutdown()方法。在調用shutdown()方法以後,ExecutorService不會當即關閉,可是它再也不接收新的任務,直到當前全部線程執行完成纔會關閉,全部在shutdown()執行以前提交的任務都會被執行。

    若是想當即關閉ExecutorService,咱們能夠調用ExecutorService.shutdownNow()方法。這個動做將跳過全部正在執行的任務和被提交尚未執行的任務。可是它並不對正在執行的任務作任何保證,有可能它們都會中止,也有可能執行完成。

二、使用Java8加強的ForkJoinPool產生線程池

      在Java 8中,引入了自動並行化的概念。它可以讓一部分Java代碼自動地以並行的方式執行,前提是使用了ForkJoinPool。

     ForkJoinPool同ThreadPoolExecutor同樣,也實現了Executor和ExecutorService接口。它使用了一個無限隊列來保存須要執行的任務,而線程的數量則是經過構造函數傳入,若是沒有向構造函數中傳入但願的線程數量,那麼當前計算機可用的CPU數量會被設置爲線程數量做爲默認值。

      ForkJoinPool主要用來使用分治法(Divide-and-Conquer Algorithm)來解決問題。典型的應用好比快速排序算法。這裏的要點在於,ForkJoinPool須要使用相對少的線程來處理大量的任務。好比要對1000萬個數據進行排序,那麼會將這個任務分割成兩個500萬的排序任務和一個針對這兩組500萬數據的合併任務。以此類推,對於500萬的數據也會作出一樣的分割處理,到最後會設置一個閾值來規定當數據規模到多少時,中止這樣的分割處理。好比,當元素的數量小於10時,會中止分割,轉而使用插入排序對它們進行排序。那麼到最後,全部的任務加起來會有大概2000000+個。問題的關鍵在於,對於一個任務而言,只有當它全部的子任務完成以後,它纔可以被執行。因此當使用ThreadPoolExecutor時,使用分治法會存在問題,由於ThreadPoolExecutor中的線程沒法像任務隊列中再添加一個任務而且在等待該任務完成以後再繼續執行。而使用ForkJoinPool時,就可以讓其中的線程建立新的任務,並掛起當前的任務,此時線程就可以從隊列中選擇子任務執行。好比,咱們須要統計一個double數組中小於0.5的元素的個數,那麼可使用ForkJoinPool進行實現以下:

複製代碼
public class ForkJoinTest {
    private double[] d;
    private class ForkJoinTask extends RecursiveTask {
        private int first;
        private int last;
        public ForkJoinTask(int first, int last) {
            this.first = first;
            this.last = last;
        }
        protected Integer compute() {
            int subCount;
            if (last - first < 10) {
                subCount = 0;
                for (int i = first; i <= last; i++) {
                    if (d[i] < 0.5){
                        subCount++;
} } }else { int mid = (first + last) /2; ForkJoinTask left = new ForkJoinTask(first, mid); left.fork(); ForkJoinTask right = new ForkJoinTask(mid + 1, last); right.fork(); subCount = left.join(); subCount += right.join(); } return subCount; } } public static void main(String[] args) { ForkJoinPool pool=new ForkJoinPool(); pool.submit(new ForkJoinTask(0, 9999999));
pool.awaitTermination(2,TimeUnit.SECONDS); System.out.println("Found " + n + " values"); } }
複製代碼

     以上的關鍵是fork()和join()方法。在ForkJoinPool使用的線程中,會使用一個內部隊列來對須要執行的任務以及子任務進行操做來保證它們的執行順序。

注:使用ThreadPoolExecutor和ForkJoinPool的性能差別:

  (1)首先,使用ForkJoinPool可以使用數量有限的線程來完成很是多的具備父子關係的任務,好比使用4個線程來完成超過200萬個任務。可是,使用ThreadPoolExecutor時,是不可能完成的,由於ThreadPoolExecutor中的Thread沒法選擇優先執行子任務,須要完成200萬個具備父子關係的任務時,也須要200萬個線程,顯然這是不可行的。

  (2)ForkJoinPool可以實現工做竊取(Work Stealing),在該線程池的每一個線程中會維護一個隊列來存放須要被執行的任務。當線程自身隊列中的任務都執行完畢後,它會從別的線程中拿到未被執行的任務並幫助它執行。所以,提升了線程的利用率,從而提升了總體性能。

  (3)對於ForkJoinPool,還有一個因素會影響它的性能,就是中止進行任務分割的那個閾值。好比在以前的快速排序中,當剩下的元素數量小於10的時候,就會中止子任務的建立。

結論:

  1. 當須要處理遞歸分治算法時,考慮使用ForkJoinPool;
  2. 仔細設置再也不進行任務劃分的閾值,這個閾值對性能有影響;
  3. Java 8中的一些特性會使用到ForkJoinPool中的通用線程池。在某些場合下,須要調整該線程池的默認的線程數量。

7、死鎖

   產生死鎖的四個必要條件以下。當下邊的四個條件都知足時即產生死鎖,即任意一個條件不知足既不會產生死鎖。

 (1)死鎖的四個必要條件

 

  • 互斥條件:資源不能被共享,只能被同一個進程使用
  • 請求與保持條件:已經獲得資源的進程能夠申請新的資源
  • 非剝奪條件:已經分配的資源不能從相應的進程中被強制剝奪
  • 循環等待條件:系統中若干進程組成環路,該環路中每一個進程都在等待相鄰進程佔用的資源

 

      舉個常見的死鎖例子:進程A中包含資源A,進程B中包含資源B,A的下一步須要資源B,B的下一步須要資源A,因此它們就互相等待對方佔有的資源釋放,因此也就產生了一個循環等待死鎖。

 

  (2)處理死鎖的方法

 

  • 忽略該問題,也即鴕鳥算法。當發生了什麼問題時,無論他,直接跳過,無視它;
  • 檢測死鎖並恢復;
  • 資源進行動態分配;
  • 破除上面的四種死鎖條件之一。

  8、線程相關類

 

(1)ThreadLocal

 

      ThreadLocal它並非一個線程,而是一個能夠在每一個線程中存儲數據的數據存儲類,經過它能夠在指定的線程中存儲數據,數據存儲以後,只有在指定線程中能夠獲取到存儲的數據,對於其餘線程來講則沒法獲取到該線程的數據。 即多個線程經過同一個ThreadLocal獲取到的東西是不同的,就算有的時候出現的結果是同樣的(偶然性,兩個線程裏分別存了兩份相同的東西),但他們獲取的本質是不一樣的。使用這個工具類能夠簡化多線程編程時的併發訪問,很簡潔的隔離多線程程序的競爭資源。

     對於多線程資源共享的問題,同步機制採用了「以時間換空間」的方式,而ThreadLocal採用了「以空間換時間」的方式。前者僅提供一份變量,讓不一樣的線程排隊訪問,然後者爲每個線程都提供了一份變量,所以能夠同時訪問而互不影響。ThreadLocal類提供了以下的三個public方法:

ThreadLocal()
          建立一個線程本地變量。
T get()
          返回此線程局部變量的當前線程副本中的值,若是這是線程第一次調用該方法,則建立並初始化此副本。
protected  T initialValue()
          返回此線程局部變量的當前線程的初始值。

     下面經過系統源碼來分析出現這個結果的緣由。 在ThreadLocal中存在着兩個很重要的方法,get()和set()方法,一個讀取一個設置。

複製代碼
    /**
    * Returns the value of this variable for the current thread. If an entry
    * doesn't yet exist for this variable on this thread, this method will
    * create an entry, populating the value with the result of
    * {@link #initialValue()}.
    *
    * @return the current value of the variable for the calling thread.
    */
    @SuppressWarnings("unchecked")
    public T get() {
       // Optimized for the fast path.
       Thread currentThread = Thread.currentThread();
       Values values = values(currentThread);
       if (values != null) {
            Object[] table = values.table;
            int index = hash & values.mask;
            if (this.reference == table[index]) {
              return (T) table[index + 1];
             }
        } else {
        values = initializeValues(currentThread);
      }
      return (T) values.getAfterMiss(this);
    }
    /**
    * Sets the value of this variable for the current thread. If set to
    * {@code null}, the value will be set to null and the underlying entry will
    * still be present.
    *
    * @param value the new value of the variable for the caller thread.
    */
    public void set(T value) {
         Thread currentThread = Thread.currentThread();
       Values values = values(currentThread);
       if (values == null) {
          values = initializeValues(currentThread);
       }
       values.put(this, value);
    }
複製代碼

     從註釋上能夠看出,get方法會返回一個當前線程的變量值,若是數組不存在就會建立一個新的。另外,對於「當前線程」和「數組」,數組對於每一個線程來講都是不一樣的 values.table。而values是經過當前線程獲取到的一個Values對象,所以這個數組是每一個線程惟一的,不能共用,而下面的幾句話也更直接了,獲取一個索引,再返回經過這個索引找到數組中對應的值。這也就解釋了爲何多個線程經過同一個ThreadLocal返回的是不一樣的東西。

     Java中爲何要這麼設置呢?

  • ThreadLocal在平常開發中使用到的地方較少,可是在某些特殊的場景下,經過ThreadLocal能夠輕鬆實現一些看起來很複雜的功能。通常來講,當某些數據是以線程爲做用域而且不一樣線程具備不一樣的數據副本的時候,就能夠考慮使用ThreadLocal。例如在Handler和Looper中。對於Handler來講,它須要獲取當前線程的Looper,很顯然Looper的做用域就是線程而且不一樣的線程具備不一樣的Looper,這個時候經過ThreadLocal就能夠輕鬆的實現Looper在線程中的存取。若是不採用ThreadLocal,那麼系統就必須提供一個全局的哈希表供Handler查找指定的Looper,這樣就比較麻煩了,還須要一個管理類。
  • ThreadLocal的另外一個使用場景是複雜邏輯下的對象傳遞,好比監聽器的傳遞,有些時候一個線程中的任務過於複雜,就可能表現爲函數調用棧比較深以及代碼入口的多樣性,這種狀況下,咱們又須要監聽器可以貫穿整個線程的執行過程。這個時候就可使用到ThreadLocal,經過ThreadLocal可讓監聽器做爲線程內的全局對象存在,在線程內經過get方法就能夠獲取到監聽器。若是不採用的話,可使用參數傳遞,可是這種方式在設計上不是特別好,當調用棧很深的時候,經過參數來傳遞監聽器這個設計太糟糕。而另一種方式就是使用static靜態變量的方式,可是這種方式存在必定的侷限性,拓展性並非特別的強。好比有10個線程在執行,就須要提供10個監聽器對象。

 

注:ThreadLocal和其餘全部的同步機制同樣,都是爲了解決多線程中對於同一變量的訪問衝突。值普通的同步機制中,經過對象加鎖來實現多線程對同一變量的安全訪問,且該變量是多線程共享的,全部須要使用這種同步機制來明確分開是在何時對變量進行讀寫,在何時須要鎖定該對象。此種狀況下,系統並無將這個資源複製多份,而是採起安全機制來控制訪問而已。ThreadLocal只是從另外一個角度解決多線程的併發訪問,即將須要併發訪問的資源複製多份,每一個線程擁有一份資源,每一個線程都有本身的資源副本。

總結:若多個線程之間須要共享資源,以達到線程間的通訊時,就使用同步機制;若僅僅須要隔離多線程之間的關係資源,則可使用ThreadLocal。

 

原文:https://www.cnblogs.com/snow-flower/p/6114765.html

相關文章
相關標籤/搜索