成爲高級程序員不得不瞭解的併發

到目前爲止,你學到的都是順序編程,順序編程的概念就是某一時刻只有一個任務在執行,順序編程當然可以解決不少問題,可是對於某種任務,若是可以併發的執行程序中重要的部分就顯得尤其重要,同時也能夠極大提升程序運行效率,享受併發爲你帶來的便利。可是,熟練掌握併發編程理論和技術,對於只會CRUD的你來講是一種和你剛學面向對象同樣的一種飛躍。html

正如你所看到的,當並行的任務彼此干涉時,實際的併發問題就會接踵而至。並且併發問題不是很難復現,在你實際的測試過程當中每每會忽略它們,由於故障是偶爾發生的,這也是咱們研究它們的必要條件:若是你對併發問題置之不理,那麼你最終會承受它給你帶來的損害。java

併發的多面性

更快的執行

速度問題聽起來很簡單,若是你想讓一個程序運行的更快一些,那麼能夠將其切成多個分片,在單獨的處理器上運行各自的分片:前提是這些任務彼此之間沒有聯繫。編程

注意:速度的提升是以多核處理器而不是芯片的形式出現的。多線程

若是你有一臺多處理器的機器,那麼你就能夠在這些處理器之間分佈多個任務,從而極大的提升吞吐量。**可是,併發一般是提升在單處理器上的程序的性能。**在單處理上的性能開銷要比多處理器上的性能開銷大不少,由於這其中增長了線程切換(從一個線程切換到另一個線程)的重要依據。表面上看,將程序的全部部分看成單個的任務運行好像是開銷更小一點,節省了線程切換的時間。併發

改進代碼的設計

在單CPU機器上使用多任務的程序在任意時刻仍舊只在執行一項工做,你肉眼觀察到控制檯的輸出好像是這些線程在同時工做,這不過是CPU的障眼法罷了,CPU爲每一個任務都提供了不固定的時間切片。Java 的線程機制是搶佔式的,也就是說,你必須編寫某種讓步語句纔會讓線程進行切換,切換給其餘線程。異步

基本的線程機制

併發編程使咱們能夠將程序劃分紅多個分離的,獨立運行的任務。經過使用多線程機制,這些獨立任務中的每一項任務都由執行線程來驅動。一個線程就是進程中的一個單一的順序控制流。所以,單個進程能夠擁有多個併發執行的任務,可是你的程序看起來每一個任務都有本身的CPU同樣。其底層是切分CPU時間,一般你不須要考慮它。ide

定義任務

線程能夠驅動任務,所以你須要一種描述任務的方式,這能夠由 Runnable 接口來提供,要想定義任務,只須要實現 Runnable 接口,並在run 方法中實現你的邏輯便可。性能

public class TestThread implements Runnable{

    public static int i = 0;

    @Override
    public void run() {
        System.out.println("start thread..." + i);
        i++;
        System.out.println("end thread ..." + i);
    }

    public static void main(String[] args) {
        for(int i = 0;i < 5;i++){
            TestThread testThread = new TestThread();
            testThread.run();
        }
    }
}
複製代碼

任務 run 方法會有某種形式的循環,使得任務一直運行下去直到再也不須要,因此要設定 run 方法的跳出條件(有一種選擇是從 run 中直接返回,下面會說到。)測試

在 run 中使用靜態方法 Thread.yield() 可使用線程調度,它的意思是建議線程機制進行切換:你已經執行完重要的部分了,剩下的交給其餘線程跑一跑吧。注意是建議執行,而不是強制執行。在下面添加 Thread.yield() 你會看到有意思的輸出this

public void run() {
  System.out.println("start thread..." + i);
  i++;
  Thread.yield();
  System.out.println("end thread ..." + i);
}
複製代碼

Thread 類

將 Runnable 轉變工做方式的傳統方式是使用 Thread 類託管他,下面展現了使用 Thread 類來實現一個線程。

