1. 程式人生 > >《Java編程思想》筆記 第二十一章 並發

《Java編程思想》筆記 第二十一章 並發

插入 tde tran 並發模式 html https 64位 target 不可用

1.定義任務

  • 實現Runnable 接口的類就是任務類(任務類不一定是實現Runnable接口的類)。
  1. 實現Runnable 接口,重寫run()方法,run方法的返回值只能是 void
  2. 任務類就是表示這個類要做什麽,run是任務執行的入口,可以在run中完成任務也可以run調用其他方法完成任務。
  3. run 方法不會產生一個線程,必須把任務附著到線程上,線程才會執行任務。
  4. 有時run中通常存在某種形式的循環,一直等待著接收信息準備執行任務,多見於服務器。

2.Thread類

  • 構造器接收一個 Runnable 對象,當線程啟動時會調用Runnable對象的run方法。
  1. Thread(Runnable target) 或者 傳入一個string 作為線程的名字 Thread(Runnable target,String name)
  2. 調用Thread對象的start( )方法啟動線程,它會自動調用Runnable的run( )方法。
  3. start方法調用之後會迅速返回,執行main線程後面的代碼,任務在啟動的線程中繼續執行。
  4. Thread對象和其他對象不同,Thread對象啟動線程後,線程任務未完成之前,即run 方法未退出之前,垃圾回收器不會回收它。
  5. 常用的一個方法currentThread() 返回當前線程對象的引用。

3.Executor

  • 執行器,用來管理Thread對象,只有一個方法 execute(Runnable command) 執行提交的任務。
  • 這是一個異步執行框架可以控制線程的啟動、執行和關閉,可以簡化並發編程的操作。
  1. ExecutorService 線程池,該接口實現了 Executor 他知道如何根據恰當的上下文來執行Runnable對象,即根據線程環境,類型有順序有條件的啟動。
    1. 線程的創建代價非常高昂,最好是提前創建幾個線程然後一直持有它,當執行完任務後不銷毀而是重新放回線程池內等待執行下一個任務。
    2. 調用execute方法可以往線程池中添加任務,這些任務由線程池中的線程去執行。
    3. 調用 shutdown 之後可以阻止線程池再加入線程。
  2. Executors 是一個工具類,他可以產生ExecutorService對象和一些線程相關的其他對象。  
    1. newCachedThreadPool() 創建一個線程池
    2. newFixedThreadPool(int i) 也可以創建一個固定線程數的程序池
    3. newSingleThreadExecutor() 創建一個沒有限制的隊列池,放入的任務會排隊執行,執行完一個任務才開始執行下一個任務。
  3. 線程池中的線程在可能情況下都會被復用,有個線程執行完一個任務會接著執行另一個任務。

4.從任務中產生返回值

  • Runnable 是執行工作的獨立任務,他不能有任何返回值。
  • 如果想要任務完成時可以返回一個值,那麽創建任務類時不用繼承Runnable 接口 而是實現 Callable 接口。
  1. Callable<V> 和 Runnable 接口使用方法一樣 V call()執行任務 並且返回一個V類型的值
  2. ExecutorService 的 submit() 會返回一個Future 對象。
           <T> Future<T> submit(Callable<T> task)
            返回Callable任務執行結果的狀態
            Future<?> submit(Runnable task)
            返回Runnable任務執行的狀態
            <T> Future<T> submit(Runnable task, T result)
            返回Runnable任務執行的狀態和 result 值 
  3. Future(接口)對象可以對Runnable和Callable任務進行取消、查詢結果是否完成、獲取結果。
  4. Future<V> 接口提供了一些方法用來對結果進行檢測
            boolean cancel(boolean mayInterruptIfRunning)
            嘗試取消執行此任務。
            V get()
            等待計算完成,然後檢索其結果。
            V get(long timeout, TimeUnit unit)
            如果在規定時間內得到結果就立刻返回,超時拋出TimeoutException
            boolean isCancelled()
            如果此任務在正常完成之前被取消,則返回 trueboolean isDone()
            如果任務已完成返回true。  完成可能是由於正常終止,異常或取消

    調用get 之前最好 isDone判斷一下,否則get將一直阻塞直到得到結果,或者使用超時get

      ExecutorService pool = Executors.newFixedThreadPool(1);
            Future<String> submit = pool.submit(new Callable<String>() {
                @Override
                public String call() throws InterruptedException {
                    Thread.sleep(5000);
                    return "時間到";
                }
            });
            pool.shutdown();
            System.out.println(new Date());
            System.out.println(submit.get());
            System.out.println(new Date());
    
    
    
    
    Wed Aug 22 17:31:02 CST 2018
    時間到
    Wed Aug 22 17:31:07 CST 2018
  5. FutureTask 類 構造器FutureTask(Callable<V> callable) FutureTask(Runnable runnable, V result) //Runnable 自身不返回結果,但可以設定返回一個V型值
    1. FutureTask實現了 RunnableTask接口,RunnableTask接口又實現了Runnable,Future接口
    2. FutureTask 也是 Future接口的唯一實現
    3. 因為FutureTask 實現了Runnable接口 所以Thread可以通過啟動FutureTask 啟動Callable任務
        FutureTask<String> futureTask = new FutureTask<String>(new Callable<String>() {
                  @Override
                  public String call() throws InterruptedException {
                      Thread.sleep(5000);
                      return "時間到";
                  }
              });
              Thread thread = new Thread(futureTask);
              thread.start();
              System.out.println(new Date());
              System.out.println(futureTask.get());
              System.out.println(new Date());

