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

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

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

併發的多面性

更快的執行

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

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

如果你有一臺多處理器的機器,那麼你就可以在這些處理器之間分佈多個任務,從而極大的提高吞吐量。但是,併發通常是提高在單處理器上的程序的性能。在單處理上的性能開銷要比多處理器上的性能開銷大很多,因爲這其中增加了線程切換(從一個線程切換到另外一個線程)的重要依據。表面上看,將程序的所有部分當作單個的任務運行好像是開銷更小一點,節省了線程切換的時間。

改進代碼的設計

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

基本的線程機制

併發編程使我們可以將程序劃分成多個分離的,獨立運行的任務。通過使用多線程機制,這些獨立任務中的每一項任務都由執行線程來驅動。一個線程就是進程中的一個單一的順序控制流。因此,單個進程可以擁有多個併發執行的任務,但是你的程序看起來每個任務都有自己的CPU一樣。其底層是切分CPU時間,通常你不需要考慮它。

定義任務

線程可以驅動任務,因此你需要一種描述任務的方式,這可以由 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() 你會看到有意思的輸出

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() 方法的比較,請參考下面這篇博客

(https://www.cnblogs.com/xiadongqing/p/9925567.html)

優先級

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

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個優先級,但是一般只有MAXPRIORITY,NORMPRIORITY,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編程思想》

https://www.javatpoint.com/join()-method

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

本文由博客一文多發平臺 OpenWrite 發佈!

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