public static void main(String[] args) {
  for(int i = 0;i < 5;i++){
    Thread t = new Thread(new TestThread());
    t.start();
  }
  System.out.println("Waiting thread ...");
}
複製代碼

Thread 構造器只須要一個 Runnable 對象,調用 Thread 對象的 start() 方法爲該線程執行必須的初始化操做,而後調用 Runnable 的 run 方法,以便在這個線程中啓動任務。能夠看到,在 run 方法尚未結束前,run 就被返回了。也就是說,程序不會等到 run 方法執行完畢就會執行下面的指令。

在 run 方法中打印出每一個線程的名字,就更能看到不一樣的線程的切換和調度

@Override
public void run() {
  System.out.println(Thread.currentThread() + "start thread..." + i);
  i++;
  System.out.println(Thread.currentThread() + "end thread ..." + i);
}
複製代碼

這種線程切換和調度是交由 線程調度器 來自動控制的,若是你的機器上有多個處理器,線程調度器會在這些處理器之間默默的分發線程。每一次的運行結果都不盡相同,由於線程調度機制是未肯定的。

使用 Executor

CachedThreadPool

JDK1.5 的java.util.concurrent 包中的執行器 Executor 將爲你管理 Thread 對象,從而簡化了併發編程。Executor 在客戶端和任務之間提供了一個間接層;與客戶端直接執行任務不一樣,這個中介對象將執行任務。Executor 容許你管理異步任務的執行,而無須顯示地管理線程的生命週期。

public static void main(String[] args) {
  ExecutorService service = Executors.newCachedThreadPool();
  for(int i = 0;i < 5;i++){
    service.execute(new TestThread());
  }
  service.shutdown();
}
複製代碼

咱們使用 Executor 來替代上述顯示建立 Thread 對象。CachedThreadPool 爲每一個任務都建立一個線程。注意:ExecutorService 對象是使用靜態的 Executors 建立的,這個方法能夠肯定 Executor 類型。對 shutDown 的調用能夠防止新任務提交給 ExecutorService ,這個線程在 Executor 中全部任務完成後退出。

FixedThreadPool

FixedThreadPool 使你可使用有限的線程集來啓動多線程

public static void main(String[] args) {
  ExecutorService service = Executors.newFixedThreadPool(5);
  for(int i = 0;i < 5;i++){
    service.execute(new TestThread());
  }
  service.shutdown();
}
複製代碼

有了 FixedThreadPool 使你能夠一次性的預先執行高昂的線程分配,所以也就能夠限制線程的數量。這能夠節省時間,由於你沒必要爲每一個任務都固定的付出建立線程的開銷。

SingleThreadExecutor

SingleThreadExecutor 就是線程數量爲 1 的 FixedThreadPool,若是向 SingleThreadPool 一次性提交了多個任務,那麼這些任務將會排隊,每一個任務都會在下一個任務開始前結束,全部的任務都將使用相同的線程。SingleThreadPool 會序列化全部提交給他的任務,並會維護它本身(隱藏)的懸掛隊列。

public static void main(String[] args) {
  ExecutorService service = Executors.newSingleThreadExecutor();
  for(int i = 0;i < 5;i++){
    service.execute(new TestThread());
  }
  service.shutdown();
}
複製代碼

從輸出的結果就能夠看到,任務都是挨着執行的。我爲任務分配了五個線程,可是這五個線程不像是咱們以前看到的有換進換出的效果,它每次都會先執行完本身的那個線程,而後餘下的線程繼續「走完」這條線程的執行路徑。你能夠用 SingleThreadExecutor 來確保任意時刻都只有惟一一個任務在運行。

從任務中產生返回值

Runnable 是執行工做的獨立任務,但它不返回任何值。若是你但願任務在完成時可以返回一個值 ,這個時候你就須要考慮使用 Callable 接口,它是 JDK1.5 以後引入的,經過調用它的 submit 方法,能夠把它的返回值放在一個 Future 對象中,而後根據相應的 get() 方法取得提交以後的返回值。

