讀書筆記之《Java多線程編程核心技術》

1、前言

    讀書筆記系列主要記錄本身看的書籍中的知識點,算是一個概括整理吧。《Java多線程編程核心技術》這本書主要講解了Java多線程相關的知識。全書分爲7章,下面將記錄我的認爲每章中重要的知識點。面試

2、Java多線程的基礎

一、進程和線程

進程是資源分配的最小單位,線程是CPU調度的最小單位。直觀點理解:對於操做系統來講,一個任務就是一個進程,好比打開一個瀏覽器就是啓動一個瀏覽器進程,打開兩個記事本就啓動了兩個記事本進程。有些進程不止同時幹一件事,好比Word,它能夠同時進行打字、拼寫檢查、打印等事情。在一個進程內部,要同時幹多件事,就須要同時運行多個「子任務」,把進程內的這些「子任務」稱爲線程(Thread)。每一個進程至少要作一件事,因此進程裏至少要有一個線程。線程是最小的執行單元,而進程由至少一個線程組成。如何調度進程和線程,徹底由操做系統決定。編程

特別注意:多進程和多線程的程序涉及到同步、數據共享的問題。進程之間共享信息可經過TCP/IP協議,線程間共享信息可經過共用內存。線程不可以獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。進程有獨立的地址空間,相互不影響,線程沒有本身獨立的地址空間(地址空間都是按進程分配的,但在地址空間裏有專屬於線程的線程棧,地址空間是系統給進程分配的虛擬內存,線程棧是線程本身獨有的)。進程的切換比線程的切換開銷大。每一個進程對應一個JVM實例,多個線程共享JVM裏的堆。單核CPU執行多任務,是操做系統輪流讓各個任務輪流執行,因爲CPU的執行速度實在是太快了,感受就像全部任務都在同時
執行同樣。真正的並行執行多任務只能在多核CPU上實現。設計模式

注意:Java採用單線程編程模型,JVM建立主線程,主線程能夠建立子線程。瀏覽器

二、Java多線程的幾種實現方式

(1)繼承Thread類,重寫run()方法;
(2)實現Runnable接口,重寫run()方法;
(3)經過Callable和FutureTask建立線程;
(4)經過線程池建立線程。安全

三、sleep()方法

使當前執行的線程休眠(暫時中止執行)指定的毫秒數,線程不會失去對監視器的全部權。休眠時間結束後,進入就緒狀態,和其餘線程一塊兒競爭CPU的執行時間。注意:wait()是Object裏的方法,wait是進入線程等待池等待,出讓系統資源,其餘線程能夠佔用CPU。調用wait方法的線程,不會本身喚醒,須要線程調用notify / notifyAll方法喚醒等待池中的全部線程,纔會進入就緒隊列中等待系統分配資源。sleep方法會自動喚醒,若是時間不到,想要喚醒,可使用interrupt方法強行打斷。多線程

四、中止線程

(1)使用退出標誌,使線程正常退出,當run()執行完後,線程終止;
(2)stop()方法強制執行,廢棄了,不要用這個方式;
(3)interrupt()方法,該方法是在當前線程打了箇中止標記,並不會真的中止線程。併發

注意:sleep()狀態下中止某一個線程,會進入catch語句,而且清除中止狀態值,使之變成false。異步

/**
 * Causes the currently executing thread to sleep (temporarily cease
 * execution) for the specified number of milliseconds, subject to
 * the precision and accuracy of system timers and schedulers. The thread
 * does not lose ownership of any monitors.
 *
 * @param  millis
 *         the length of time to sleep in milliseconds
 *
 * @throws  IllegalArgumentException
 *          if the value of {@code millis} is negative
 *
 * @throws  InterruptedException
 *          if any thread has interrupted the current thread. The
 *          <i>interrupted status</i> of the current thread is
 *          cleared when this exception is thrown.
 */
public static native void sleep(long millis) throws InterruptedException;

五、interrupted()、isInterrupted()

(1)interrupted()方法,測試當前線程是否已是中斷狀態,執行後具備將狀態標誌清除爲false的功能;
(2)isInterrupted()方法,測試線程Thread對象是否已是中斷狀態,但不清除狀態標誌。分佈式

六、yield()

