Java并發總結

Java并發總結

1.多線程的優點

  • 資源利用率更好
  • 程序在某些情況下更簡單
  • 程序響應更快

2.創建線程

1.實現Runnable接口

new Thread(Runnable).start()

  • 可以避免由于java單繼承帶來的局限
  • 增強程序的健壯性,代碼能夠被多個線程共享,代碼和數據是=數據是獨立的
  • 適合多個相同程序代碼的線程區處理同意資源的情況

2.繼承Thread類

new MyThread().start()

注意:啟動線程的方式必須是start(),若是直接調用Thread.run()代碼也能執行,但是就變成了普通方法的調用了,并沒有啟動線程

3.線程狀態

1.線程狀態介紹

線程在一定條件下,狀態會發生變化。線程一共有以下幾種狀態:

  • 新建狀態(New):新創建了一個線程對象。

  • 就緒狀態(Runnable):線程對象創建后,其他線程調用了該對象的start()方法。該狀態的線程位于“可運行線程池”中,變得可運行,只等待獲取CPU的使用權。即在就緒狀態的進程除CPU之外,其它的運行所需資源都已全部獲得。

  • 運行狀態(Running):就緒狀態的線程獲取了CPU,執行程序代碼。

  • 阻塞狀態(Blocked):阻塞狀態是線程因為某種原因放棄CPU使用權,暫時停止運行。直到線程進入就緒狀態,才有機會轉到運行狀態。阻塞的情況分三種:

  • 等待阻塞:運行的線程執行wait()方法,該線程會釋放占用的所有資源,JVM會把該線程放入“等待池”中。進入這個狀態后,是不能自動喚醒的,必須依靠其他線程調用notify()或notifyAll()方法才能被喚醒,

  • 同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程占用,則JVM會把該線程放入“鎖池”中。

  • 其他阻塞:運行的線程執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該線程置為阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。

  • 死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命周期。

線程狀態圖

2.中斷機制

  • 可中斷的阻塞狀態:Thread.sleep(),Object.wait(),Thread.join(),ReenTrantLock.lockInterruptibly()。

  • 不可中斷的阻塞狀態:
    synchronized和I/O阻塞狀態

1.可以通過調用Thread對象的interrupt()方法來中斷線程,但是此方法只是將中斷標志設置為true標志,并不能直接中斷線程,若執行interrupt()方法時線程處于:

  • 未阻塞狀態,那么此時阻塞標志已經設為true,等到下一次阻塞狀態來臨時,就會直接拋出InterruptedException異常,但是只會被捕捉到,可以在catch塊內自行return來結束run方法,否則,只是異常被捕捉,線程仍然可以繼續往下執行

  • 可中斷的阻塞狀態,線程收到中斷信號后,會立即拋出InterruptedException, 同時會把中斷狀態置回為false。

  • 不可中斷的阻塞狀態,不拋出InterruptedException,也不會退出阻塞狀態

2.檢查中斷狀態:

  • 使用 Thread對象的isInterrupted()方法判斷中斷狀態,當調用了interrupt(),isInterrupted()返回true,一旦拋出中斷異常中斷標志被置為false,isInterrupted()返回false

  • Thread.interrupted()只是靜態方法,只用來判斷當前調用它的線程的中斷狀態,和Thread對象的isInterrupted不同的是,它在每次調用一定會將中斷狀態置為false

4.守護線程

Java中有兩類線程:

1.用戶線程:運行在前臺的線程
2.守護線程:運行在后臺的線程,并為前臺線程的運行提供便利服務(比如垃圾回收線程),當所有的用戶線程都結束了,那么守護線程也會結束,因為被守護者沒有了。因此,不要在守護線程中執行業務邏輯操作(比如對數據的讀寫等)。

  • 可以用Thread對象的setDaemon(true)方法設置當前線程為守護線程,但要在start()之前,否則無效.
  • 在守護線程中創建的子線程也是守護線程
  • 不是所有的應用都可以分配給守護線程來進行服務,比如讀寫操作或者計算邏輯
  • 后臺進程在不執行finally子句的情況下就會終止其run()方法

5.同步機制

1.原子性和可見性

  • 原子性不能被線程調度器中斷的操作,是不可分割的。由Java內存模型來直接保證的原子性變量操作包括read、load、assign、use、store、和write這六個,基本數據類型的訪問讀寫是具備原子性的(long和double)的非原子性協定例外),synchronized塊之間的操作也具備原子性

  • 可見性,Java允許多個線程保存共享成員變量的私有拷貝,等到進行完操作后,再賦值回主存(減少了同主存通信的次數,提高了運行的速度)。因此線程對變量的修改是互相不可見的,在賦值的時候就會發生覆蓋,這樣就引出一個問題-變量可見性因此,當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改,就稱這個變量具有可見性

2.volatile關鍵字

private volatile boolean value;  