public class TaskWithResult implements Callable<String> {

    private int id;

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

    @Override
    public String call() throws Exception {
        return "result of TaskWithResult " + id;
    }
}

public class CallableDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executors = Executors.newCachedThreadPool();
        ArrayList<Future<String>> future = new ArrayList<>();
        for(int i = 0;i < 10;i++){

            // 返回的是調用 call 方法的結果
            future.add(executors.submit(new TaskWithResult(i)));
        }
        for(Future<String> fs : future){
            System.out.println(fs.get());
        }
    }
}
複製代碼

submit() 方法會返回 Future 對象,Future 對象存儲的也就是你返回的結果。你也可使用 isDone 來查詢 Future 是否已經完成。

休眠

影響任務行爲的一種簡單方式就是使線程 休眠,選定給定的休眠時間,調用它的 sleep() 方法, 通常使用的TimeUnit 這個時間類替換 Thread.sleep() 方法,示例以下:

public class SuperclassThread extends TestThread{

    @Override
    public void run() {
        System.out.println(Thread.currentThread() + "starting ..." );

        try {
            for(int i = 0;i < 5;i++){
                if(i == 3){
                    System.out.println(Thread.currentThread() + "sleeping ...");
                    TimeUnit.MILLISECONDS.sleep(1000);
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread() + "wakeup and end ...");
    }

    public static void main(String[] args) {
        ExecutorService executors = Executors.newCachedThreadPool();
        for(int i = 0;i < 5;i++){
            executors.execute(new SuperclassThread());
        }
        executors.shutdown();
    }
}
複製代碼

關於 TimeUnit 中的 sleep() 方法和 Thread.sleep() 方法的比較,請參考下面這篇博客

(www.cnblogs.com/xiadongqing…)

優先級

上面提到線程調度器對每一個線程的執行都是不可預知的,隨機執行的,那麼有沒有辦法告訴線程調度器哪一個任務想要優先被執行呢?你能夠經過設置線程的優先級狀態,告訴線程調度器哪一個線程的執行優先級比較高,"請給這個騎手立刻派單",線程調度器傾向於讓優先級較高的線程優先執行,然而,這並不意味着優先級低的線程得不到執行,也就是說,優先級不會致使死鎖的問題。優先級較低的線程只是執行頻率較低。

public class SimplePriorities implements Runnable{

    private int priority;

    public SimplePriorities(int priority) {
        this.priority = priority;
    }

    @Override
    public void run() {
        Thread.currentThread().setPriority(priority);
        for(int i = 0;i < 100;i++){
            System.out.println(this);
            if(i % 10 == 0){
                Thread.yield();
            }
        }
    }

    @Override
    public String toString() {
        return Thread.currentThread() + " " + priority;
    }

    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        for(int i = 0;i < 5;i++){
            service.execute(new SimplePriorities(Thread.MAX_PRIORITY));
        }
        service.execute(new SimplePriorities(Thread.MIN_PRIORITY));
    }
}
複製代碼

toString() 方法被覆蓋,以便經過使用 Thread.toString() 方法來打印線程的名稱。你能夠改寫線程的默認輸出,這裏採用了 Thread[pool-1-thread-1,10,main] 這種形式的輸出。

經過輸出,你能夠看到,最後一個線程的優先級最低,其他的線程優先級最高。注意,優先級是在 run 開頭設置的,在構造器中設置它們不會有任何好處,由於這個時候線程尚未執行任務。

儘管JDK有10個優先級,可是通常只有MAX_PRIORITY,NORM_PRIORITY,MIN_PRIORITY 三種級別。

做出讓步

咱們上面提過,若是知道一個線程已經在 run() 方法中運行的差很少了,那麼它就能夠給線程調度器一個提示:我已經完成了任務中最重要的部分,可讓給別的線程使用CPU了。這個暗示將經過 yield() 方法做出。

有一個很重要的點就是,Thread.yield() 是建議執行切換CPU,而不是強制執行CPU切換。

對於任何重要的控制或者在調用應用時,都不能依賴於 yield()方法,實際上, yield() 方法常常被濫用。

後臺線程

後臺(daemon) 線程,是指運行時在後臺提供的一種服務線程,這種線程不是屬於必須的。當全部非後臺線程結束時,程序也就中止了,**同時會終止全部的後臺線程。**反過來講,只要有任何非後臺線程還在運行,程序就不會終止。

public class SimpleDaemons implements Runnable{

