掌握之併發編程-2.線程

掌握高併發、高可用架構

第二課 併發編程

從本課開始學習併發編程的內容。主要介紹併發編程的基礎知識、鎖、內存模型、線程池、各種併發容器的使用。

第二節 線程

併發編程 併發基礎 進程 線程 線程通信

上一節學習了進程和線程的關係,CPU和線程的關係。在程序開發過程中,最主要的還是線程,畢竟它是用來執行任務的。所以就需要知道,如何啓動和停止線程;線程的狀態;線程間如何通信。

線程的啓動
  1. 實現Runnable接口,然後當成Thread的構造參數生成線程對象,調用t.start()方法
public class MyThread implements Runnable {
    @Override
    public void run() {
        System.out.println("thread02");
    }

    public static void main(String[] args) {
        Thread t = new Thread(new MyThread());
        t.start();
    }
}

這是線程最本質的實現。Thread類實現了Runnable接口,在執行t.start()時,會調用Threadrun()方法,從而間接調用target.run()

Thread類實現Runnalbe接口:

public class Thread implements Runnable {
    private Runnable target;
    public void run() {
        if (target != null) {
            target.run();
        }
    }
}

2 繼承Thread類,然後調用start()方法

public class MyThread extends Thread {
    public void run() {
        System.out.println("thread01");
    }
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
    }
}

由於Thread實現了Runnable,所以繼承Thread來重寫run()方法的本質依然是實現Runnable接口的定義。此時,由於target對象爲null,所以Threadrun()方法不會執行target.run(),而是直接執行自定義的run()方法。

3 實現Callable接口,並通過FutureTask

public class MyCallable implements Callable<String> {

    @Override
    public String call() throws Exception {
        return null;
    }

    public static void main(String[] args) {
        MyCallable m = new MyCallable();
        FutureTask<String> f = new FutureTask<>(m);
        Thread t = new Thread(f);
        t.start();

        String result = f.get(); // 同步獲取任務執行結果
        System.out.println(result);
    }
}

由於FutureTask實現了RunnableTask接口,而RunnableTask又實現了RunnableFuture接口,因此在構造Thread時,FutureTask還是被轉型爲Runnable來使用了。

前兩種方法只能執行任務,而不能得到任務的結果;第三種方法可以通過FutureTaskget()方法同步的獲取任務結果。當任務執行中時,其會阻塞直到任務完成。

4 匿名內部類

public class DemoThread {

    public static void main(String[] args) {
        new Thread() {
            @Override
            public void run() {
                //...
            }
        }.start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                //...
            }
        }).start();
    }
}

5 Lambda表達式

public class Demo {

    public static void main(String[] args) {
        new Thread(() -> System.out.println("running")).start();
    }
}

6 線程池

public class MyThreadPool implements Runnable {
    @Override
    public void run() {
        // TODO
    }

    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();

        MyThreadPool m = new MyThreadPool();
        exec.execute(m);
    }
}

把任務的執行交給ExecutorService去處理,最終還是利用Thread創建線程。優點是線程的複用,省去了每個線程的創建和銷燬過程,從而提高效率。

7 定時器

public class MyTimer {

    public static void main(String[] args) {
        Timer t = new Timer();

        t.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                // TODO
            }
        }, 2000, 1000);
    }
}

TimerTask實現了Runnable接口,Timer內部有個TimerThread繼承了Thread,所以還是Thread+Runnable

線程的停止
  1. 當線程的run方法執行完成後,線程自動釋放資源進而終止。
  2. 在另外的線程中調用interrupt來中斷某個線程。這是線程間通信,我們後續再講
線程的狀態

先上圖(借用CSDN博主 潘建南 的圖)。

掌握之併發編程-2.線程

所以,線程的狀態一共有6種。下面咱們來詳細講解。

  1. 初始狀態 NEW

通過實現Runnable或繼承Thread得到一個線程類,並使用new創建出一個線程對象,就進入了初始狀態。此時,還未調用start方法。

  1. 運行狀態 RUNNABLE

JAVA中將 就緒(READY)和 運行中(RUNNING)兩種狀態統稱爲“運行”狀態。

就緒 READY:就是說線程有資格運行,但此時調度程序還未選擇線程。當以下行爲發生時,線程進入就緒狀態。

  • 調用線程的start方法
  • 當前線程的sleep結束
  • 其他線程join結束
  • 等待用戶輸入,但用戶輸入完畢
  • 線程拿到對象鎖
  • 當前線程的時間片用完了
  • 調用當前線程的yield方法

運行中 RUNNING:調度程序從就緒的線程池中選擇一個線程使其成爲當前線程,此時線程處於的狀態就是運行中。

  1. 阻塞 BLOCKED

阻塞狀態是線程在獲取對象的同步鎖synchorized時,因爲該鎖被其他線程佔用而放棄CPU使用權,暫時停止運行的狀態。此時的線程會被JVM放入鎖池中。

  1. 等待 WAITING

運行的線程執行wait()方法,會釋放線程佔用的所有資源,並進入等待池中。此時,線程是不能自動喚醒的,必須依靠其他線程調用notify()notifyAll()方法才能喚醒。

  1. 超時等待 TIMED_WAITING

運行的線程執行sleep()join()方法,或者發出I/O請求時的狀態。此時線程會放棄CPU使用權。當sleep()超時、join()等待線程終止或超時、I/O處理完畢時,重新轉入就緒。

  1. 終止 TERMINATED

線程執行完成或因異常而退出run方法體的狀態。

線程各個狀態之間的跳轉,可以仔細看圖。

線程間通信
  1. 通過共享變量通信

在共享對象的變量中設置信號量。線程A修改信號量的值,線程B根據信號量來做不同的處理。

  1. 通過wait()notify()notifyAll()來通信

JAVA要求wait()notify()notifyAll()必須在同步代碼塊中使用。就是說,必須要獲得對象鎖。所以wait()notify()notifyAll()經常和sychronized搭配使用。

執行了鎖定對象的wait()方法後,當前線程會釋放獲得的對象鎖,進入鎖定對象的等待池

在執行同步代碼塊的過程中,如果調用Thread.sleep()Thread.yield(),當前線程只是放棄CPU,並不會釋放對象鎖

JOIN

作用:讓 主線程 等待 子線程 執行完成再繼續運行。

// 主線程
public class Father extends Thread {
    public void run() {
        Son son = new Son();
        son.start();
        son.join();
        ...
    }
}

// 子線程
public class Son extends Thread {
    public void run() {
        ...
    }
}

在Father主線程中,先啓動Son子線程,然後調用son.join(),此時,Father主線程會一直等待,直到子線程執行完成,才能繼續運行。

分析源碼可以知道,JOIN的實現原理是:只要子線程是活動的,就一直觸發主線程的wait()方法,使其一直處於等待狀態。

public final void join() throws InterruptedException {
    join(0);
}

public final synchronized void join(long millis)
throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}
yield

調用yield()方法,意思是放棄CPU使用權,回到就緒狀態。

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