volatile關鍵字具有可見性,被它修飾的變量不能被線程拷貝,即直接在主存讀和寫,保證了新值能立即刷新,每個線程時刻看到的都是最新值。因此,保證了多線程操作時變量的可見性,而不具有原子性,因為它不會阻塞線程,是一種稍弱的同步機制,要使volatile變量提供理想的線程安全(同時可見性和原子性),必須同時滿足下面兩個條件,否則要加鎖來保證原子性:

  • 對變量的寫操作不依賴于當前值。例如自增操作就依賴當前值,因為它是一個讀取-修改-寫入操作序列組成的組合操作,必須以原子方式執行,而 volatile 不能提供必須的原子特性
  • 該變量沒有包含在具有其他變量的不變式中。

3.synchronised關鍵字

采用synchronized修飾符實現的同步機制叫做互斥鎖機制,每一個對象都有一個monitor(鎖標記),只能分配給一個線程。當線程擁有這個鎖標記時才能訪問這個資源,沒有鎖標記便進入鎖池,因此叫做互斥鎖,synchronised同時具有可見性和原子性,原子性是因為鎖內的操作不可分割,可見性因為 入鎖(進入synchronized)會獲取主存中變量的最新值和出鎖(退出synchronized)會將變量的最新值刷新回主存

1.實例方法的同步與實例方法內的同步塊
實例方法內的synchronized是同步在某個實例對象上。即對象鎖

public class MyClass {
public synchronized void log1(String msg1, String msg2){
        //...
}
public void log2(String msg1, String msg2){
        synchronized(object){
            //...
        }
    }}

2.靜態方法的同步與靜態方法內的同步塊
靜態方法內的synchronized是同步在類對象上,鎖住的是整個類,即類鎖
public class MyClass {

public static synchronized void log1(String msg1, String msg2){
        //...
}
public void log2(String msg1, String msg2){
        synchronized(MyClass.class){
            //...
        }
    }
}

使用同步機制獲取互斥鎖的情況,進行幾點說明:

  • 一旦某個被某個線程獲取,那么其他所有在這個鎖(同一個對象或類)上競爭的線程都會阻塞,不管是不是同一個方法或代碼塊(仔細體會)。以對象鎖為例,假如有三個synchronized方法a,b,c,當線程A進入實例對象M中的方法a時,它便獲得了該M對象鎖,其他的線程會在M的所有的synchronized方法處阻塞,即在方法 a,b,c 處都要阻塞

  • 對象級別鎖,鎖住的是對象,有上面說的鎖的特性.

  • 類級別鎖,鎖住的是整個類,它用于控制對 static 成員變量以及 static 方法的并發訪問。有上面說的鎖的特性

  • 互斥是實現同步的一種手段,臨界區、互斥量和信號量都是主要的互斥實現方式。synchronized 關鍵字經過編譯后,會在同步塊的前后分別形成 monitorenter 和 monitorexit這兩個字節碼指令。根據虛擬機規范的要求,在執行 monitorenter指令時,首先要嘗試獲取對象的鎖,如果獲得了鎖,把鎖的計數器加 1,相應地,在執行 monitorexit 指令時會將鎖計數器減 1,當計數器為 0 時,鎖便被釋放了。由于synchronized同步塊對同一個線程是可重入的,一個線程可以多次獲得同一個對象的互斥鎖,要釋放相應次數的該互斥鎖,才能最終釋放掉該鎖。

4.顯示的Lock鎖

  • unlock()需放在finally子句中,try中必須有return,以確保unlock()不會過早的發生。

  • 顯示的Lock對象在加鎖和釋放鎖方面,相比synchronized,還賦予了更細粒度的控制力。

  • Lock對象必須被顯示的創建、鎖定和釋放。相比synchronized,代碼缺乏優雅性。

  • 在使用Lock鎖時,某些事物失敗拋出一個異常,可以使用finally去做清理工作,以維護系統使其處于良好的狀態,這是synchronized不具有的

  • ReentrantLock允許嘗試獲取但最終未獲取鎖,如果其他線程已經獲取這個鎖,可以決定離開去執行其他一些事情,而不是等待鎖被釋放。這是synchronized不具有的

5.synchronized 和 Volatile的比較

  • 在訪問volatile變量時不會執行加鎖操作,因此也不會使線程阻塞。

  • 加鎖機制既可以確保可見性又可以確保原子性,而 volatile 變量只能確保可見性。

  • 如果過度依賴volatile變量來控制狀態的可見性,通常會比使用鎖的代碼更脆弱。僅當volatile變量能簡化代碼的實現以及對同步策略的驗證時,才使用它

  • 在需要同步的時候,第一選擇應該是synchronized關鍵字,這是最安全的方式,

6.TheadLocal

ThreadLocal類的實例,即便被多個線程鎖共享,但是在每個線程當中都有一份私有拷貝,并且多個線程無法看到對方的值,即線程對于此變量的使用完全是在自己拷貝對象上。

private ThreadLocal myThreadLocal = new ThreadLocal();

ThreadLocal可以儲存任意對象