    @Override
    public void run() {
        while (true){
            try {
                TimeUnit.MILLISECONDS.sleep(100);
                System.out.println(Thread.currentThread() + " " + this);
            } catch (InterruptedException e) {
                System.out.println("sleep() interrupted");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for(int i = 0;i < 10;i++){
            Thread daemon = new Thread(new SimpleDaemons());
            daemon.setDaemon(true);
            daemon.start();
        }
        System.out.println("All Daemons started");
        TimeUnit.MILLISECONDS.sleep(175);
    }
}
複製代碼

在每次的循環中會建立10個線程,並把每一個線程設置爲後臺線程,而後開始運行,for循環會進行十次,而後輸出信息,隨後主線程睡眠一段時間後中止運行。在每次run 循環中,都會打印當前線程的信息,主線程運行完畢,程序就執行完畢了。由於 daemon 是後臺線程,沒法影響主線程的執行。

可是當你把 daemon.setDaemon(true) 去掉時,while(true) 會進行無限循環,那麼主線程一直在執行最重要的任務,因此會一直循環下去沒法中止。

ThreadFactory

按須要建立線程的對象。使用線程工廠替換了 Thread 或者 Runnable 接口的硬鏈接,使程序可以使用特殊的線程子類,優先級等。通常的建立方式爲

class SimpleThreadFactory implements ThreadFactory {
  public Thread newThread(Runnable r) {
    return new Thread(r);
  }
}
複製代碼

Executors.defaultThreadFactory 方法提供了一個更有用的簡單實現,它在返回以前將建立的線程上下文設置爲已知值

ThreadFactory 是一個接口,它只有一個方法就是建立線程的方法

public interface ThreadFactory {

    // 構建一個新的線程。實現類可能初始化優先級,名稱,後臺線程狀態和 線程組等
    Thread newThread(Runnable r);
}
複製代碼

下面來看一個 ThreadFactory 的例子

public class DaemonThreadFactory implements ThreadFactory {

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r);
        t.setDaemon(true);
        return t;
    }
}

public class DaemonFromFactory implements Runnable{

    @Override
    public void run() {
        while (true){
            try {
                TimeUnit.MILLISECONDS.sleep(100);
                System.out.println(Thread.currentThread() + " " + this);
            } catch (InterruptedException e) {
                System.out.println("Interrupted");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService service = Executors.newCachedThreadPool(new DaemonThreadFactory());
        for(int i = 0;i < 10;i++){
            service.execute(new DaemonFromFactory());
        }
        System.out.println("All daemons started");
        TimeUnit.MILLISECONDS.sleep(500);
    }
}
複製代碼

Executors.newCachedThreadPool 能夠接受一個線程池對象,建立一個根據須要建立新線程的線程池,但會在它們可用時重用先前構造的線程,並在須要時使用提供的ThreadFactory建立新線程。

public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
  return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                60L, TimeUnit.SECONDS,
                                new SynchronousQueue<Runnable>(),
                                threadFactory);
}
複製代碼

加入一個線程

一個線程能夠在其餘線程上調用 join() 方法,其效果是等待一段時間直到第二個線程結束才正常執行。若是某個線程在另外一個線程 t 上調用 t.join() 方法,此線程將被掛起,直到目標線程 t 結束纔回復(能夠用 t.isAlive() 返回爲真假判斷)。

也能夠在調用 join 時帶上一個超時參數,來設置到期時間,時間到期,join方法自動返回。

對 join 的調用也能夠被中斷,作法是在線程上調用 interrupted 方法,這時須要用到 try...catch 子句

public class TestJoinMethod extends Thread{