yield()方法的做用是放棄當前的CPU資源,將它讓給其餘的任務去佔用CPU執行時間。可是放棄的時間不肯定,有可能剛剛放棄,立刻又得到了CPU時間片。yield讓當前線程由「運行狀態」進入到「就緒狀態」。函數

七、優先級

操做系統中,線程能夠劃分優先級,優先級較高的線程獲得的CPU資源較多。線程的優先級分爲1~10 10個等級,1最低,10最高。注意:優先級和執行順序具備不肯定性和隨機性。

八、守護線程

Java線程分兩種:用戶線程、守護線程。守護線程是一種特殊的線程,當進程中不存在非守護線程了,那麼守護線程自動銷燬。典型的守護線程就是垃圾回收線程。

3、多線程中對併發訪問的控制

一、synchronized關鍵字

(1)synchronized關鍵字取得的鎖是對象鎖(對象鎖鎖住的是,一樣由synchronized修飾的方法或代碼段),而不是把一段代碼或者方法、函數看成鎖。哪一個線程先執行帶synchronized關鍵字的方法,哪一個線程就持有該方法所屬對象的鎖Lock,那麼其餘線程就只能呈現等待狀態,前提是多個線程訪問的是同一個對象。若是多個線程訪問多個對象,那麼JVM會建立多個鎖。

(2)synchronized關鍵字擁有鎖重入的功能,在使用synchronized時,當一個線程獲得一個對象鎖後,再次請求此對象鎖時是能夠再次得到該對象的鎖的。在一個synchronized方法/塊的內部調用本類的其餘synchronized方法/塊時,是永遠能夠獲得鎖的。當存在父子類繼承關係時,子類是徹底能夠經過「可重入鎖」調用父類的同步方法的(注意:同步不能夠繼承,子類方法中也須要加上synchronized)。當一個線程執行的代碼出現異常時,其所持有的鎖會自動釋放。

注意:只有共享資源的讀寫訪問才須要同步化,若是不是共享資源,那麼就沒有同步的必要。

注意:A線程先持有object對象的Lock鎖,那麼B線程能夠以異步方式調用object對象中的非synchronized類型的方法;A線程先持有object對象的Lock鎖,B線程若是這時也調用object對象的synchronized類型的方法則需等待,也就是同步。

(3)synchronized關鍵字聲明方法的話在某些狀況下是有弊端的,好比A線程調用同步方法執行一個長時間的任務,那麼B線程就須要等待很長時間。這個時候可使用同步代碼塊。當兩個併發線程訪問同一個對象中的synchronized(this)同步代碼塊時,一段時間內只能有一個線程被執行,另外一個線程須要等待當前線程執行完這個代碼塊後才能夠執行該代碼塊,可是另外一個線程能夠訪問該對象中的非synchronized(this)同步代碼塊。

注意:當一個線程訪問object的一個synchronized(this)同步代碼塊時,其餘線程對同一個object中全部其餘synchronized(this)同步代碼塊的訪問將被阻塞。即synchronized使用的對象監視器是一個。synchronized、synchronized(this)都是鎖定當前對象的。

(4)Java還支持將「任意對象」做爲「對象監視器」來實現同步,這個「任意對象」大多數是實例變量及方法的參數,使用格式爲synchronized(非this對象),鎖非this對象具備必定的優勢:若是在一個類中有不少個synchronized方法,這時雖然能實現同步,可是會受到阻塞,影響效率;若是使用同步代碼塊鎖非this對象,則synchronized(非this)代碼塊中的程序與同步方法是異步的,不與其餘鎖this同步方法爭搶this鎖,則能夠大大提升運行效率。

注意:當多個線程同時執行synchronized(x){}同步代碼塊時呈同步效果;當其餘線程執行x對象中synchronized同步方法時呈同步效果;當其餘線程執行x對象方法裏面的synchronized(this)代碼塊時也呈現同步效果。

(5)synchronized關鍵字加到static靜態方法上是給Class類上鎖,而synchronized關鍵字加到非static靜態方法上是給對象上鎖。(一個是Class鎖,一個是對象鎖,是會產生異步的)。

Class鎖能夠對類的全部對象實例起做用,也就是說若是做用在兩個實例,那麼靜態的同步方法仍是同步運行。synchronized(類.class)同步代碼塊的做用和synchronized static方法的做用是同樣的。