myThreadLocal.set("A thread local value");//存儲此對象的值
String threadLocalValue = (String) myThreadLocal.get();//讀取
ThreadLocal myThreadLocal1 = new ThreadLocal<String>();//泛型使用

7.死鎖

1.普通循環等待死鎖

如果在同一時間,線程A持有鎖M并且想獲得鎖N,線程B持有鎖N并且想獲得鎖M,那么這兩個線程將永遠等待下去,這種情況就是最簡單的死鎖形式。

public class DeadLock{
private final Object left = new Object();
private final Object right = new Object();

public void leftRight() throws Exception
{
    synchronized (left)
    {
        Thread.sleep(2000);
        synchronized (right)
        {
            System.out.println("leftRight end!");
        }
    }
}

public void rightLeft() throws Exception
{
    synchronized (right)
    {
        Thread.sleep(2000);
        synchronized (left)
        {
            System.out.println("rightLeft end!");
        }
    }
}
}

死鎖的四個必要條件:

  • 互斥條件:線程對所分配到的資源進行排他性使用,即在一段時間內,某資源只能被一個線程占用。如果此時還有其它線程請求該資源,則只能等待,直到占有該資源的線程用畢釋放。
  • 請求和保持條件:已經保持了至少一個資源,但是又提出新的資源請求,而資源已被其他線程占有,此時請求線程只能等待,但對自己已獲得的資源保持不放。
  • 不可搶占條件:線程已獲得的資源在未使用完之前不能被搶占,只能線程使用完之后自己釋放。
  • 循環等待條件:在發生死鎖時,一個任務等待其他任務所持有的資源,后者又在等待另一個任務所持有的資源,這樣一直下去,直到有一個任務所持有的資源,使得大家被鎖住。

避免死鎖的方式:
1、只在必要的最短時間內持有鎖,考慮使用同步語句塊代替整個同步方法;
2、設計時考慮清楚鎖的順序,盡量減少潛在的加鎖交互數量
3、既然死鎖的產生是兩個線程無限等待對方持有的鎖,我們可以使用Lock類中的tryLock方法去嘗試獲取鎖,這個方法可以指定一個超時時限,獲取鎖超時后會返回一個失敗信息,放棄取鎖。

2.重入鎖死

如果一個線程在兩次調用lock()間沒有調用unlock()方法,那么第二次調用lock()就會被阻塞,這就出現了重入鎖死。避免重入鎖死有兩個選擇:

  • 編寫代碼時避免再次獲取已經持有的鎖
  • 使用可重入鎖

8.多線程集合的安全使用

在 Collections 類中有多個靜態方法,它們可以獲取通過同步方法封裝非同步集合而得到的集合:
public static Collection synchronizedCollention(Collection c)
public static List synchronizedList(list l)
public static Map synchronizedMap(Map m)
public static Set synchronizedSet(Set s)
public static SortedMap synchronizedSortedMap(SortedMap sm)
public static SortedSet synchronizedSortedSet(SortedSet ss)

在多線程環境中,當遍歷當前集合中的元素時,希望阻止其他線程添加或刪除元素。安全遍歷的實現方法如下:

import java.util.*;  

public class SafeCollectionIteration extends Object {  
public static void main(String[] args) {  
    //為了安全起見,僅使用同步列表的一個引用,這樣可以確保控制了所有訪問  
    //集合必須同步化,這里是一個List  
    List wordList = Collections.synchronizedList(newArrayList());  

    //wordList中的add方法是同步方法,會自動獲取wordList實例的對象鎖  
    wordList.add("Iterators");  
    wordList.add("require");  
    wordList.add("special");  
    wordList.add("handling");  

    //獲取wordList實例的對象鎖,  
    //迭代時,此時必須阻塞其他線程調用add或remove等方法修改元素
    synchronized ( wordList ) {  
        Iterator iter = wordList.iterator();  
        while ( iter.hasNext() ) {  
            String s = (String) iter.next();  
            System.out.println("found string: " + s + ", length=" + s.length());  
        }  
    }  
}  
} 

大部分的線程安全類都是相對線程安全的,也就是我們通常意義上所說的線程安全,像Vector這種,add、remove方法都是原子操作,不會被打斷,但也僅限于此,如果有個線程在遍歷某個Vector、有個線程同時在add這個Vector,99%的情況下都會出現·ConcurrentModificationException·,也就是fail-fast機制

6.多線程協作

1.wait、notify、notifyAll的使用

wait():將當前線程置入休眠狀態,直到接到喚醒通知或被中斷為止。調用后當前線程立即釋放鎖。

notify():用來通知那些正在等待該對象的對象鎖的其他線程。如果有多個線程等待,則線程規劃器任意挑選出其中一個wait()狀態的線程來發出通知喚醒它。其他線程繼續阻塞,但是調用后當前線程不會立馬釋放該對象鎖,直到程序出鎖