5.休眠

  • 終止任務一段時間
  1. 可以使用TimeUtil.MILLISECONDS.sleep(1000); 休眠1000毫秒,也可以使用其他時間單位,或者使用Thread.sleep(1000),單位只能是毫秒
  2. 對sleep調用要拋出InterruptedException異常並且在run,call 方法中捕獲,異常不能跨線程被捕獲。

6.優先級

  • 線程的優先級將線程的重要性傳遞給了調度器,調度器
  • CPU 處理線程集的順序不一定,但調度器傾向於讓優先級高的線程執行。傾向於並不是一定
  1. 優先權不會導致死鎖,優先權低的只是執行頻率較低,並不是不執行。
  2. 絕大多時間裏所有線程應該使用默認優先級,試圖操縱線程的優先級讓其先執行往往的不到效果,CPU不一定按優先級執行。
  3. Thread.currentThread().setPriority(int i) i 可以是1-10,未設置默認5,一般可以設置MAX_PRIORITY 最大10,或者MIN_PRIORITY最小1,NORM_PRIORITY中間5。
  4. 設置優先級一般在run 任務中第一句設置。
  5. Thread.yield() 建議具有相同優先級的其他線程執行,這只是一種建議,無法確保一定執行。

7.後臺線程

  • daemon, 程序進程在運行的時候在後臺提供一種通用服務的線程,並且不屬於不可獲取,程序可以沒有後臺線程,有時候需要後臺線程提供一些服務。
  1. 一個程序(進程)在運行的時候如果所有非後臺線程結束,該程序也就終止了,同時會殺死所有後臺線程,不管後臺線程是否執行完畢。
  2. 程序只要有一個非後臺線程沒有結束,程序就不會結束,main線程就是一個非後線程。
  3. 在一個線程調用start()之前調用 setDaemon(true) 可以設置該線程為後臺線程。
  4. isDaemon() 判斷一個線程是否為後臺線程.
  5. 後臺線程中創建的任線程都自動設置為後臺線程。
  6. 所有非後臺線程結束後,後臺線程立刻被殺死即使有finally語句也不會執行

8.ThreadFactory

  • 該接口可以設置線程的屬性,僅有一個方法Thread newThread(Runnable r)
  1. 實現ThreadFacory接口可以定制有由Executors創建的 線程池中的所有線程的屬性
  2. 由Executors創建線程池的靜態方法都被重載為一個可以接收ThreadFactory對象
     ExecutorService service = Executors.newCachedThreadPool(new DaemonThread(),);
            service.submit(new Prints());
    
    
    class DaemonThread implements ThreadFactory{
        @Override
        public Thread newThread(Runnable r) {
            Thread t  = new Thread(r);
            t.setDaemon(true);
            return t;
        }
    }

9.代碼的變體

  • 一般都是任務類實現Runable接口,再傳遞給線程Thread啟動,有時也可以用別的方法來替換這種方式。
  • 任務類不一定要從Runnable或者Callable接口實現,只要有run 或者call方法 並且線程能夠執行這兩個方法中,他們就統稱任務類。
  1. 任務類直接從Thread繼承:任務類直接繼承Thread,在構造器中調用start(),當創建任務類對象時就啟動了線程並執行任務
    lass SimpleThread extends Thread{
        SimpleThread(){
            start();
        }
    
        @Override
        public void run() {
            System.out.println("繼承Thread的任務類");
        }
    }
  2. 任務類實現Runnable,內置Thread對象並傳入任務類自己的引用this,並在構造器中啟動start()
    class SelfManaged implements Runnable{
        SelfManaged(){
            thread.start();
        }
        Thread thread = new Thread(this);
    
        @Override
        public void run() {
            System.out.println("內置Thread的任務類");
        }
    }
  3. 註意1、2都是在構造器中啟動線程,這意味著這個任務對象還未完全穩定線程就啟動了,有可能另一個線程會訪問這個不穩定的對象,這樣就存在隱患。顯示創建Thread就會存在這種問題,這也是優先使用Executor的原因。
  4. 可以通過內部類或者匿名內部類將線程代碼隱藏在類中或者方法中。