    @Override
    public void run() {
        for(int i = 0;i < 5;i++){
            try {
                TimeUnit.MILLISECONDS.sleep(1000);
            } catch (InterruptedException e) {
                System.out.println("Interrupted sleep");
            }
            System.out.println(Thread.currentThread() + " " + i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TestJoinMethod join1 = new TestJoinMethod();
        TestJoinMethod join2 = new TestJoinMethod();
        TestJoinMethod join3 = new TestJoinMethod();

        join1.start();
// join1.join();

        join2.start();
        join3.start();
    }
}
複製代碼

join() 方法等待線程死亡。 換句話說,它會致使當前運行的線程中止執行,直到它加入的線程完成其任務。

線程異常捕獲

因爲線程的本質,使你不能捕獲從線程中逃逸的異常,一旦異常逃出任務的run 方法,它就會向外傳播到控制檯,除非你採起特殊的步驟捕獲這種錯誤的異常,在 Java5 以前,你能夠經過線程組來捕獲,可是在 Java5 以後,就須要用 Executor 來解決問題,由於線程組不是一次好的嘗試。

下面的任務會在 run 方法的執行期間拋出一個異常,而且這個異常會拋到 run 方法的外面,並且 main 方法沒法對它進行捕獲

public class ExceptionThread implements Runnable{

    @Override
    public void run() {
        throw new RuntimeException();
    }

    public static void main(String[] args) {
        try {
            ExecutorService service = Executors.newCachedThreadPool();
            service.execute(new ExceptionThread());
        }catch (Exception e){
            System.out.println("eeeee");
        }
    }
}
複製代碼

爲了解決這個問題,咱們須要修改 Executor 產生線程的方式,Java5 提供了一個新的接口 Thread.UncaughtExceptionHandler ,它容許你在每一個 Thread 上都附着一個異常處理器。Thread.UncaughtExceptionHandler.uncaughtException() 會在線程因未捕獲臨近死亡時被調用。

public class ExceptionThread2 implements Runnable{

    @Override
    public void run() {
        Thread t = Thread.currentThread();
        System.out.println("run() by " + t);
        System.out.println("eh = " + t.getUncaughtExceptionHandler());
      
      	// 手動拋出異常
        throw new RuntimeException();
    }
}

// 實現Thread.UncaughtExceptionHandler 接口,建立異常處理器
public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{

    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("caught " + e);
    }
}

public class HandlerThreadFactory implements ThreadFactory {

    @Override
    public Thread newThread(Runnable r) {
        System.out.println(this + " creating new Thread");
        Thread t = new Thread(r);
        System.out.println("created " + t);
        t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
        System.out.println("ex = " + t.getUncaughtExceptionHandler());
        return t;
    }
}

public class CaptureUncaughtException {

    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool(new HandlerThreadFactory());
        service.execute(new ExceptionThread2());
    }
}
複製代碼

在程序中添加了額外的追蹤機制,用來驗證工廠建立的線程會傳遞給UncaughtExceptionHandler,你能夠看到,未捕獲的異常是經過 uncaughtException 來捕獲的。

文章來源:

《Java編程思想》

www.javatpoint.com/join()-meth…

下面爲本身作個宣傳,歡迎關注公衆號 Java建設者,號主是Java技術棧,熱愛技術,喜歡閱讀,熱衷於分享和總結,但願能把每一篇好文章分享給成長道路上的你。 關注公衆號回覆 002 領取爲你特地準備的大禮包,你必定會喜歡並收藏的。

file
相關文章
相關標籤/搜索