線程的基本概念

進程和線程算是操作系統內兩個很基本、很重要的概念了,進程是操作系統中進行保護和資源分配的基本單位,操作系統分配資源以進程爲基本單位。而線程是進程的組成部分,它代表了一條順序的執行流。

進程從操作系統獲得基本的內存空間,所有的線程共享着進程的內存地址空間。當然,每個線程也會擁有自己私有的內存地址範圍,其他線程不能訪問。

由於所有的線程共享進程的內存地址空間,所以線程間的通信就容易的多,通過共享進程級全局變量即可實現。

同時,在沒有引入多線程概念之前,所謂的『併發』是發生在進程之間的,每一次的進程上下文切換都將導致系統調度算法的運行,以及各種 CPU 上下文的信息保存,非常耗時。而線程級併發沒有系統調度這一步驟,進程分配到 CPU 使用時間,並給其內部的各個線程使用。

在分時系統中,進程中的每個線程都擁有一個時間片,時間片結束時保存 CPU 及寄存器中的線程上下文並交出 CPU,完成一次線程間切換。當然,當進程的 CPU 時間使用結束時,所有的線程必然被阻塞。

JAVA 對線程概念的抽象

JAVA API 中用 Thread 這個類抽象化描述線程,線程有幾種狀態:

NEW:線程剛被創建
RUNNABLE:線程處於可執行狀態
BLOCKED、WAITING:線程被阻塞,具體區別後面說
TERMINATED:線程執行結束,被終止
其中 RUNNABLE 表示的是線程可執行,但不代表線程一定在獲取 CPU 執行中,可能由於時間片使用結束而等待系統的重新調度。BLOCKED、WAITING 都是由於線程執行過程中缺少某些條件而暫時阻塞,一旦它們等待的條件滿足時,它們將回到 RUNNABLE 狀態重新競爭 CPU。

此外,Thread 類中還有一些屬性用於描述一個線程對象:

private long tid:線程的序號
private volatile char name[]:線程的名稱
private int priority:線程的優先級
private boolean daemon = false:是否是守護線程
private Runnable target:該線程需要執行的方法
其中,tid 是一個自增的字段,每創建一個新線程,這個 id 都會自增一。優先級取值範圍,從一到十,數值越大,優先級越高,默認值爲五。

Runnable 是一個接口,它抽象化了一個線程的執行流,定義如下:

public interface Runnable {

public abstract void run();

}
通過重寫 run 方法,你也就指明瞭你的線程在得到 CPU 之後執行指令的起點。我們一般會在構造 Thread 實例的時候傳入這個參數。

創建並啓動一個線程

創建一個線程基本上有兩種方式,一是通過傳入 Runnable 實現類,二是直接重寫 Thread 類的 run 方法。我們詳細看看:

1、自定義 Runnable 實現

public class MyThread implements Runnable{
@Override
public void run(){
System.out.println(“hello world”);
}
}
public static void main(String[] args) {
Thread thread = new Thread(new MyThread());
thread.start();
System.out.println(“i am main Thread”);
}
運行結果:

i am main Thread
hello world
其實 Thread 這個類也是繼承 Runnable 接口的,並且提供了默認的 run 方法實現:

@Override
public void run() {
if (target != null) {
target.run();
}
}
target 我們說過了,是一個 Runnable 類型的字段,Thread 構造函數會初始化這個 target 字段。所以當線程啓動時,調用的 run 方法就會是我們自己實現的實現類的 run 方法。

所以,自然會有第二種創建方式。

2、繼承 Thread 類

既然線程啓動時會去調用 run 方法,那麼我們只要重寫 Thread 類的 run 方法也是可以定義出我們的線程類的。

public class MyThreadT extends Thread{
@Override
public void run(){
System.out.println(“hello world”);
}
}
Thread thread = new MyThreadT();
thread.start();
效果是一樣的。

幾個常用的方法

關於線程的操作,Thread 類中也給我們提供了一些方法,有些方法還是比較常用的。

1、sleep

public static native void sleep(long millis)
這是一個本地方法,用於阻塞當前線程指定毫秒時長。

2、start

public synchronized void start()
這個方法可能很多人會疑惑,爲什麼我通過重寫 Runnable 的 run 方法指定了線程的工作,但卻是通過 start 方法來啓動線程的?

那是因爲,啓動一個線程不僅僅是給定一個指令開始入口即可,操作系統還需要在進程的共享內存空間中劃分一部分作爲線程的私有資源,創建程序計數器,棧等資源,最終纔會去調用 run 方法。

3、interrupt

public void interrupt()
這個方法用於中斷當前線程,當然線程的不同狀態應對中斷的方式也是不同的,這一點我們後面再說。

4、join

public final synchronized void join(long millis)
這個方法一般在其他線程中進行調用,指明當前線程需要阻塞在當前位置,等待目標線程所有指令全部執行完畢。例如:

Thread thread = new MyThreadT();
thread.start();

thread.join();

System.out.println(“i am the main thread”);
正常情況下,主函數的打印語句會在 MyThreadT 線程 run 方法執行前執行,而 join 語句則指明 main 線程必須阻塞直到 MyThreadT 執行結束。

多線程帶來的一些問題

多線程的優點我們不說了,現在來看看多線程,也就是併發下會有哪些內存問題。

1、競態條件

這是一類問題,當多個線程同時訪問並修改同一個對象,該對象最終的值往往不如預期。

我們創建了 100 個線程,每個線程啓動時隨機 sleep 一會,然後爲 count 加一,按照一般的順序執行流,count 的值會是 100。

但是我告訴你,無論你運行多少遍,結果都不盡相同,等於 100 的概率非常低。這就是併發,原因也很簡單,count++ 這個操作它不是一條指令可以做的。

它分爲三個步驟,讀取 count 的值,自增一,寫回變量 count 中。多線程之間互相不知道彼此,都在執行這三個步驟,所以某個線程當前讀到的數據值可能早已不是最新的了,結果自然不盡如期望。

但,這就是併發。

2、內存可見性

內存可見性是指,某些情況下,線程對於一些資源變量的修改並不會立馬刷新到內存中,而是暫時存放在緩存,寄存器中。

這導致的最直接的問題就是,對共享變量的修改,另一個線程看不到。

這段代碼很簡單,主線程和我們的 ThreadTwo 共享一個全局變量 flag,後者一直監聽這個變量值的變化情況,而我們在主線程中修改了這個變量的值,由於內存可見性問題,主線程中的修改並不會立馬映射到內存,暫時存在緩存或寄存器中,這就導致 ThreadTwo 無法知曉 flag 值的變化而一直在做循環。

總結一下,進程作爲系統分配資源的基本單元,而線程是進程的一部分,共享着進程中的資源,並且線程還是系統調度的最小執行流。在實時系統中,每個線程獲得時間片調用 CPU,多線程併發式使用 CPU,每一次上下文切換都對應着「運行現場」的保存與恢復,這也是一個相對耗時的操作。

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