21.1 Java 多線程編程基礎

進程和線程

進程: 一個正在執行的程序。每個進程執行都有一個執行順序,該順序是一個執行路徑,或叫一個控制單元。一個進程至少有一個線程。

線程:就是進程中的一個獨立的控制單元. 線程控制這進程的執行。

多進程的缺點:進程切換開銷大;進程間的通信不方便。

多線程: 指的是在單個程序中可以同時運行多個不同的線程,執行不同的任務,線程切換的開銷小 。

在 Java 中,當我們啟動 main 函數時其實就啟動了一個 JVM 的進程,而 main 函數所在的線程就是這個進程中的一個線程,也稱主線程。

線程創建與運行

Java 中表示線程,用到了 Thread 類。其構造方法如下:

public Thread();
public Thread (ThreadGroup group,Runnable target,String name);     
public Thread(Runnable target);
public Thread(Runnable target,String name);
public Thread(String name);
public Thread(ThreadGroup group,Runnable target);
public Thread(ThreadGroup group,String name); 

線程創建的三種方式

  • 繼承 Thread 類實現多線程
  • 實現 Runnable 接口編寫多線程,將一個 Runnable 接口對象傳遞給線程,線程在調度時將自動調用 Runnable 接口對象的 run方法。
  • 使用 FutureTask 方式。創建的 FutrueTask 對象作為任務創建了一個線程并且啟動它,最后通過 futureTask.get()等待任務執行完畢并返回結果。

使用線程

程序中不要直接調用 run 方法,而是調用線程對象的 start()方法啟動線程,讓其進入可調度狀態,線程將在合適時機獲得調度自動執行 run()方法。

若想有效使用多線程代碼,要對監視器和鎖有些基本的認識。你需要知道的要點如下。
? 同步是為了保護對象的狀態和內存,而不是代碼。
? 同步是線程間的協助機制。一個缺陷就可能破壞這種協助模型,導致嚴重的后果。
? 獲取監視器只能避免其他線程再次獲取這個監視器,而不能保護對象。
? 即便對象的監視器鎖定了,不同步的方法也能看到(和修改)不一致的狀態。
? 鎖定 Object[] 不會鎖定其中的單個對象。
? 基本類型的值不可變,因此不能(也無需)鎖定。
? 接口中聲明的方法不能使用 synchronized 修飾。
? 內部類只是語法糖,因此內部類的鎖對外層類無效(反過來亦然)。
? Java 的鎖可重入(reentrant)。這意味著,如果一個線程擁有一個監視器,這個線程遇到具有同一個監視器的同步代碼塊時,可以進入這個代碼塊。

wait() 和 notify() 方法必須在 synchronized 修飾的方法或代碼塊中使用,因為只有臨時把鎖放棄,這兩個方法才能正常工作。

線程調度與優先級(體現了多線程的隨機性)

Java 采用搶占式調度策略,下面幾種情況下,當前線程會放棄CPU:

  1. 當前時間片用完;
  2. 線程在執行時調用了yield()sleep()方法主動放棄;
  3. 進行 I/O 訪問,等待用戶輸入,導致線程阻塞;或者為等候一個條件變量,線程調用wait方法;
  4. 有高優先級的線程參與調度。

線程的優先級
用數字來表示,范圍從 1~10。主線程的默認優先級為 5。

    // 三個常用的優先級
    Thread.MIN_PRIORITY=1          
    Thread.NORM_PRIORITY=5     
    Thread.MAX_PRIORITY=10
  

線程的生命周期

Java 做了很多工作,力求把這些細節抽象化。Java 提供了一個名為 Thread.State 的枚舉類型,囊括了操作系統看到的線程狀態。 Thread.State 中的值概述了一個線程的生命周期。

常見的線程狀態
  • NEW 新建狀態
    已經創建線程,但還沒在線程對象上調用 start() 方法。所有線程一開始都處于這個
    狀態。

  • RUNNABLE 運行中
    當操作系統調度線程時可以運行或該線程正在運行。

  • BLOCKED 阻塞狀態
    因為它在等待獲得一個鎖,以便進入聲明為 synchronized 的方法或代碼塊。

  • WAITING
    線程中止運行,因為它調用了 Object.wait() 或 Thread.join() 方法。
    在 sleep 和 wait 時。

  • TIMED_WAITING
    線程中止運行,因為它調用了 Thread.sleep() 方法,或者調用了 Object.wait() 或Thread.join() 方法,而且傳入了超時時間。

  • TERMINATED
    線程執行完畢。線程對象的 run() 方法正常退出,或者拋出了異常。

