併發編程(一)之創建線程和線程中的常用方法

1. 進程線程

1.1 進程

        程序由指令和數據組成。但這些指令要運行,數據要讀寫,就必須將指令加載至CPU,數據加載至內存。在指令運行過程中還需要用到磁盤、網絡等設備,而進程就是用來加載指令、管理內存、管理IO的

程序和進程
        當一個程序被運行,從磁盤加載這個程序的代碼至內存。這時,就開啓了一個進程。進程(動態)可以視爲程序(靜態)的一個實例。大部分程序可以同時運行多個實例進程。如:記事本、瀏覽器;也有的程序只能啓動一個實例進程。如:任務管理器

1.2 線程

        一個進程可以分爲多個線程,而一個線程就是一個指令流,將指令流中的一條條指令以一定的順序交給CPU執行。

        在Java中,線程作爲最小調度單位,進程作爲資源分配的最小單位。

2. Java中的線程

在Java程序啓動時,都會創建一個線程----Main線程。

2.1 創建並運行線程

如果你想在Main線程之外創建其他線程,可以用如下方法。

  1. Thread
  2. Runnable接口
  3. FutureTask

2.1.1 Thread

// 創建線程 (使用了匿名內部類)
Thread t1 = new Thread() {
    public void run() {
		// 要執行的任務
	}
}
// 啓動線程
t1.start()

2.1.2 Runnable

Runnable r = new Runnable() {
	public void run() {
		// 要執行的任務
	}
}
// 創建線程對象
Thread t1 = new Thread(r, "t1");
// 啓動線程
t1.start();

Java8 以後可以使用 Lambda 表達式來精簡代碼。如:

Runnable r = () -> {
	// 要執行的任務
}
// 創建線程對象
Thread t1 = new Thread(r, "t1");
// 啓動線程
t1.start();

如果對 Lambda 表達式不瞭解的,可以看看這篇博客 手把手地帶你走進Lambda表達式之門

2.1.3 FutureTask

FutureTask 能夠接收 Callable 接口的參數,用來處理有返回結果的情況。

FutureTask<Integer> task = new FutureTask<>(() -> {
	// 要執行的任務
	return 666;
})
new Thread(task, "t1").start();

// 主線程阻塞,同步等待 task 執行完畢的結果
Integer result = task.get();

2.1.4 Thread與Runnable

查看 Thread 源碼:

  1. Thread 實現了 Runnable 接口
  2. Thread 有一個 Runnable 類型的成員變量 target

查看 Thread 的構造方法:

public Thread(Runnable target) {
   init(null, target, "Thread-" + nextThreadNum(), 0);
}

最終會調用:

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
    ...
    this.target = target;
    ...
}

將構造方法中的參數傳遞給了成員變量 target。

查看 Thread 中的 run() 方法:

public void run() {
    if (target != null) {
        target.run();
    }
}

如果 target != null,則運行 Runnable 接口中的 run() 方法;否則,直接運行Thread 中的 run() 方法。

方法1 中的原理:
        通過 匿名內部類實現的方式可以看做是 Thread 的子類(亦或繼承 Thread),子類重寫了父類中的 run() 方法。所以,最終運行的是子類中的方法

方法2 中的原理:
        把 Runnable 類型的變量作爲 Thread 的構造方法的參數,在此構造方法中,將此參數傳遞給成員變量 target,所以 target != null,最終會執行 Runnable 接口中的方法,而上述的 Lambda 表達式將 Runnable 接口中的 run() 方法進行重寫,所以,最終會執行 Lambda表達式。

2.2 棧與棧幀

        JVM是由堆、棧、方法區等組成,其中,棧內存是給線程用的,每個線程啓動後,JVM會爲它分配一個棧內存。而每個棧是由多個棧幀組成,對應着每次方法調用時所佔用的內存

2.3 上下文切換

因爲以下原因導致CPU不再執行當前的線程,轉而執行另一個線程的代碼:

  1. 線程的CPU時間片用完
  2. 垃圾回收
  3. 有更優先級的線程需要執行
  4. 線程自己調用了sleep()、wait()、lock() 方法等

3. 常用的方法