10.加入一個線程

  1. A線程調用B線程的join()方法,則A線程會被掛起直到B線程執行被中斷或者正常結束再繼續執行A。 技術分享圖片
    class A extends Thread{
    
        Thread b;
        A(Thread b){
           this.b = b;
            start();
        }
    
        @Override
        public void run() {
            System.out.println("A線程啟動");
            System.out.println("B線程加入");
            try {
                b.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("A線程結束");
        }
    }
    
    class B extends Thread{
    
       B(){
           start();
       }
    
        @Override
        public void run() {
            System.out.println("B線程執行");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("B結束執行");
        }
    }
    
    
    
       B b = new B();
          A a = new A(b);
    View Code
  2. 可以調用interrupt()中斷一個線程
    1. 調用interrupt()中斷一個線程後,此時線程會設置中斷標誌位 true
    2. 如果線程處於阻塞狀態被中斷那麽會拋出InterruptedException異常,此異常被捕獲之後會清理中斷標誌位。
    3. 調用isInterrupted() 可以查看中斷標誌位狀態。中斷標誌位在其他地方也有用,某些線程可能會檢查其他線程的中斷狀態。

11.捕獲異常

  1. 一般無法捕獲從線程逃逸出去的異常,run只能捕獲run方法catch中 的異常其他異常無法捕獲,一但異常逃出run方法那麽會直接輸出到控制臺。
    public void run() {
            try {
                Thread.sleep(5);
                System.out.println("Prints");
                throw  new RuntimeException(); //任何地方無法捕獲此異常
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
    Exception in thread "pool-1-thread-1" java.lang.RuntimeException
    at Prints.run(Main.java:46)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)
    
     

    由於Runnable的run方法沒有throws 異常所以run中沒被catch捕獲的異常不會拋給上一級所以就丟失了,而Callable 中 call throws Exception 所以拋出異常後被上一級捕獲, 通過Future get結果是可以的到異常信息。

  2. 需要捕獲逃出的異常可以修改Executor生產線程的方式來解決。
  3. 讓線程具備捕獲逃出run方法的異常就要使用ThreadFactory,讓一個線程經過ThreadFactory包裝後具有捕獲異常的方法。
  4. ThreadFactory中的Thread對象調用setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh) 方法可以設置 當出現未捕獲的異常導致突然中止時 自動調用哪個異常處理程序去處理異常。需要將實現的異常處理對象當做參數傳入。可以通過getUncaughtExceptionHandler() 的到這個異常處理程序的對象。
  5. Thread.UncaughtExceptionHandler是Thread類的一個內部接口,用來創建那些 處理未被捕獲的異常的對象。
  6. 可根據逃逸出的異常逐個設置處理程序,也可以setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh) 設置一個為默認處理程序。當專有處理程序沒有時自動調用默認處理程序。默認處理程序不用使用ThreadFactory包裝直接在當前線程設置即可 技術分享圖片
    public class Main {
    
        public static void main(String[] args) throws Exception {
           /* 設置默認處理程序
            Thread.setDefaultUncaughtExceptionHandler(new     MyUncaughtExceptionHandler());
            ExecutorService service = Executors.newCachedThreadPool();
        */
           // 專有處理程序
            ExecutorService service = Executors.newCachedThreadPool(new MyThreadFactory());
            service.execute(new Prints());
      
        }
    
    
    }
    
    
    class Prints implements Runnable {
    
        @Override
        public void run() {
            try {
                Thread.sleep(5);
                System.out.println("Prints");
                throw  new RuntimeException();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    class MyThreadFactory implements ThreadFactory{
        @Override
        public Thread newThread(Runnable r) {
            Thread t  = new Thread(r);
           t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
            return t;
        }
    }
    
    class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{
    
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            System.out.println(e);
        }
    }
    View Code

12.共享受限資源

  1. 多個線程同時訪問同一資源,有可能某個線程恰好在資源“不恰當”的狀態下問了它,自然的到的結果也是不正確的。
  2. 永遠不知道一個線程何時在運行。
  3. 並發模式解決線程沖突時都是采用 序列化訪問共享資源 的解決方案,這意味這某一時刻只能有一個任務訪問共享資源。
  4. 通常在代碼上加上鎖語句來實現 序列化訪問 這意味著某段時間只能有一個任務可以執行這段代碼,因為鎖產生了一種 相互排斥的效果 所以常常稱為互斥量。

13.synchronized

  • 同步鎖 。為防止資源沖突提供內置支持,保證原子性。
  1. 當任務要執行被synchronized保護的代碼片段時,會檢查它的鎖能不能用,然後獲取鎖執行代碼後釋放鎖。
  2. 共享資源一般指以對象形式存在的內存片段,要控制對內存片段訪問,首先把它置於某個對象中 再把所有要訪問這個資源的方法加synchronized.
  3. 所有對象都自動含有單一的鎖(監視器),當對象上任意一個synchronized方法被調用時,此方法被加鎖,對象中其他synchronized方法無法拿到該對象的鎖就無法進入方法執行代碼。
  4. 一個synchronized方法拿到鎖還沒釋放鎖之前,其他任何線程想要執行synchronized 方法都會被阻塞,因為同一對象中所有synchronized方法都共享同一把鎖,別的線程拿到了鎖,其他線程無法再拿到鎖。
  5. 把域設為private,只有通過方法才能訪問,synchronized就可以阻止其他對象同時訪問該域
  6. 一個任務可以多次獲得同一個對象的鎖。當一個synchronized方法調用了同一個對象的synchronized方法那麽鎖的數量就會 +1,執行完方法後鎖數量 -1。jvm負責跟蹤鎖的數量。
  7. 每個Class對象也有鎖,使用synchronized static 方法就可以控制住對類成員(static數據)的並發訪問。

14.何時使用synchronize

  • 何時使用同步,可以運用 Brian 同步規則
  1. 當你正在寫一個變量接下來可能被另一個線程讀取,或者正在讀取一個上一次已經被另一個線程修改過的變量,你就得使用同步,並且 讀寫線程都得使用相同的 對象的鎖 同步。
  2. 如果類中有超過一個方法處理共享數據那麽必須同步所有相關的方法。如果只同步一個有可能在其他線程中被同步方法調用。

15.顯示Lock對象

  • Lock(接口)對象必須被顯式的創建、鎖定、銷毀。
  1. 創建鎖對象 Lock lock = new ReentrantLock(); 或 ReentrantLock lock = new ReentrantLock(); 與synchronized有相同的作用,但也可以擴展功能。
  2. lock() 必須放置在要加鎖的方法的第一句,緊接著try - finally 語句在 finally中執行 unlock(),因為鎖必須要被釋放。
  3. synchronized 失敗時會拋出異常,沒有機會去做任何清理工作,而顯式的Lock鎖就可以使用finally 去做清理工作。
  4. ReentrantLock 的 tryLock() 方法是嘗試獲取鎖如果可以則獲得鎖並且返回true,tryLock(long timeout, TimeUnit unit)在規定時間內嘗試獲得鎖。synchronized 則沒有這個能力。

16.原子性與易變性

  • 原子操作:不能被線程機制中斷的操作,一旦操作開始那麽他一定可以在可能發生上下文切換(即切換到其他線程)之前執行完畢。
  1. 一個不正確的認知 : 原子操作不需要進行同步控制
  2. 除了 long 和 double 之外所有的基本數據類型的 簡單操作如 賦值 i = 2,返回 return i 這都是原子操作,其他的操作不一定是原子操作。
  3. jvm 將64位的操作如 double long 的讀寫分為2個32位的讀寫操作所以這就有可能產生了上下文切換,有時也稱 字撕裂

17.Java內存模型 JMM

  1. JMM : 所有數據都在主存中,每個線程從主存復制一份數據到自己的工作內存(高速緩存),所有對變量的操作都在自己的工作內存中,不能對主存直接操作,不同的線程不能訪問其他線程的工作內存。
  2. 數據的讀寫過程:從主存讀取數據到工作內存,CPU再從工作內存中加載數據計算後回寫到緩存,緩存再刷新到主存。

18.volatile

  • 保證了可見性,不能保證原子性
  1. 可見性:對一個volatile變量進行了寫操作,那麽所有讀操作都會看到這個修改,因為對volition變量修改之後會從緩存立刻刷回到主存。同時通知其他所有在緩存中的volition變量失效,重新從主存中讀取。
  2. 非volition變量的原子操作不會立刻刷回到主存,其他讀取該變量的線程也立刻看不到更新後的值
  3. long double 變量用volition修飾後 簡單的賦值,返回就是原子操作。
  4. 同步 也會 使工作內存失效,導致從主存重新獲取數據,使用了同步 就沒有必要使用volition
  5. 當一個值 依賴於它之前的值 或者受限制與於其他值,那麽使用volition之後就無法正常工作了得到正確結果,如 計數器 i++ (依賴於前一個值),多線程下i++會計算錯誤且比理論值小。參考博客

19.使用synchronized和volition的要求

  1. 如果一個變量與其他變量無關且對會被多個任務訪問,而且不在synchronized中那麽必須使用volition,如果有多個類似的值那麽都要使用volition
  2. 第一選擇synchronized,只有確保使用volition是安全的才使用。

20.原子類

  • SE5加入了 特殊的原子性變量,在java.util.concurrent.atomic.* 包中,以Atomic開頭
  • 對這些原子類的操作都有相應的原子操作方法
  • 這些類在性能調優是作用大,一般編程使用地方少
  • 其中一部分方法 技術分享圖片
     int addAndGet(int delta)
            將給定的值原子地添加到當前值。
            int decrementAndGet()
            減1再返回。
            int get()
            獲取當前值。
            int getAndAdd(int delta)
            將給定的值原子地添加到當前值。
            int getAndDecrement()
            返回當前值再減1。
            int getAndIncrement()
            返回當前值再加1。
            int getAndSet(int newValue)
            將原子設置為給定值並返回舊值。
            int getAndUpdate(IntUnaryOperator updateFunction)
            用應用給定函數的結果原子更新當前值,返回上一個值。
            int incrementAndGet()
            加1再返回。
    View Code

21.臨界區

  • 同步代碼塊。 希望防止多個線程同時訪問方法中的部分代碼,而不是防止整個方法。可以提高任務訪問對象的時間性能。
      public void println(String x) {
            synchronized (this) {
                print(x);
                newLine();
            }
        }

    進入代碼塊必須獲得synchronized 同步鎖

  1. 也可以使用Lock 來創建臨界區

22.其他對象上同步

  • synchronized 同步方法是拿到的鎖 是調用這個方法的當前對象的鎖,同步代碼塊可以使用別的對象的鎖。
  1. 使用同步代碼塊是要給synchronized傳遞一個對象來獲取這個對象的鎖
  2. 同步代碼塊獲得的鎖不同,則不同鎖之間的方法不受同步控制的影響,只有爭奪同一把鎖才可以達到互斥同步的要求
    public void print(){
            synchronized(new Object()){
                System.out.println("同步代碼塊");
            }
        }

    這是一個有問題的方法。不同線程new Object()肯定不是同一個對象,所以任何線程都能同時執行這段代碼,要想做到同步就要使用唯一不變的對象作為鎖。

  3. 如果鎖的對象時this,也就是當前對象那麽同步代碼塊拿到了鎖,任何synchronized方法誰都沒法獲得鎖。
     public void print(){
            synchronized(this){
                System.out.println("同步代碼塊");
            }
        }
        public synchronized void say(){
            System.out.println("同步方法");
        }

23.線程本地存儲

  • 防止任務在共享資源上產生沖突,一種解決辦法是同步,第二種辦法就是不共享。
  1. ThreadLocal<T> 類 為每個不同的線程創建一個存儲副本。
  2. 可以set (T value ) 往 本地線程存儲一個對象,使用 T get()取出對象。(只能存一個對象,因為set底層是個map,map的key是本地線程對象所在的線程,所以set第二個值會覆蓋第一個)
  3. ThreadLocal 不會出現競爭。因為每個線程的本地線程獨立的。
  4. ThreadLocal 在web開發中也應用廣泛,一個用戶的與服務器的交互就是一條線程,所以可以使用本地線程來存儲一下相關信息如用戶ID ,判斷用戶是否登錄可以在本地線程中去找用戶ID.

24.線程的狀態

  1. 線程的四種狀態

    1. 新建 new : 線程被創建時短暫處於這種狀態,此時已經分配了資源,執行了初始化有了資格獲得CPU時間,之後調度器會把這個線程轉變為可運行狀態或者阻塞。
    2. 就緒 Runnable :這種狀態下只要調度器把時間片分給線程,線程就開始運行。也就是說任意時刻線程可以運行也可以不運行,只要調度器能分配時間片給線程那麽線程就可以運行,這不同於死亡或者阻塞。
    3. 阻塞 Blocked : 線程可以運行但是有個條件阻止他運行。當線程處於阻塞狀態,調度器將忽略該線程,不會分配CPU時間給線程,除非他重新進入就緒狀態,他才有可能執行。
      1. 阻塞狀態說明 執行Thread.sleep(1000) 代碼 在1 秒內線程將被調度器忽略,1s 後進入就緒轉態而不是立刻執行,還要等調度器給他分配時間片才執行。
    4. 死亡 Dead : 處於死亡狀態的或者終止狀態的線程將不再時可調度的,並且也不會得到CPU的時間,這種狀態通常是從run方法返回,但任務線程還是可以被中斷的
  2. 進入阻塞狀態

    1. 調用sleep 使任務休眠
    2. 使用wait( ) 使線程掛起,直到的到notify()或者notifyAll( )消息進入就緒狀態。
    3. 任務正在等待某個輸入輸出
    4. 試圖獲得某個對象的鎖,但是鎖不可用,另一個線程已經的到了這個鎖。
  3. 中斷

    • 在中斷就是為了結束任務,但Java沒有提供中斷任務的機制而是給出了中斷標誌位,讓你自己去實現中斷。
    1. 每個線程都有一個中斷標誌,初始值時為false,調用Thread 的interrupt()方法就會將中斷標誌設為true。並且只有這個方法可以設置中斷標誌位為true
    2. 如果一個阻塞任務被中斷,那麽它會結束阻塞繼續執行任務再恢復中斷標誌位為false並且拋出InterruptedException 異常,這樣就要在run的catch中捕獲它。
    3. I/O 、synchronized鎖的阻塞任務不可被中斷,所以設為true之後也不會拋異常。可以通過關閉阻塞的底層資源中斷阻塞任務,但這時就會拋IO異常而不是InterruptedException異常。
    4. Lock使用lock()獲取鎖時的阻塞不可以被中斷,lockInterruptibly())獲取鎖時的阻塞可以被中斷,而且會拋出InterruptedException 異常。
    5. NIO類通道可以自動響應中斷,不必顯示關閉底層資源。
    6. 非阻塞任務在執行過程中中斷標誌被設為true,不會對之這個任務的執行有任何影響,除非你有檢測中斷標誌的代碼進行控制。
    7. Thread.interrupted() 會返回當前中斷標誌位,並且恢復到初始狀態false。
    8. isInterrupted( )也會返回當前中斷標誌位,但是不會去恢復中斷標誌 。
  4. 設置中斷標誌

    1. 一般的線程可以調用interrupt() 方法設置中斷標誌。
    2. 對使用Executor創建的線程要中斷其中某一條線程就要調用submit()啟動任務, 調用返回的Futuer的cancel(true)方法,傳遞參數true,它就會代用執行次任務線程的interrupt方法,並且返回阻塞任務的列表。
    3. Executor調用 shutdownNow() 會發送interrupt給它所有啟動的任務。
    4. Executor調用shutdown() 只是阻止往Executor添加任務,不會改變中斷標誌。
  5. 中斷檢查

    1. Thread.interrupted() 會返回當前中斷標誌位,並且恢復到初始狀態false。
    2. isInterrupted( )也會返回當前中斷標誌位,但是不會去恢復中斷標誌 。
  6. 優雅的處理中斷

    1. 當想處理一個中斷時可以使用Thread.interrupted() 或isInterrupted( )作為判斷條件來處理中斷。
    2. 拋出InterruptedException 異常來處理中斷,拋此異常說明這是一個阻塞方法。
    3. 查看如何處理InterruptedException 異常

25.線程之間的協作

  • 任務協作時關鍵的問題是這些任務之間的握手,為了實現握手,使用了相同的基礎特性:互斥。
  • 互斥 消除了競爭條件,在互斥上在添加一種途徑就是將自身掛起,直到某個條件達到,任務繼續執行。
  1. wait() 和 notifyAll()

    1. wait 使你可以等待某個條件發生變化,而改變這個條件超出了當前方法的控制能力,所以需要將任務掛起,等待滿足條件的通知到來
    2. wait 將任務掛起之後只有 notify()或者notifyAll()方法調用時才能喚醒任務。
    3. wait 掛起任務之後會釋放的到的鎖,而sleep、yield 則不會釋放鎖。正是因為這樣其他同步方法才可以被其他任務調用。
    4. 兩種wait 無參的只有通知才能被喚醒,帶參數的需要2個精確的參數 (毫秒,納秒) 超過參數時間或者得到通知才能被喚醒。
    5. wait 、notify()、notifyAll() 這都是Object 的方法因為他們要操作 鎖 ,鎖也是對象的一部分所以在Object中可以控制所有對象的鎖。
    6. wait 、notify()、notifyAll()因為要控制所,所以他們只能放在同步方法或者同步塊中。
    7. 在同步方法中調用wait它操作的鎖就是對象本身的鎖,在同步代碼塊在則操作代碼塊加鎖對象的鎖
    8. 沒有同步使用 wait 、notify()、notifyAll() 雖然編譯可以通過,但是運行會拋出IllegalMonitorStateException異常。
  2. wait() 、notify()、notifyAll()

    1. 首先這些方法要使用在共享對象上,因為他們要拿到共享對象的鎖,且只有 wait notify notifyAll 的鎖相同才能被喚醒。
    2. 不同的任務執行共享對象的方法時如果被掛起,就是該任務被掛起,然後釋放了鎖,其他任務就可以得到鎖然後執行任務。
    3. 其他任務在執行過程中達到條件 使用 notify 或者 notifyAll 喚醒剛剛被掛起的任務。
    4. 使用notifyAll 會喚醒所有wait的任務,notify只會喚醒一個,如果有多個wait時使用notify那就要確保喚醒的是正確的那一個,最好是多個wait任務在等待相同的條件,否則使用notify將喚醒不正確的任務。
    5. 任務在被真正喚醒之前首先要得到所在方法的鎖,如果得不到鎖那麽就不能被喚醒。
  3. 錯失信號

    • 要確保 wait 執行之前 另一個線程的 notify沒有執行,否則會錯失notify信號造成wait無限制等待,造成死鎖。

26.阻塞隊列

  • 使用 wait notifyAll 解決任務互操作是一種非常低級的方式,Java 並發包提供了阻塞隊列來解決任務互操作問題。
  • BlockQueue 阻塞隊列接口 有大量的實現來解決任務之間同步互操作的問題,無需考慮同步 掛起等問題,BlockQueue已經都為你做好。
  • 只需往BlockingQueue中put 隊列滿了會掛起,在可以put進去時就put進去,take也是一樣。
  1. BlockingQueue
            boolean add(E e)
            將指定的元素插入,不成功則拋出IllegalStateException。
            boolean offer(E e)
            將指定的元素插入,不成功則返回false。
            boolean offer(E e, long timeout, TimeUnit unit)
            將指定的元素規定時間內插入。
            void put(E e)
            將指定的元素插入到此隊列中,不成功則阻塞
            E poll(long timeout, TimeUnit unit)
            指定時間內取出頭,否則返回false
            boolean remove(Object o)
            從該隊列中刪除指定元素的單個實例(如果存在)
            E   take()
            取出隊頭元素,如果沒有則一直阻塞

      BlockingQueue部分實現

    1. SynchronousQueue 同步隊列,任何時刻都只允許一個任務插入或移除元素。他並沒有存儲能力,不算一個真正的隊列。
    2. ArrayBlockingQueue 創建時必須指定大小,看名稱也能知道內部是個數組
    3. LinkedBlockingQueue 無界隊列,可以存放大量的任務.
  2. BlockingQueue 的實現類的構造器 一些可以接受 boolean fair 參數,它指定了訪問的公平性。
    1.  默認 fair 為 false ,即 不公平訪問 就是 很多個put都被阻塞後如果可以有空位可以插入那麽不會安照阻塞先後順序插入。
    2. 公平訪問就是按照阻塞先後迅速插入,take也一樣。公平訪問會對性能有一定影響。
  3. 任務使用管道進行輸入輸出
    1.  這種通信多出現於阻塞隊列出現以前的Java代碼中。
    2. 一對管道流其實就是一個阻塞隊列。
    3. 管道流和普通I/O直接的區別就是管道阻塞可以被中斷。

27.死鎖

  1. 死鎖發生的四個條件
    1. 互斥條件 :資源中至少有一個是不能共享的。
    2. 不可剝奪性: 任務獲得的資源未使用完之前,別的任務不能強行奪走。
    3. 請求和保持條件:至少有一個任務持有一個資源且正在等待獲取一個當前正被別的任務持有的資源。
    4. 循環等待:若幹個任務形成一個首尾相接等待獲取資源的環
  2. 防止死鎖發生只需破壞四個條件中的一個即可。
    1. 最容易的辦法就是破壞循環等待條件。
    2. 可以用 初始化限制動作 達到破壞循環等待的條件

28.concurrent 類庫

  • SE5 加入 java.util.concurrent 包來解決辦法問題。
  1. CountDownLatch 發令槍

    1. CountDownLatch(int count) 計數不可逆。一個用來同步線程開始的工具類,可以使多個線程並行開始執行。也可以多個發令槍形成一條鏈分別控制獨立的任務,形成一個解決問題的鏈路。
      void await() 
              時當前任務掛起,直到計數器為0,自動喚醒執行。
              boolean await(long timeout, TimeUnit unit)
              設置超時時間
              void countDown()
              減少鎖存器的計數,每次減1,如果計數達到零,釋放所有等待的線程。
              long getCount()
              返回當前計數。
              String toString()
              返回一個標識此鎖存器的字符串及其狀態。 
  2. CyclicBarrier 同步屏障

    • 若幹個任務分別到達某一步驟前調用await()停下,當所有任務都到達時再同步開始執行。
    1. CyclicBarrier(int parties) 當 parties 個任務調用了await()時開始再開始執行接下來的步驟。
    2. CyclicBarrier(int parties, Runnable barrierAction) 當 parties 個任務調用了await()時執行barrierAction任務。
      int await()
              掛起線程
              int await(long timeout, TimeUnit unit)
              設置超時時間
              int getNumberWaiting()
              返回目前正在等待障礙的各方的數量。
              int getParties()
              返回所需要的await數量。
              boolean isBroken()
              查看處於wait的線程是否被中斷
              void reset()
              將屏障重置為初始狀態。
  3. DelayQueue 延遲隊列

    • 這是一個無界的BlockingQueue,用於放置實現了 Delayed 接口的對象。(其實就是一個Delayed的PriorityQueue)
    1. DelayQueue 內的Delayed對象只能在延遲期時間過後才能取出,隊頭元素一點定是到期時間最長的元素。比方某一時刻同時放入A,B 兩種元素A延遲期5s,B延遲期3s,那麽10s後隊頭就是元素B,因為他到期時間最長。
    2. 如果沒有元素到期,那麽pull就會拿到null,take自然就是阻塞。
    3. Delayed 接口

      1. 此接口只有一個方法 long getDelay(TimeUnit unit) 來返回 元素剩余的延遲時間。
        1. TimeUtil 是一個時間單位的枚舉類,它會根據傳入的時間單位,把剩余時間換算成對應單位的時間返回。
        2. 這句就表示過期時間:long expireTime = TimeUnit.NANOSECONDS.convert(saveTime, TimeUnit.SECONDS) + System.nanoTime();
        3. 返回剩余時間:return unit.convert(this.expireTime - System.nanoTime(), TimeUnit.NANOSECONDS);
        4. System.nanoTime() 表示隨機從某一時刻(同一線程這個時刻是固定的)到現在所經過的納秒數,所以有可能是負數,只適合用於計算時間段。
        5. System.currentTimeMillis()是1970.1.1 UTC 零點到現在的時間單位是毫秒,可以用來計算各種日期。
        6. 2個計算時間的方法都是返回一個long型值,顯示日期是因為底層經過了數據計算和格式化顯示。
      2. Delayed接口實現了Comparable接口,所以要創建Delayed 對象就要同時實現getDelay()和compareTo() 方法。
        1. getDelay() 將延遲到期時間剩余時間返回
        2. compareTo() 比較 延遲到期時間剩余時間 的大小。
  4. PriorityBlokckingQueue 優先級阻塞隊列

    • 這是一個很基礎的優先隊列,它具有阻塞 讀 的操作。
    1. 可以放入實現了Comparable接口的對象。
    2. 也可以構造器中傳入一個比較器Comparator,使用這個比較器進行排序。
    3. 可以限制隊列大小也可以不限制。
  5. ScheduledThreadPoolExecutor 定時任務

    void execute(Runnable command)
            執行 command零要求延遲。
            <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit)
            在到達延遲時間後執行任務並返回一個ScheduledFuture對象
            ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit)
            在到達延遲時間後執行任務並返回一個ScheduledFuture對象
        
  6. Semaphore 計數信號量

    • 正常鎖只允許一個任務訪問資源,Semaphore 可以允許若幹個任務同時訪問資源,可以看出是給任務發放一個訪問許可標簽。
              Semaphore(int permits)
              創建一個許可數量為permits的 Semaphore,默認非公平模式。
              Semaphore(int permits, boolean fair)
              創建一個許可數量為permits的 Semaphore,可設置是否是公平模式。
    1. Semaphore 使用方法和Lock類似 。
              void acquire()
              取許1個可證,獲取不到則一直阻塞直到可用,或線程為 interrupted 
              void acquire(int permits)
              取許permits個可證,獲取不到則一直阻塞直到可用,或線程為 interrupted
              void release()
              釋放1個許可證,將其返回到信號量。
              void release(int permits)
              釋放permits個許可證,將其返回到信號量。     
    2. 能不能訪問資源或者阻塞就看acquir能不能拿到需要的許可證,釋放的許可重新回到池裏被其他acquire獲取
  7. Exchanger<V> 交換者

    • V 是2個任務要交換的數據對象類型。
    • 該對象只有一個方法V exchange(V x) 或者設置超時時間V exchange(V x, long timeout, TimeUnit unit)
    • 當第一個任務調用 V exchange(V x) 就會掛起,等到第二個任務調用V exchange(V x) 2個任務就會交換x對象,所以V exchange(V x) 只能成對使用。
    • 第一個可以看成生產者,第二個可以看成消費者