Thread 類中常用方法

  • setName()getName()
    開發者使用這兩個方法設定或取回單個線程的名稱。為線程起名字是個好習慣,因為這樣調試時更方便,尤其是使用 jvisualvm 等工具。

  • isAlive()
    返回線程是否還“活著”。線程被啟動后,run 方法運行結束前,返回值都是 true。

  • start()
    這個方法用來創建一個新應用線程,然后再調用 run() 方法調度這個線程,開始執行。正常情況下,執行到 run() 方法的末尾或者執行 run() 方法中的一個 return 語句后,線程就會結束運行。

  • interrupt()
    中斷線程。如果調用 sleep() 、 wait() 或 join() 方法時阻塞了某個線程,那么在表示這個線程的Thread 對象上調用 interrupt() 方法,會讓這個線程拋出 InterruptedException 異常(并把線程喚醒)。如果線程中涉及可中斷的 I/O 操作,那么這個 I/O 操作會終止,而且線程會收到 ClosedByInterruptException 異常。即便線程沒有從事任何可中斷的操作,線程的中斷狀態也會被設為 true。

  • join()
    是一個對象方法,可以讓調用 join 的當前線程一直處于等待狀態,等待直到該線程結束。可以把這個方法理解為一個指令,在其他線程結束之前,當前線程不會繼續向前運行。貌似只在start()后才生效。

  • setDaemon()
    用戶線程是這樣一種線程,只要它還“活著”,進程就無法退出——這是線程的默認行為。有時,程序員希望線程不阻止進程退出——這種線程叫守護線程(可以理解為后臺線程)。一個線程是守護線程還是用戶線程,由 setDaemon() 方法控制。這個方法必須在線程 start 前調用,否則會拋出 IllegalThreadStateException。

  • setUncaughtExceptionHandler()
    線程因拋出異常而退出時,默認的行為是打印線程的名稱、異常的類型、異常消息和堆棧跟蹤。如果這么做還不夠,可以在線程中自定義的處理程序,處理未捕獲的異常。

  • yield() 這也是一個靜態方法,調用該方法,是告訴操作系統的調度器:我現在不著急占用CPU,你可以先讓其他線程運行。不過,這對調度器也僅僅是建議,調度器如何處理是不一定的,它可能完全忽略該調用。

  • sleep() :Thread 有一個靜態的 sleep 方法,調用該方法會讓當前線程睡眠指定的時間。

@Override
public void run() {
    while (!Thread.currentThread().isInterrupted() && more work to do) {
        try {
            ...
            sleep(delay);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();//重新設置中斷標示
        }
    }
}

sleep 與 yield 方法的區別
當線程調用 sleep 方法時調用線程會被阻塞掛起指定的時間,在這期間線程調度器不會去調度該線程。而調用 yield 方法時,線程只是讓出自己剩余的時間片,并沒有被阻塞掛起,而是處于就緒狀態,線程調度器下一次調度時就有可能調度到當前線程執行。

Thread 類棄用的方法

Thread 類除了有一些有用的方法之外,還有一些危險的方法,開發者不應該使用。這些方法是 Java 線程 API 原來提供的,但很快就發現不適合開發者使用。可惜的是,因為 Java要向后兼容,所以不能把這些方法從 API 中移除。

  • stop()
    如若不違背并發安全的要求,幾乎不可能正確使用 Thread.stop() ,因為 stop() 方法會立即“殺死”線程,不會給線程任何機會把對象恢復成合法狀態。這和并發安全等原則完全相悖,因此絕對不能使用 stop() 方法。

  • suspend() 、 resume() 和 countStackFrames()
    調用 suspend() 方法掛起線程時,不會釋放這個線程擁有的任何一個監視器,因此,如果其他線程試圖訪問這些監視器,這些監視器會變成死鎖。其實,這種機制會導致死鎖之間的條件競爭,而且 resume() 會導致這幾個方法不能使用。

  • destroy()
    這個方法一直沒有實現,如果實現了,會遇到與 suspend() 方法一樣的條件競爭。開發者始終應該避免使用這些棄用的方法。為了達到上述方法的預期作用,Java 開發了一些安全的替代模式。前面提到的“關閉前一直運行”模式就是這些模式的一例。

可見性和可變性

在 Java 中,其實一個進程中的每個 Java 應用線程都有自己的棧(和局部變量),不過這些線程共用同一個堆,因此可以輕易在線程之間共享對象,畢竟需要做的只是把引用從一個線程傳到另一個線程.

  • 由此引出 Java 的一個一般設計原則——對象默認可見。如果我有一個對象的引用,就可以復制一個副本,然后將其交給另一個線程,不受任何限制。Java 中的引用其實就是類型指針,指向內存中的一個位置,而且所有線程都共用同一個地址空間,所以默認可見符合自然規律。
  • 除了默認可見之外,Java 還有一個特性對理解并發很重要——對象是可變的(mutable),對象的內容(實例字段的值)一般都可以修改。使用 final 關鍵字可以把變量或引用聲明為常量,但這種字段不屬于對象的內容。

