按照規劃,從本篇開始咱們開啓『併發』系列內容的總結,從本篇的線程開始,到線程池,到幾種併發集合源碼的分析,咱們一點點來,但願你也有耐心,由於併發這塊知識是你職業生涯始終繞不過的坎,任何一個項目都或多或少的要涉及一些併發的處理。java
這一系列文章只能算是對併發這塊基本理論知識的一個總結與介紹,想要成爲併發高手,必然是須要經過大規模併發訪問的線上場景應用,或許之後我有了相關經驗了,再給大家作一點分享吧。git
進程和線程算是操做系統內兩個很基本、很重要的概念了,進程是操做系統中進行保護和資源分配的基本單位,操做系統分配資源以進程爲基本單位。而線程是進程的組成部分,它表明了一條順序的執行流。github
系統中的進程線程模型是這樣的:算法
進程從操做系統得到基本的內存空間,全部的線程共享着進程的內存地址空間。固然,每一個線程也會擁有本身私有的內存地址範圍,其餘線程不能訪問。緩存
因爲全部的線程共享進程的內存地址空間,因此線程間的通訊就容易的多,經過共享進程級全局變量便可實現。bash
同時,在沒有引入多線程概念以前,所謂的『併發』是發生在進程之間的,每一次的進程上下文切換都將致使系統調度算法的運行,以及各類 CPU 上下文的信息保存,很是耗時。而線程級併發沒有系統調度這一步驟,進程分配到 CPU 使用時間,並給其內部的各個線程使用。微信
在分時系統中,進程中的每一個線程都擁有一個時間片,時間片結束時保存 CPU 及寄存器中的線程上下文並交出 CPU,完成一次線程間切換。固然,當進程的 CPU 時間使用結束時,全部的線程必然被阻塞。markdown
JAVA API 中用 Thread 這個類抽象化描述線程,線程有幾種狀態:多線程
其中 RUNNABLE 表示的是線程可執行,但不表明線程必定在獲取 CPU 執行中,可能因爲時間片使用結束而等待系統的從新調度。BLOCKED、WAITING 都是因爲線程執行過程當中缺乏某些條件而暫時阻塞,一旦它們等待的條件知足時,它們將回到 RUNNABLE 狀態從新競爭 CPU。併發
此外,Thread 類中還有一些屬性用於描述一個線程對象:
其中,tid 是一個自增的字段,每建立一個新線程,這個 id 都會自增一。優先級取值範圍,從一到十,數值越大,優先級越高,默認值爲五。
Runnable 是一個接口,它抽象化了一個線程的執行流,定義以下:
public interface Runnable {
public abstract void run();
}
複製代碼
經過重寫 run 方法,你也就指明瞭你的線程在獲得 CPU 以後執行指令的起點。咱們通常會在構造 Thread 實例的時候傳入這個參數。
建立一個線程基本上有兩種方式,一是經過傳入 Runnable 實現類,二是直接重寫 Thread 類的 run 方法。咱們詳細看看:
一、自定義 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 方法。
因此,天然會有第二種建立方式。
二、繼承 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 類中也給咱們提供了一些方法,有些方法仍是比較經常使用的。
一、sleep
public static native void sleep(long millis)
複製代碼
這是一個本地方法,用於阻塞當前線程指定毫秒時長。
二、start
public synchronized void start()
複製代碼
這個方法可能不少人會疑惑,爲何我經過重寫 Runnable 的 run 方法指定了線程的工做,但倒是經過 start 方法來啓動線程的?
那是由於,啓動一個線程不只僅是給定一個指令開始入口便可,操做系統還須要在進程的共享內存空間中劃分一部分做爲線程的私有資源,建立程序計數器,棧等資源,最終纔會去調用 run 方法。
三、interrupt
public void interrupt()
複製代碼
這個方法用於中斷當前線程,固然線程的不一樣狀態應對中斷的方式也是不一樣的,這一點咱們後面再說。
四、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 執行結束。
多線程的優勢咱們不說了,如今來看看多線程,也就是併發下會有哪些內存問題。
一、競態條件
這是一類問題,當多個線程同時訪問並修改同一個對象,該對象最終的值每每不如預期。例如:
咱們建立了 100 個線程,每一個線程啓動時隨機 sleep 一會,而後爲 count 加一,按照通常的順序執行流,count 的值會是 100。
可是我告訴你,不管你運行多少遍,結果都不盡相同,等於 100 的機率很是低。這就是併發,緣由也很簡單,count++ 這個操做它不是一條指令能夠作的。
它分爲三個步驟,讀取 count 的值,自增一,寫回變量 count 中。多線程之間互相不知道彼此,都在執行這三個步驟,因此某個線程當前讀到的數據值可能早已不是最新的了,結果天然不盡如指望。
但,這就是併發。
二、內存可見性
內存可見性是指,某些狀況下,線程對於一些資源變量的修改並不會立馬刷新到內存中,而是暫時存放在緩存,寄存器中。
這致使的最直接的問題就是,對共享變量的修改,另外一個線程看不到。
這段代碼很簡單,主線程和咱們的 ThreadTwo 共享一個全局變量 flag,後者一直監聽這個變量值的變化狀況,而咱們在主線程中修改了這個變量的值,因爲內存可見性問題,主線程中的修改並不會立馬映射到內存,暫時存在緩存或寄存器中,這就致使 ThreadTwo 沒法知曉 flag 值的變化而一直在作循環。
總結一下,進程做爲系統分配資源的基本單元,而線程是進程的一部分,共享着進程中的資源,而且線程仍是系統調度的最小執行流。在實時系統中,每一個線程得到時間片調用 CPU,多線程併發式使用 CPU,每一次上下文切換都對應着「運行現場」的保存與恢復,這也是一個相對耗時的操做。
ps:前段時間確實有點忙,拖更好多天,這裏再給你們說聲抱歉了,感謝大家尚未走,如今正式恢復,開啓併發系列總結~
文章中的全部代碼、圖片、文件都雲存儲在個人 GitHub 上:
歡迎關注微信公衆號:OneJavaCoder,全部文章都將同步在公衆號上。