notifyAll():notifyAll會使在該對象鎖上wait的所有線程統統退出wait的狀態(即全部被喚醒),待程序出鎖后,所有被喚醒的線程共同競爭該鎖,沒有競爭到鎖的線程會一直競爭(不是阻塞)

注意:在調用wait、notify、notifyAll方法之前,必須先獲得對象鎖,并且只能在synchronized代碼塊或者方法中調用。否則拋出IllegalMonitorStateException異常

總結:

  • 如果線程調用了對象的wait()方法,那么該線程便會處于該對象的 等待池 中,等待池中的線程不會去競爭該對象的鎖。

  • 如果線程調用了對象的notifyAll()方法(喚醒所有wait線程)或notify()方法(只隨機喚醒一個wait線程),被喚醒的的線程便會進入該對象的鎖池中,鎖池中的線程會去競爭該對象鎖。

  • 優先級高的線程競爭到對象鎖的概率大,假若某線程沒有競爭到該對象鎖,它還會留在鎖池中。

2.notify 通知的遺漏

當線程 A 還沒開始 wait 的時候,線程 B 已經 notify 了,這樣,線程 B 的通知是沒有任何響應的,當 線程B 退出 synchronized 代碼塊后,線程A 再開始 wait,便會一直阻塞等待。也就是說這個通知信號提前來了,沒有wait線程收到,因此丟失了信號,為了避免丟失信號,可以設置一個成員變量來標志信號。

public class MyWaitNotify{
MonitorObject myMonitorObject = new MonitorObject();

//一旦調用notify,則設為true表示喚醒信號發出來了,則設為false表示喚醒信號已經被其他某個線程消耗了,
boolean wasSignalled = false;
public void doWait(){
    synchronized(myMonitorObject){
    //自旋鎖,循環檢查,只有當為true時才表示有喚醒信號來了
    while(!wasSignalled){
             try{
               myMonitorObject.wait();
           } catch(InterruptedException e){...}
          }
       //clear signal and continue running.
         wasSignalled = false;
  }
 }
 public void doNotify(){
     synchronized(myMonitorObject){
        wasSignalled = true;
        myMonitorObject.notify();
   }
 }
}

如果有多個線程被notifyAll()喚醒,所有被喚醒的線程都會在while循環里檢查wasSignalled變量值,但是只有一個線程可以獲得對象鎖并且退出wait()方法并清除wasSignalled標志(設為false)。這時這個標志已經被第一個喚醒的線程消耗了,所以其余的線程會檢查到標志為false,還是會回到等待狀態。

3.字符串常量或全局對象作為鎖的隱患

字符串常量全局對象在不同的實例當中是同一個對象,即其實用的是同一把鎖。因此本來在不同實例對象的線程會互相干擾,例如在實例A中的線程調用notifyAll()可能會喚醒實例B當中的wait線程。因此應該避免使用這兩種對象作為監視器對象,而應使用每個實例中唯一的對象

String myMonitorObject = "";//相同String 常量賦值在內存當中只會有一份對象

4.生產者-消費者模型synchronized實現

生產者和消費者在同一時間段內共用同一存儲空間,生產者向空間里生產數據,而消費者取走數據。問題在于如何通過線程之間的協作使得生產和消費輪流進行。

package job_3;

import java.util.Random;

public class Datebuf {
private Integer i = 0;
private int result;
private Random random;

public Datebuf() {
    random = new Random();
}

public void sendData() {
    while (!Thread.interrupted()) {
        synchronized (this) {
            try {
                while (i != 0) {
                    this.wait();
                }
                i = random.nextInt(100);
                System.out.println("線程 " + Thread.currentThread().getName()
                        + "生產" + i);
                this.notify();
            } catch (InterruptedException e) {
                return;
            }
        }
    }
}

public void addData() {
    while (!Thread.interrupted()) {
        synchronized (this) {
            try {
                while (i == 0) {
                    this.wait();
                }
                result += i;
                System.out.println("線程 " + Thread.currentThread().getName()
                        + "消費" + i + "--result=" + result);
                i = 0;
                this.notify();
            } catch (InterruptedException e) {
                return;
            }
        }
    }
}

}

5.其他協調方法

1.join()
一個線程可以調用其他線程的join()方法,其效果是等待其他線程結束才繼續執行。如果某個線程調用t.join(),此線程將被掛起,直到目標線程t結束才恢復(即t.isAlive()為假)。

2.yield()
建議線程調度器讓其他具有相同優先級的線程優先運行,但只是建議,并不一定就是別的線程先運行了

7.線程池

1.ExecutorService介紹

ExecutorService的生命周期包括三種狀態:運行,關閉,終止。創建后便進入了運行狀態,當調用了 shutdown()方法時,便進入關閉狀態。

使用線程池的好處:

  • 降低資源消耗。通過重復利用已創的線程降低線程創建和銷毀在成的消耗
  • 提高響應速度。當任務到達的時候,不需要再去創建線程就能立即執行
  • 提高線程的可管理性。線程是稀缺資源,如果無限制的創建不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配、調優和監控