方法名 功能描述 注意
start 啓動一個新的線程,在新的線程中運行 run() 方法 start() 方法只是讓線程進入就緒狀態,裏面的代碼不一定就馬上執行(CPU時間片還沒分給它)。每個線程對象的 start() 方法只能調用一次,如果調用多次,會拋出異常IllegalThreadStateException
run 新的線程啓動後,會執行的代碼
join 等待線程運行結束
isInterrupted 判斷是否被打斷 不會清除打斷標記
interrupt 打斷線程 如果被打斷的線程正在sleep、wait、join 會導致被打斷的線程拋出異常 InterruptedException,並清除標記;如果打斷正在運行的線程,則會設置打斷標記;park 線程被打斷,也會設置打斷標記
interrupted 判斷當前線程是否被打斷 會清除打斷標記
sleep(n) 讓當前執行的線程休眠 n 毫秒,休眠時間讓出 CPU
yield 提示線程調度器讓出當前線程對 CPU 的使用

3.1 start()與run()

start() 方法表示啓動一個新的線程,run() 方法表示線程啓動後要執行的代碼,那能否能直接調用 run() 方法?

public static void main(String[] args) {
    Thread t1 = new Thread() {
        @Override
        public void run () {
            System.out.println("hello world" + "=====" +Thread.currentThread().getName());
        }
    };
    t1.run();
}

在這裏插入圖片描述
run() 方法還是可以執行,但並沒有創建新的線程,還是在 main 線程中執行的

3.2 sleep()

  1. 調用 sleep() 方法會讓當前線程從 Running 進入 Timed Waiting 狀態
  2. 其它線程可以使用 interrupt() 方法打斷正在睡眠的線程,這時,sleep() 方法會拋出 InterruptedException異常
public static void main(String[] args) throws Exception{
    Thread t1 = new Thread() {
        @Override
        public void run () {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                System.out.println("wake up");
            }
        }
    };
    t1.start();
    Thread.sleep(1000);
    t1.interrupt();
}

在這裏插入圖片描述

3.3 join()

看下這段代碼,打印 i 是什麼?

static int i = 1;

private static void test() throws Exception{
    Thread t1 = new Thread(() -> {
        try {
            Thread.sleep(1000);
            i = 100;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    t1.start();
    System.out.println("i = " + i);
}

public static void main(String[] args) throws Exception{
    test();
}

在這裏插入圖片描述
         流程分析:main 線程啓動,調用 test() 方法,開啓一個新的線程 t1,main 和 t1 線程都是在同時運行的,main 線程會先執行語句 “System.out.println("i = " + i);”,只有當 t1 線程休眠1s後,纔會將 i 賦值爲 100。

那麼,如何讓打印的 i 爲100呢?
只需要添加一個 join() 方法

t1.start();
t1.join();
System.out.println("i = " + i);

當 main 線程 執行到語句 “t1.join()”時,main 線程會一直等待着 t1 線程(t1 調用了 join()方法),直到 t1 線程運行結束後,main 線程纔會繼續執行。

3.4 interrupt()

3.4.1 打斷阻塞的線程

打斷 sleep、wait、join 的線程會清空打斷標記。以 sleep 爲例

public static void main(String[] args) throws Exception{
    Thread t1 = new Thread(() -> {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    t1.start();

    Thread.sleep(100);
    t1.interrupt();
    System.out.println("打斷標記:" + t1.isInterrupted());
}

3.4.2 打斷運行的線程

打斷運行的線程是不會清除標記的

3.4.3 應用

兩階段終止模式
在線程 t1 中如何優雅地終止線程 t2?

        先看看兩階段終止模式的一個應用場景----做一個系統的健康監控,如:定時地去監控CPU 的使用率、內存的使用率等。可以使用一個後臺的監控線程每隔2s不斷地進行記錄即可,當點擊停止按鈕時便不再監控了。

在這裏插入圖片描述
代碼實現:

public class TwoPhraseTermination {
	// 監控線程
    private Thread monitor;

    // 啓動監控線程
    public void start() {
        monitor = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();
                if (current.isInterrupted()) {
                    System.out.println("料理後事");
                    break;
                }
                try {
                    Thread.sleep(2000);
                    System.out.println("執行監控記錄");
                } catch (InterruptedException e) {
                    // 重新設置打斷標記
                    current.interrupt();
                }
            }
        });
        monitor.start();
    }

    public void stop() {
        monitor.interrupt();
    }

    public static void main(String[] args) throws Exception{
        TwoPhraseTermination termination = new TwoPhraseTermination();
        termination.start();
        Thread.sleep(5000);
        termination.stop();
    }
}

這就是實現了在一個 main 線程中優雅地終止了另一個線程。

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