鎖對象的改變:在將任何數據類型做爲同步鎖時,須要注意的是,是否有多個線程同時持有鎖對象,若是同時持有相同的鎖對象,那麼這些線程之間就是同步的,若是分別得到鎖對象,這些線程之間就是異步的。

注意:只要對象不變,即便對象的屬性改變,結果仍是同步的。

二、volatile關鍵字

volatile關鍵字的主要做用是使實例變量在多個線程間可見。volatile關鍵字,強制從公共堆棧中取得變量的值,而不是從線程私有數據棧中取得變量的值。可是volatile關鍵字不支持原子性。

(1)synchronized和volatile比較

volatile是線程同步的輕量級實現,synchronized是重量級;volatile只能修飾變量,synchronized能夠修飾方法以及代碼塊。
多線程訪問volatile不會發生阻塞,而synchronized會發生阻塞。
volatile能保證數據的可見性,但不能保證原子性,而synchronized能夠保證原子性,也能夠間接保證可見性,它會將私有內存和公有內存中的數據作同步。
volatile解決的是變量在多個線程之間的可見性,而synchronized解決的是多個線程之間訪問資源的同步性。

4、線程間的通訊、交互

一、等待、通知機制(wait/notify機制)

在調用wait()前,線程必須得到該對象的對象級別鎖,即只能在同步方法或同步塊中調用wait方法。執行wait()後,當前線程釋放鎖。方法notify()也要在同步方法或同步塊中調用,即在調用前,線程必須得到該對象的對象級別鎖。在執行notify()方法後,當前線程不會立刻釋放該對象鎖,呈wait狀態的線程也不能立刻得到該對象鎖,要等到執行notify()方法的線程將程序執行完,即退出synchronized代碼塊後,當前線程纔會釋放鎖。
總結:wait使線程中止運行,notify使中止的線程繼續運行。

wait()方法可使調用該方法的線程釋放共享資源的鎖,而後從運行狀態退出,進入等待隊列,直到被再次喚醒。notify()方法能夠隨機喚醒等待隊列中等待同一共享資源的「一個」線程,並使該線程退出等待隊列,進入可運行狀態。notifyAll()使全部等待隊列中等待同一共享資源的「所有」線程從等待狀態退出,進入可運行狀態。

wait(long)方法的功能是等待一段時間內是否有線程對鎖進行喚醒,若是超過這個時間就自動喚醒。

二、生產者、消費者模式

原理基於wait/notify。特別注意:一些大廠面試會問這個,手寫。

三、join()方法

join()的做用是等待線程對象銷燬。使用場景舉例:主線程建立並啓動子線程,子線程運行時間長的話,主線程線運行完,若是主線程想要獲取子線程的運行結果,也就是主線程想要等子線程運行完再結束,那麼就能夠用join()。

方法join的做用是使所屬的線程對象x正常執行run()方法中的任務,而使當前線程z進行無限期的阻塞,等待線程x銷燬後再繼續執行線程z後面的代碼。

join()在內部使用wait()方法進行等待,而synchronized關鍵字使用的是「對象監視器」原理作同步。

四、ThreadLocal

ThreadLocal解決的是變量在不一樣線程間的隔離性,也就是不一樣線程擁有本身的值。(可參考:一文帶你搞定ThreadLocal原理與使用)。

5、Lock的使用

一、ReentrantLock類

調用ReentrantLock對象的lock()方法得到鎖,調用unlock()方法釋放鎖。

一個Lock對象裏面能夠建立多個Condition(即監視器對象)實例,線程對象能夠註冊在指定的Condition中,從而能夠選擇性的進行線程通知,
調度上更加靈活。在使用notify()/notifyAll()方法進行通知時,被通知的線程由JVM隨機選擇,但使用ReentrantLock結合Condition能夠實現「選擇性通知」。

Object中的wait()至關於Condition裏的await()。Object裏的notify()至關於Condition裏的signal()。

二、公平鎖、非公平鎖

鎖Lock分爲公平鎖、非公平鎖。公平鎖表示線程得到鎖的順序是按照線程加鎖的順序來分配的,先來先得。非公平鎖是一種得到鎖的搶佔機制,隨機得到鎖。

三、ReentrantReadWriteLock