2.自定義線程池

public ThreadPoolExecutor (
            int corePoolSize, 
            int maximumPoolSize, 
            long keepAliveTime, 
            TimeUnit unit,
            BlockingQueue<Runnable> workQueue)
  • corePoolSize:線程池中所保存的核心線程數,包括空閑線程。

  • maximumPoolSize:池中允許的最大線程數

  • keepAliveTime:線程池中的空閑線程所能持續的最長時間,超過將被線程池移除,可以通過調大此值來提高線程的利用率。

  • unit:持續時間的單位

  • workQueue:任務執行前保存任務的隊列,僅保存由 execute 方法提交的 Runnable 任務。

    • ArrayBlockingQueue:是一個基于數組結構的有界阻塞隊列,此隊列按 FIFO(先進先出)原則對元素進行排序。

    • LinkedBlockingQueue:一個基于鏈表結構的阻塞隊列,此隊列按FIFO (先進先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個隊列。

    • SynchronousQueue:一個不存儲元素的阻塞隊列。每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處于阻塞狀態(緩沖區為1的生產者消費者模式)

    • PriorityBlockingQueue:一個具有優先級的無限阻塞隊列。

  • 飽和策略

    • AbortPolicy: 直接拋出異常。
    • CallerRunsPolicy:只用調用者所在線程來運行任務。
    • DiscardOldestPolicy:丟棄隊列里最近的一個任務,并執行當前任務。
    • DiscardPolicy:不處理,丟棄掉。
    • 當然也可以根據應用場景需要來實現RejectedExecutionHandler接口自定義策略。如記錄日志或持久

3.創建線程池

以下四個線程池底層都是調用了ThreadPoolExecutor的構造方法,所以它們主要只是參構造數設置上的差異,理解了它們的默認構造參數值就能明白它們的區別

newCachedThreadPool()

public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                            60L, TimeUnit.SECONDS,
                            new SynchronousQueue<Runnable>());
}
  • 直接提交策略SynchronousQueue,無界線程池(maximumPoolSize無限大),可以進行自動線程回收

newFixedThreadPool(int)

public static ExecutorService newFixedThreadPool(int nThreads){
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
  • 使用了LinkedBlockingQueue無界隊列(workQueue無限大),線程數當超過了coreSize,此隊列由于是鏈式則可以無限添加(資源耗盡,另當別論),永遠也不會觸發產生新的線程,且線程結束即死亡不會被重復利用。

newScheduledThreadPool(int)

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

延遲調用周期執行示例,表示延遲1秒后每3秒執行一次:

scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
           @Override
           public void run() {
System.out.println("delay 1 seconds, and excute every 3     seconds");
}
}, 1, 3, TimeUnit.SECONDS);
  • 定長調度型線程池,這個池子里的線程可以按 schedule 依次 delay 執行,或周期執行,這是特殊的DelayedWorkQueue的效果.