這兩個特性(跨線程下對象的可見性和可變性)結合在一起,大大增加了理解 Java 并發編程的難度。

并發編程的安全性

原子操作
在 Java 中, 對非 long 和 double 類型的域的讀取和寫入操作是原子操作。對象引用的讀取和寫入操作也是原子操作。在多線程程序中使用 long 和 double 型的共享變量時,需要把變量申明為 volatile 以保證讀取和寫入操作的完整性.

如果我們想編寫正確的多線程代碼,得讓程序滿足一個重要的條件,

即:在一個程序中,不管調用什么方法,也不管操作系統如何調度應用線程,一個對象看到的任何其他對象都不處于非法或不一致的狀態,這樣的程序才稱得上是安全的多線程程序 。

互斥(mutual exclusion)和狀態保護

臨界資源問題
只要修改或讀取對象的過程中,對象的狀態可能不一致,這段代碼就要受到保護。為了保護這種代碼,Java 平臺只提供了一種機制:互斥

Java 平臺會為它創建的每個對象記錄一個特殊的標記,這個標記叫監視器(monitor)。Java 使用 synchronized 指明對應的監視器(或叫鎖)。

同步是保護狀態的一種協助機制,因此非常脆弱。一個缺陷(需要使用synchronized 修飾的方法卻沒有使用)就可能為系統的整體安全性帶來災難性的后果。

  • 對象如同鎖,持有鎖的線程可以在同步中執行。
  • 沒有持有鎖的線程即使獲取 CPU 的執行權,也進不去,因為沒有獲取鎖。

同步的前提:

  1. 必須要有兩個或者以上的線程
  2. 必須要多個線程使用同一個鎖

好處:解決了多線程的安全問題
弊端:多個線程需要判斷鎖,較為消耗資源.

synchronized 關鍵字

Java 為開發者提供了 synchronized 關鍵字。這個關鍵字可以用在代碼塊或方法上,使用時,Java 平臺會限制訪問代碼塊或方法中的代碼。

之所以使用 synchronized 這個詞作為“需要臨時互斥存儲”的關鍵詞,除了說明需要獲取監視器之外,還表明進入代碼塊時,JVM 會從主內存中重新讀取對象的當前狀態。類似地,退出 synchronized 修飾的代碼塊或方法時,JVM 會刷新所有修改過的對象,把新狀態存入主內存。

執行 synchronized 實例方法的過程大致如下:
1)嘗試獲得鎖,如果能夠獲得鎖,繼續下一步,否則加入等待隊列,阻塞并等待喚醒。
2)執行實例方法體代碼。
3)釋放鎖,如果等待隊列上有等待的線程,從中取一個并喚醒,如果有多個等待的線程,喚醒哪一個是不一定的,不保證公平性。

synchronized的實際執行過程比這要復雜得多,而且Java虛擬機采用了多種優化方式以提高性能,但從概念上,我們可以這么簡單理解。

因為 synchronized 關鍵字把代碼包圍起來,所以很多開發者認為,Java 的并發和代碼有關。有些資料甚至把 synchronized 修飾的塊或方法中的代碼稱為 臨界區 ,還認為臨界區是并發的關鍵所在。其實不然,其實我們要防范的是數據的不一致性

synchronized 的使用

synchronized 可用于聲明在方法上,稱之為同步方法,也可用于包裝代碼塊。

synchronized(對象) {
    需要被同步的代碼
}

synchronized 使用常見誤區

synchronized 保護的是對象,對實例方法,保護的是當前實例對象 this,對靜態方法,保護的是類對象。實際上,每個對象都有一個鎖和一個等待隊列,類對象也不例外。

被保護的代碼塊過多. 比如一個方法中只有少數幾行代碼訪問共享變量, 卻把整個方法聲明為 synchronized. 這樣會影響程序的性能, 正確的做法是把需要同步的代碼塊用 synchronized 代碼塊包圍即可。

synchronized 靜態方法和 synchronized 實例方法保護的是不同的對象,不同的兩個線程,可以一個執行 synchronized 靜態方法,另一個執行 synchronized 實例方法。