類ReentrantLock具備徹底互斥排他的效果,即同一時刻只有一個線程在執行ReentrantLock.lock()方法後面的業務。這樣保證了實例變量的線程安全性,可是效率低。可使用讀寫鎖。

讀寫鎖中,讀相關操做的鎖,稱爲共享鎖;寫操做相關的鎖,稱爲排它鎖。即讀鎖之間不互斥,寫鎖與讀鎖互斥,寫鎖與寫鎖互斥。

5、定時器

這一章都是講Timer類的。對定時任務感興趣的能夠去研究研究分佈式定時任務,實際項目中,通常仍是用分佈式定時任務多一些。

6、單例模式與多線程

單例設計模式,在實際應用中比較常見。可是結合多線程使用時候,仍是須要有不少須要注意的地方。

一、餓漢模式/當即加載

當即加載(餓漢模式)就是使用類的時候已經將對象建立完畢。

public class MyObject {

    private static MyObject myObject = new MyObject();

    private MyObject() {

    }
    
    public static MyObject getInstance() {
        // 缺點:不能有其餘實例變量,由於該方法沒作同步,可能出現非線程安全問題
        return myObject;
    }
    
}

二、懶漢模式/延遲加載

延遲加載就是在調用get()方法時實例才被建立。

public class MyObject {

    private static MyObject myObject;

    private MyObject() {

    }

    public static MyObject getInstance() {
        // 沒作同步,不安全
        if (myObject == null) {
            myObject = new MyObject();
        }
        return myObject;
    }

}

三、雙鎖檢查機制(存在反射攻擊問題、序列化問題)

public class MyObject {

    private volatile static MyObject myObject;

    private MyObject() {

    }

    public static MyObject getInstance() {
        if (myObject == null) {
            synchronized (MyObject.class) {
                if (myObject == null) {
                    myObject = new MyObject();
                }
            }
        }
        return myObject;
    }

}

四、使用靜態內置類(存在序列化問題)

public class MyObject {

    private static class MyObjectHelper {
        private static MyObject myObject = new MyObject();
    }

    private MyObject() {

    }

    public static MyObject getInstance() {
        return MyObjectHelper.myObject;
    }

}

五、使用static代碼塊

靜態代碼塊中的代碼在使用類的時候就已經執行了,能夠利用該特性來實現單例設計模式。

public class MyObject {

    private static MyObject instance = null;
    
    private MyObject() {
        
    }
    
    static {
        instance = new MyObject();
    }
    
    public static MyObject getInstance() {
        return instance;
    }
    
}

六、使用枚舉enum (最佳,推薦這種)

枚舉enum和靜態代碼塊的特性類似,在使用枚舉類時,構造方法會被自動調用,也能夠利用該特性實現單例設計模式。

public enum Singleton {

    INSTANCE;

    public Singleton getInstance() {
       return INSTANCE;
    }

}

7、拾漏增補

一、線程的狀態

public enum State {
    // 至今還沒有啓動的線程
    New,
    // 正在JVM中執行的線程
    RUNNABLE,
    // 受阻塞並等待某個監視器鎖的線程
    BLOCKED,
    // 無限期的等待另外一個線程來執行某一特定操做的線程
    WAITING,
    // 等待另外一個線程來執行,取決於指定等待時間的操做的線程
    TIMED_WAITING,
    // 已退出的線程
    TERMINATED;
}

New狀態是線程實例化後還未執行start()方法時的狀態,Runnable包含Ready和Running,yield就是將線程從Running置爲Ready。
wait、join--->WAITING,sleep(time)、wait(time)、join(time)--->TIMED_WAITING

二、線程組 (ThreadGroup)

能夠把線程歸屬到線程組中,線程組中能夠有線程對象,也能夠有線程組,組裏還能夠有線程。線程組的做用是批量管理線程或者線程組對象。

線程組有自動歸屬特性,若是實例化一個ThreadGroup線程組x時,沒有指定x所屬的線程組,那麼x線程組自動歸屬到當前線程對象所屬的線程組裏。

JVM的根線程組是system,system沒有父線程組。(咱們經常使用的main開始方法,它所在的線程組是main,線程組main的父線程組是system)。

ThreadGroup.interrupt()方法能夠將該組中全部正在運行的線程批量中止。

相關文章
相關標籤/搜索