SingleThreadExecutor()

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService(
    new ThreadPoolExecutor(1, 1,0L, 
    TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
}
  • 單例線程,任意時間池中只能有一個線程,如果向該線程池提交了多個任務,這些任務將排隊

當試圖通過 excute 方法將一個 Runnable 任務添加到線程池中時,按照如下順序來處理:

ex=>start: 提交任務
co=>condition: 核心線程池滿了?
new=>operation: 創建新線程
queue=>condition: 緩沖隊列無法加入?
add=>operation: 加入緩沖隊列
bao=>operation: 飽和政策處理
max=>condition: 最大線程池滿了?
e=>end

ex->co
co(yes)->queue
co(no)->new
queue(yes)->max
queue(no)->add
max(yes)->bao
max(no)->new

4.幾種排隊的策略

  • 直接提交。緩沖隊列采用 SynchronousQueue,它將任務直接交給線程處理而不保持它們。如果不存在可用于立即運行任務的線程(即無空閑線程),則試圖把任務加入緩沖隊列將會失敗,因此會構造一個新的線程來處理新添加的任務,并將其加入到線程池中。直接提交通常要求無界maximumPoolSizes(Integer.MAX_VALUE) 以避免拒絕新提交的任務。newCachedThreadPool 采用的便是這種策略。

  • 無界隊列,典型的便是的LinkedBlockingQueue(鏈式),當線程數超過corePoolSize時,新的任務將在緩沖隊列無限排隊。因此,創建的線程就不會超過corePoolSize,maximumPoolSize的值也就無效了。當每個任務完全獨立于其他任務,即任務執行互不影響時,適合于使用無界隊列。newFixedThreadPool采用的便是這種策略。

  • 有界隊列。當使用有限的 maximumPoolSizes 時,有界隊列(一般緩沖隊列使用ArrayBlockingQueue,并制定隊列的最大長度)有助于防止資源耗盡,但是可能較難調整和控制,隊列大小和最大池大小需要相互折衷,需要設定合理的參數。

5.關閉線程池

  • shutdown:不可以再submit新的task,已經submit(分兩種,正在運行的和在緩沖隊列的)的將繼續執行。并interrupt()空閑線程。
  • shutdownNow:試圖停止當前正執行的task,清除未執行的任務并返回尚未執行的task的list。
  • 只要調用了這兩個關閉方法的其中一個,isShutdown方法就會返回true。當所有的任務都已關閉后,才表示線程池關閉成功,這時調用isTerminaed方法會返回true

6.Executor執行Runnable和Callable任務

Runnable
無返回值,無法拋出經過檢查的異常,通過execute方法添加

ExecutorService executorService = Executors.newSingleThreadExecutor();//創建線程池
executorService.execute(new TestRunnable());//添加任務

Callable
返回Future對象,獲取返回結果時可能會拋出異常,通過submit方法添加

package threadLearn;

import java.util.ArrayList;   
import java.util.List;   
import java.util.concurrent.*;   

public class CallableDemo{   
public static void main(String[] args){   
    ExecutorService executorService=Executors.newCachedThreadPool();   
    List<Future<String>> resultList = new ArrayList<Future<String>>();   
    //創建10個任務并執行   
        for (int i = 0; i < 10; i++){   
        //使用ExecutorService執行Callable類型的任務,并將結果保存在future變量中   
        Future<String> future = executorService.submit(new TaskWithResult(i));   
        //將任務執行結果存儲到List中   
        resultList.add(future);   
    }   
    //遍歷任務的結果   
    for (Future<String> fs : resultList){   
            try{   
                while(!fs.isDone()){//Future返回如果沒有完成,則一直循環等待,直到Future返回完成  
                    System.out.println("還沒完成");
                }
                System.out.println(fs.get());//打印各個線程(任務)執行的結果   
            }catch(InterruptedException e){   
                e.printStackTrace();   
            }catch(ExecutionException e){   
                e.printStackTrace();   
            }finally{   
                //啟動一次順序關閉,執行以前提交的任務,但不接受新任務  
                executorService.shutdown();   
            }   
    }   
}   
}   

class TaskWithResult implements Callable<String>{   
private int id;   

public TaskWithResult(int id){   
    this.id = id;   
}   

/**  
 * 任務的具體過程,一旦任務傳給ExecutorService的submit方法, 
 * 則該方法自動在一個線程上執行 
 */   
public String call() throws Exception {  
    System.out.println("call()方法被自動調用" + Thread.currentThread().getName());   
    Thread.sleep(1000);
    //該返回結果將被Future的get方法得到  
    return "call()方法被自動調用,任務返回的結果是:" + id + "-----" + Thread.currentThread().getName();   
    }   
} 

如果真正的結果的返回尚未完成,則get()方法會阻塞等待,可以通過調用 isDone()方法判斷 Future 是否完成了返回。

9.線程異常處理

由于線程的本質特性(可以理解不同的線程是平行空間),從某個線程中逃逸的異常是無法被別的線程捕獲的。一旦異常逃出任務的run()方法,就會向外傳向控制臺。Thread.UncaughtExceptionHandler是JavaSE5中的新接口,它允許在每個Thread對象上都附著一個異常處理器。Thread.UncaughtExceptionHandler.uncaughtException()會在線程因未捕獲的異常而臨近死亡時被調用。

Thread t = new Thread(r);
t.setUncaughtExceptionHandler(new  MyUncaughtExceptionHandler());

8.Lock 鎖與Condition

1.Lock鎖的介紹

Lock接口有3個實現它的類:ReentrantLock、ReetrantReadWriteLock.ReadLock和ReetrantReadWriteLock.WriteLock,即重入鎖、讀鎖和寫鎖。

  • lock 必須被顯式地創建、鎖定和釋放,為了使用更多的功能,一般用 ReentrantLock 為其實例化
  • 為了保證鎖最終一定會被釋放(可能會有異常發生),要把互斥區放在 try 語句塊內,并在finally語句塊中釋放鎖,尤其當有return語句時,return 語句必須放在try字句中,以確保unlock()不會過早發生,從而將數據暴露給第二個任務。

(1)ReentrantLock與synchronized的比較

  • 等待可中斷:當持有鎖的線程長期不釋放鎖時,正在等待的線程可以選擇放棄等待,改為處理其他事情,由synchronized產生的互斥鎖時,會一直阻塞,是不能被中斷的

  • 可實現公平鎖:多個線程在等待同一個鎖時,必須按照申請鎖的時間順序排隊等待,通過構造方法 ReentrantLock(ture)來要求使用公平鎖

  • 鎖可以綁定多個條件:ReentrantLock 對象可以同時綁定多個 Condition 對象(名曰:條件變量或條件隊列),我們還可以通過綁定 Condition 對象來判斷當前線程通知的是哪些線程(即與Condition對象綁定在一起的其他線程

(2)ReetrantLock的忽略中斷鎖和響應中斷鎖

忽略中斷鎖與 synchronized實現的互斥鎖一樣,不能響應中斷,而響應中斷鎖可以響應中斷。

ReentrantLock lock = new ReentrantLock();  
lock.lockInterruptibly();//獲取響應中斷鎖  

(3)讀寫鎖

用讀鎖來鎖定讀操作,用寫鎖來鎖定寫操作,這樣寫操作和寫操作之間會互斥,讀操作和寫操作之間會互斥,但讀操作和讀操作就不會互斥。

ReadWriteLock rwl = new ReentrantReadWriteLock();  
rwl.writeLock().lock()  //獲取寫鎖  
rwl.readLock().lock()  //獲取讀鎖 

2.生產者-消費者模型Lock與Condition實現

package job_3;
import java.util.Random;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class LockDatebuf implements Data {
private Integer i = 0;
private int result;
private Random random;
private ReentrantLock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public LockDatebuf() {
    random = new Random();
}

public void sendData() throws InterruptedException {
    while (!Thread.interrupted()) {
        lock.lockInterruptibly();
            try {
                while (i != 0) {
                    condition.await();
                }
                i = random.nextInt(100);
                System.out.println("線程 " + Thread.currentThread().getName()
                        + "生產" + i);
                condition.signal();
            } catch (InterruptedException e) {
                
            }finally{
                lock.unlock();
            }
    }
}

public void addData() throws InterruptedException {
    while (!Thread.interrupted()) {
        lock.lockInterruptibly();
            try {
                while (i == 0) {
                    condition.await();
                }
                result += i;
                System.out.println("線程 " + Thread.currentThread().getName()
                        + "消費" + i + "--result=" + result);
                i = 0;
                condition.signal();
            } catch (InterruptedException e) {
                
            }finally{
                lock.unlock();
            }
    }
}
}

9.并發新特性

1.CountDownLatch

可以讓一組任務必須在另一組任務全部結束后才開始執行,向CountDownLatch對象設置一個初始計數值,任何在這個對象上調用await()方法的線程都將阻塞,直至計數值子減為0。其他任務在結束其工作時,可以調用countDown()來減小這個計數值。CountDownLatch被設計為只觸發一次。

package threadLearn;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
    // 只觸發一次,計數值不能被重置
    int size = 5;
    CountDownLatch latch = new CountDownLatch(size);
    ExecutorService exec = Executors.newCachedThreadPool();
    exec.execute(new Waiting(latch,"wait線程"));
    for (int i = 0; i < size; i++) {
        exec.execute(new OtherTask(latch));
    }
    TimeUnit.SECONDS.sleep(1);
    exec.shutdown();
}
}