29.性能調優

  1. concurrent類庫中的部分類適用於常規應用,如BlockingQueue,而部分類只適用於提高性能。
  2. Lock 通常比 synchronized 要高效許多,但synchronized可讀性好,再者ring如何退出互斥的開銷遠比方法內運算時間小很多,所以2者速度對外影響不大。
  3. 免鎖容器

    1. SE5 加入了新容器,通過更靈巧的技術來避免加鎖,從而提高線程安全的性能。
    2. 免鎖容器背後的策略就是:對容器的修改和讀取可以同時發生,只要讀取者能看到修改的結果即可,修改其實在容器的一個副本中進行的,副本在修改過程中是不可視的。
    3. CopyOnwriteArrayList : 往裏加元素時會創建一個副本,加成後把原來引用指向副本。好處是可以一邊加一邊讀。
    4. CopyOnwriteSet: 使用了CopyOnwriteArrayList 來實現。
    5. ConcurrentHashMap和ConcurrentHashMap 采用了同樣的思想。
    6. 從免鎖容器中讀要比synchronized對應讀快許多,因為加鎖釋放鎖的開銷省掉了。
  4. 樂觀鎖 : 假設不會發生並發沖突,只在提交的時候檢查數據有沒有被改變過(數據完整性)。Atomic類就是如此。
  5. 悲觀鎖: 假設會發生並發沖突,屏蔽一切可能違反數據完整性的操作,如synchronized。
  6. ReadWriterLock 一個特殊的鎖接口
    • 對於不頻繁寫但頻繁讀取的數據結構進行了優化。可以同時多個任務讀取,但是如果有寫入任務持有鎖,那麽所有讀都不可以。
    • 只有一個實現類 ReentrantReadWriteLock 。要想實現多讀的功能就可以使用 readLock()在讀取的前面加讀鎖,在寫入數據的前面加 writeLock()寫鎖
    • ReentrantReadWriteLock 還有不少方法,涉及公平性 決策性等等。

30.活動對象,一種解決並發競爭的方式

  1. 活動對象
    • 每個活動對象都有自己的線程
    • 每個對象都將維護對他自己的域的全部權利。
    • 所有活動對象間的通信都將以這些對象之間的消息形式發生。
    • 活動對象之間的消息都要排隊。
  2. 活動對象之間的消息都成了一個任務,而任務會被傳入一個List保存,然後逐個執行,任何時刻都只有一個調用,所以不會產生死鎖。

知識點

  1. 如果要保證 好幾個方法調用同一個對象,那麽最好給這個對象的引用加上 final 。
  2. Random.nextInt(i) 是線程安全的
  3. 線程的好處就是提供了輕量級的上下文切換(100)條以內,而不會重量級的上下文切換(上千條)。
  4. 進程內的線程共享相同的內存空間(線程獨享各自的工作內存),輕量級的上下文切換只改變程序的執行序列和局部變量,而進程切換(重量級上下文切換)必須改變所有內存空間。

《Java編程思想》筆記 第二十一章 並發