可重入性
synchronized 有一個重要的特征,它是可重入的,也就是說,對同一個執行線程,它在獲得了鎖之后,在調用其他需要同樣鎖的代碼時,可以直接調用。比如,在一個 synchronized 實例方法內,可以直接調用其他 synchronized 實例方法。可重入是一個非常自然的屬性,應該是很容易理解的,之所以強調,是因為并不是所有鎖都是可重入的,后續章節我們會看到不可重入的鎖。可重入是通過記錄鎖的持有線程和持有數量來實現的,當調用被 synchronized 保護的代碼時,檢查對象是否已被鎖,如果是,再檢查是否被當前線程鎖定,如果是,增加持有數量,如果不是被當前線程鎖定,才加入等待隊列,當釋放鎖時,減少持有數量,當數量變為 0 時才釋放整個鎖。

volatile 關鍵字

如果只是為了保證內存可見性,使用 synchronized 的成本有點高,有一個更輕量級的方式,那就是給變量加修飾符 volatile。這個關鍵字指明,應用代碼使用字段或變量前,必須重新從主內存讀取值。同樣,修改使用 volatile 修飾的值后,在寫入變量之后,必須存回主內存。

volatile 關鍵字的主要用途之一是在“關閉前一直運行”模式中使用。編寫多線程程序時,如果外部用戶或系統需要向處理中的線程發出信號,告訴線程在完成當前作業后優雅關閉線程,那么就要使用 volatile 。這個過程有時叫作“優雅結束”模式。

當一個變量定義為 volatile 之后,將具備兩種特性:

  1. 保證此變量對所有的線程的可見性,這里的“可見性”,如本文開頭所述,當一個線程修改了這個變量的值,volatile 保證了新值能立即同步到主內存,以及每次使用前立即從主內存刷新。但普通變量做不到這點,普通變量的值在線程間傳遞均需要通過主內存(詳見:Java內存模型)來完成。

  2. 禁止指令重排序優化。有 volatile 修飾的變量,賦值后多執行了一個 “load addl $0x0, (%esp)”操作,這個操作相當于一個內存屏障(指令重排序時不能把后面的指令重排序到內存屏障之前的位置),只有一個 CPU 訪問內存時,并不需要內存屏障;(什么是指令重排序:是指CPU采用了允許將多條指令不按程序規定的順序分開發送給各相應電路單元處理)。

volatile 性能

volatile 的讀性能消耗與普通變量幾乎相同,但是寫操作稍慢,因為它需要在本地代碼中插入許多內存屏障指令來保證處理器不發生亂序執行。

死鎖和避免死鎖

使用 synchronized 或者其他鎖,要注意死鎖。所謂死鎖就是類似這種現象,比如,有a、b兩個線程,a持有鎖A,在等待鎖B,而 b 持有鎖B,在等待鎖 A, a 和 b 陷入了互相等待,最后誰都執行不下去。

那么為什么會產生死鎖呢?學過操作系統的朋友應該都知道,死鎖的產生必須具備以下四個條件。
● 互斥條件:指線程對已經獲取到的資源進行排它性使用,即該資源同時只由一個線程占用。如果此時還有其他線程請求獲取該資源,則請求者只能等待,直至占有資源的線程釋放該資源。
● 請求并持有條件:指一個線程已經持有了至少一個資源,但又提出了新的資源請求,而新資源已被其他線程占有,所以當前線程會被阻塞,但阻塞的同時并不釋放自己已經獲取的資源。
● 不可剝奪條件:指線程獲取到的資源在自己使用完之前不能被其他線程搶占,只有在自己使用完畢后才由自己釋放該資源。
● 環路等待條件:指在發生死鎖時,必然存在一個線程—資源的環形鏈,即線程集合{T0, T1,T2, …, Tn} 中的 T0 正在等待一個 T1 占用的資源,T1 正在等待 T2 占用的資源,……Tn正在等待已被 T0 占用的資源。

死鎖 Java 代碼示例:

package qy.basic.ch21;

public class Ch21_1_DeadLockDemo {
    static class ThreadA extends Thread {
        @Override
        public void run() {
            synchronized (ThreadA.class) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (ThreadB.class) {
                }
            }
            
        }
    }
    
    static class ThreadB extends Thread {
        @Override
        public void run() {
            synchronized (ThreadB.class) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (ThreadA.class) {
                }
            }
            
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new ThreadA().start();
        new ThreadB().start();
    }
}

怎么解決呢?要想避免死鎖,只需要破壞掉至少一個構造死鎖的必要條件即可,但是學過操作系統的讀者應該都知道,目前只有請求并持有環路等待條件是可以被破壞的。

造成死鎖的原因其實和申請資源的順序有很大關系,使用資源申請的有序性原則就可以避免死鎖。

所以解決方法是應該盡量避免在持有一個鎖的同時去申請另一個鎖,如果確實需要多個鎖,所有代碼都應該按照相同的順序去申請鎖。

參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,157評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,157評論 2 375

推薦閱讀更多精彩內容