class Waiting implements Runnable {
    private CountDownLatch latch;
    private String name;
    public Waiting(CountDownLatch latch,String name) {
        this.latch = latch;
        this.name = name;
    }
    public void run() {
        try {
            latch.await();
            System.out.println(name+"最后執行的任務...");
        } catch (Exception e) {
            return;
        }
    }
}

class OtherTask implements Runnable {
    private CountDownLatch latch;
    private Random rand = new Random();
    public OtherTask(CountDownLatch latch) {
        this.latch = latch;
    }

    public void run() {
        try {
            doWork();
            latch.countDown();
        } catch (Exception e) {
            return;
        }
    }

    private void doWork() throws InterruptedException {
        TimeUnit.MICROSECONDS.sleep(1000);
        System.out.println(Thread.currentThread().getName() + " 執行完畢");
    }
}

2.障礙器 CyclicBarrier

它適用于這樣一種情況:你希望創建一組任務,它們并發地執行工作,另外的一個任務在這一組任務并發執行結束前一直阻塞等待,直到該組任務全部執行結束,這個任務才得以執行。這非常像CountDownLatch,只是 CountDownLatch是只觸發一次的事件,而CyclicBarrier可以多次重用

package threadLearn;
import java.util.concurrent.BrokenBarrierException;   
import java.util.concurrent.CyclicBarrier;   
public class CyclicBarrierTest {   
    public static void main(String[] args) {   
            //創建CyclicBarrier對象,  
            //并設置執行完一組5個線程的并發任務后,再執行MainTask任務  
            CyclicBarrier cb = new CyclicBarrier(5, new MainTask());   
            new SubTask("A", cb).start();   
            new SubTask("B", cb).start();   
            new SubTask("C", cb).start();   
            new SubTask("D", cb).start();   
            new SubTask("E", cb).start();  
    }   
}   

/**  
* 最后執行的任務 
*/   
class MainTask implements Runnable {   
    public void run() {   
            System.out.println("......終于要執行最后的任務了...    ...");   
    }   
}   

/**  
* 一組并發任務  
*/   
class SubTask extends Thread {   
    private String name;   
    private CyclicBarrier cb;   

    SubTask(String name, CyclicBarrier cb) {   
            this.name = name;   
            this.cb = cb;   
    }   

    public void run() {   
            System.out.println("[并發任務" + name + "]  開始執行");   
            for (int i = 0; i < 999999; i++) ;    //模擬耗時的任務   
            System.out.println("[并發任務" + name + "]  開始執行完畢,通知障礙器");   
            try {   
                    //每執行完一項任務就通知障礙器   
                    cb.await();   
            } catch (InterruptedException e) {   
                    e.printStackTrace();   
            } catch (BrokenBarrierException e) {   
                    e.printStackTrace();   
            }   
    }   
}  

3.信號量Semaphore

信號量 Semaphore 實際上是一個功能完畢的計數信號量,從概念上講,它維護了一個許可集合,對控制一定資源的消費與回收有著很重要的意義。Semaphore 可以控制某個資源被同時訪問的任務數,它通過acquire()獲取一個許可,release()釋放一個許可。如果被同時訪問的任務數已滿,則其他 acquire 的任務進入等待狀態,直到有一個任務被release掉,它才能得到許可。Semaphore 僅僅是對資源的并發訪問的任務數進行監控,而不會保證線程安全,因此,在訪問的時候,要自己控制線程的安全訪問。

10.性能調優

(1)比較各類互斥技術

  • Atomic類
    不激烈情況下,性能比synchronized略遜,而激烈的時候,也能維持常態。激烈的時候,Atomic的性能會優于ReentrantLock一倍左右。缺點是只能同步一個值,一段代碼中只能出現一個Atomic的變量,多于一個同步無效。因為他不能在多個Atomic之間同步。

  • 關鍵字synchronized
    在資源競爭不是很激烈的情況下,Synchronized的性能要優于ReetrantLock,原因在于,編譯程序通常會盡可能的進行優化synchronize

  • Lock
    在資源競爭很激烈的情況下,Synchronized的性能會下降幾十倍,但是ReetrantLock的性能能維持常態。ReentrantLock提供了多樣化的同步,比如有時間限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。ReentrantLock擁有Synchronized相同的并發性和內存語義,此外還多了 鎖投票,定時鎖等候和中斷鎖等候。可以被中斷。

(2)免鎖容器

CopyOnWiteArrayList的寫入將導致創建整個底層數組的副本,而原數組將保留在原地,使得復制的數組在被修改時,讀取操作可以安全的執行。當修改完成時,一個原子性的操作把新的數組換入,使得新的讀取操作可以看到這個新的修改。

  • 好處是當多個迭代器同時遍歷和修改這個列表時,不會拋出ConcurrentModificationException。
  • CopyOnWriteArraySet將使用CopyOnWriteArrayList來實現其免鎖行為。
  • ConcurrenHashMap和ConcurrentLinkedQueue使用了類似的技術,允許并發的讀取和寫入,但是容器中只有部分內容而不是整個容器可以被復制和修改。在修改完成之前,讀取者仍舊不能看到他們。

(3)ReadWriteLock

對向數據結構相對不頻繁的寫入,但是有多個任務要經常讀取這個數據結構的這類情況進行了優化。ReadWriteLock使得你可以同時有多個讀者,只要他們都不試圖寫入即可。如果寫鎖已經被其他任務持有,那么任何讀者都不能訪問,直至這個寫鎖被釋放為止。即適用于讀者多于寫者的情況。

對于ReadWriteLock的應用主要是:緩存和提高對數據結構的并發性。

  • 鎖降級:重入還允許從寫入鎖降級為讀取鎖,其實現方式是:先獲取寫入鎖,然后獲取讀取鎖,最后釋放寫入鎖。但是,從讀取鎖升級到寫入鎖是不可能的。

  • 鎖獲取的中斷:讀取鎖和寫入鎖都支持鎖獲取期間的中斷。

  • Condition 支持 :寫入鎖提供了一個Condition實現,對于寫入鎖來說,該實現的行為與 ReentrantLock.newCondition() 提供的 Condition 實現對 ReentrantLock 所做的行為相同。當然,此 Condition 只能用于寫入鎖。
    讀取鎖不支持 Condition,readLock().newCondition() 會拋出 UnsupportedOperationException。

  • 重入:此鎖允許 reader 和 writer 按照 ReentrantLock 的樣式重新獲取讀取鎖或寫入鎖。在寫入線程保持的所有寫入鎖都已經釋放后,才允許重入 reader 使用它們。

下面的代碼展示了如何利用重入來執行鎖降級:

class CachedData {
Object data;
volatile boolean cacheValid;
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

void processCachedData() {
    rwl.readLock().lock();
    if (!cacheValid) {
    //在獲取寫鎖之前必須釋放讀鎖
        rwl.readLock().unlock();
        rwl.writeLock().lock();
        // 重新檢查狀態,因為可能其他線程已經獲取到讀鎖了
        if (!cacheValid) {
            data = ...
            cacheValid = true;
        }
        rwl.readLock().lock();
        rwl.writeLock().unlock(); 
    }
    use(data);
    rwl.readLock().unlock();
}

}

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容