Java多線程總結


Java多線程總結

系統學了一遍多線程該給自己一個交代,5000多字全部手碼,總結+隨時查資料>=10小時,這是卸載PUBG、卸載微博、卸載抖音后做的第一件事。。。

總結完整版pdf已經上傳到github作為備份,克隆地址:git@github.com:MrZhiJian/java-multi-thread.git

一、   實現線程的方式及其常用方法與屬性

1.1      進程與線程的概念及線程的有點

進程可以說是操作系統的基礎,是程序的一次運行,進程是操作系統進行資源分配和調度的最小單元。

線程可以理解為進程的一個或多個子任務,如果一個進程只有一個線程,可以理解為單任務進程,單任務的特點就行排隊執行,也就是同步。使用多線程的目的就是在線程安全的情況下進行異步執行,盡可能的提高CPU及系統資源的利用率,這也是其優點。

1.2 實現線程的兩種方式

Java實現多線程常用的兩種方式有繼承Thread類、實現Runnable接口,實現Runnable接口相對來說具有優勢,突破了Java單繼承的局限性,維持了程序的健壯性,尤其是在多個線程需要造作同一資源時,實現接口的方式是首選。在將多線程交由線程池管理的情況下也必須是實現接口的方式。

實現多線程還有其他方式,給自己留個疑問后續在深究一下。

1.3 Java多線程中常用方法

currentThread():指明代碼段正在被哪個線程調用;

isAlive():判斷當前線程是否處於活動狀態,線程處於就緒狀態或運行狀態為活動狀態;

sleep(long millisecond):指定毫秒數讓當前正在執行的線程暫停執行,當線程持有鎖的情況下,執行此方法可達到暫停執行作用但不釋放鎖,因此在有鎖的情況下慎用此方法;

停止線程的方法:

  1. 使用退出標志,當線程執行完run()方法中程序時線程終止。
  2. 使用stop()方法強行終止線程,不推薦使用,容易出現臟數據。
  3. 使用interrupt()方法中斷線程。             

使用退出標志即為常用的while(flag){}死循環,當flag變為false時,程序正常執行完畢,即線程終止。

 使用stop()方法可以達到退出線程的效果,由於在調用此方法時會釋放鎖,這就有可能使得其他線程拿到臟數據,造成數據不同步,因此此方式已過時不推薦使用。

interrupt()方法配合拋異常或return,都能達到終止線程得效果。當檢測到線程處於中斷狀態時,拋出異常或return即可。在睡眠狀態調用此方法中斷線程會拋出異常。給出檢測線程中斷狀態得兩個接口方法:

this.interrupted():測試當前線程是否處於中斷狀態,當前線程時指運行此方法的線程;

this.isInterrupted():測試線程是否已中斷,同樣用this指定時與上一個方法意義不同,用線程對象指定時意義相同,且此方法具有清除線程中斷狀態的作用,當線程調用interrupt()方法后處於中斷狀態時,調用此方法可激活。

線程暫停及恢復:

suspend():線程暫停

resume():恢復已暫停線程

此二方法已過時不推薦使用,由於suspend()具有獨占的特性,當擁有公共資源時調用此方法不釋放會造成后續線程無限時間等待,例如在synchronized 代碼塊內調用此方法時並不會釋放鎖;使用此方法還易造成數據不同步。

yield():讓出CPU使其重新調度

1.4 線程的常用屬性

線程的優先級:

Java中線程的優先級分為10個等級,1-10,通常使用3個常量來設置線程的priority屬性,三個常量分別為:

MIN_PRIORITY=1

NORM_PRIORITY=5

MAX_PRIORITY=10

線程的優先級具有繼承性,比如說A線程中啟動B線程,那么B線程的優先級與A線程的優先級是一樣的。

線程的優先級具有一定的規則性,並不是說優先級高的線程就一定會首先被執行,且線程優先級高的線程也並不一定是先執行完,也就是說CPU只是盡量將執行資源讓給優先級比較高的線程。

守護線程:

守護線程顧名思義可理解為陪伴線程,當進程中所有非守護線程都執行完畢了,則守護線程自動銷毀。典型的守護線程有垃圾回收線程。

線程對象thread通過調用setDaemon(true)設置當前線程為守護線程。

二、   對象及變量的並發訪問

2.1 synchronized關鍵字

多個線程共同訪問1個對象中的實例變量就有可能造成非線程安全,為了解決此問題引入synchronized關鍵字,此關鍵字可作用在變量、方法、代碼塊。

該關鍵字取得的鎖都是對象的鎖,而不是把一段代碼或方法當作鎖,哪個線程執行帶此關鍵字的方法或代碼塊,或訪問帶此關鍵字的變量,哪個線程就持有該方法、代碼塊、變量所屬的對象鎖。

只有共享資源的讀寫訪問才需要同步化,如果不是共享資源沒有同步的必要。

關鍵字synchronized具有鎖重入的特性,在一個synchronized方法或代碼塊的內部調用本類的其他synchronized方法或代碼塊時是永遠可以得到的。

出現異常時,鎖自動釋放。

鎖不具備繼承性,例如父類A擁有同步方法a,其子類B重寫方法a,但在方法聲明時未添加synchronized關鍵字,則B類中的方法a並不具有同步性。

同步方法與同步代碼塊的差異性:

同步方法是對當前對象進行加鎖,而同步代碼塊是對任一對象進行加鎖,同步方法會使方法內所有操作流程進行排隊機制,排隊就會效率降低,而同步代碼塊只針對涉及到線程安全的地方進行加鎖,減少互斥訪問的代碼塊,從而在保證線程安全的前提下盡可能的提升程序運行效率。

靜態同步synchronized方法:

靜態同步方法是對當前的*.java文件對應的類進行加鎖,在同一個類中既有靜態同步方法,又有非靜態同步方法,其分別持有的是不同的鎖,非靜態同步方法持有的鎖是對象的鎖。

Synchronized(class)代碼塊的作用與synchronized static 方法的作用是一樣的,都是鎖在*.java 文件上。

多線程的死鎖:

當有不同的線程都等待在根本不可能釋放的鎖上時就會造成死鎖,因此在設計同步訪問時必須要避免此問題。

鎖對象發生改變不會影響同步效果,只要對象不對,即使對象的屬性發生改變,運行的結果還是同步的。

2.2 volatile關鍵字

引入線程堆棧、公共堆棧的概念,JVM為提高程序運行效率,在線程運行時,會將程序片段加入到當前工作的工作內存中即為線程的私有堆棧,運行期間只從私有堆棧中讀取數據,當多個線程訪問公共資源時,每個線程將公共資源引入到自己線程的私有堆棧中,當線程執行完畢后將值同步到公共堆棧中,這就會造成公共資源值不同步的結果,引入volatile關鍵字強制使線程每次從公共堆棧中讀取共享資源值,此關鍵字增加了共享資源在多個變量之間的可見性。但是此關鍵字的缺點是不支持原子性。

volatile與synchronized的比較:

  1. volatile是線程的輕量級實現,性能要高於synchronized關鍵字;volatile只能修飾變量,而synchronized可修飾變量、方法、代碼塊,隨着JDK新版本的發布synchronized關鍵字的效率在逐步提高。
  2. 多線程訪問volatile不會發生阻塞,而synchronized會發生阻塞,這正是其效率高的原因,也是其不支持原子性的根源。
  3. volatile能保證數據的可見性,但不能保證原子性;而synchronized既能保證原子性,也能通過鎖機制間接保證數據的可見性。

線程安全主要包含原子性及可見性兩個方面,synchronized還包含有互斥性。

volatile關鍵字使用場景,當實例變量發生變化時,並且多個線程需要獲得最新的值使用,此時聲明帶有此關鍵字的變量,當有synchronized關鍵字出現時volatile關鍵字是多余的。

三、   線程通信

首先說為什么要進行線程間的通信,線程是進程中子任務,多個線程之間彼此互相獨立,通過線程之間的通信增加其交互性,在提高CPU的同時,還能夠對多個線程進行有效的把控與監督。

3.1      等待/通知機制。

等待通知的經典案例就是生產者消費者模型

Java 中用wait()方法使當前線程進入等待狀態,並且在wait()所在代碼行處停止,直到接到通知或被中斷為止,調用此方法前線程必須獲得該對象的對象鎖,因此只能在同步方法或同步代碼塊中調用此方法,在執行wait()方法后釋放鎖。

Java中用notify()方法實現通知,調用前線程同樣需要獲得對象鎖,該方法用來通知那些可能等待在該對象的對象鎖上的線程,如果有多個線程則由線程規划器挑選其中一個呈wait狀態的線程。帶參數的wait(long)方法的功能是等待某一時間內是否有線程對鎖進行喚醒,如果超過這個時間則自動喚醒。執行notify()方法之后,當前線程不會馬上釋放該對象鎖,要等到執行notify()方法的線程將程序執行完,也就是說退出synchronized代碼塊之后,當前線程才會釋放鎖。notify()方法可以使等待隊列中的其中一個線程喚醒,也就是進入可執行狀態,notifyAll()方法,使等待在某一對象鎖的線程全部喚醒進入可執行狀態。

中斷呈wait狀態的線程會拋出異常,即當線程調用wait()后未被喚醒時調用interrupt()方法會拋出線程中斷異常InterruptedException。

生產者消費者模型中,一生產一消費的模型可正常執行,多消費者多生產者時容易出現假死,所謂假死就是所有線程進入到wait狀態,原因就是模型采用notify()方法喚醒某一個等待在鎖上的線程,而喚醒的線程有可能是同類,也是就說生產者喚醒的可能仍是生產者,這就導致進入假死狀態,這也是notify()方法的弊端,在這里notifyAll()方法可避免此假死狀態,因為其喚醒了所有等待在相同對象鎖上的線程,包括同類以及異類。雖然notifyAll()方法解決了此問題,但在效率上不可忽視,線程切換的開銷不可忽略,后面我們會提到只喚醒異類線程的問題。

3.2      通過管道實現線程間的通信

Java提供一種特殊的流——管道流(pipeStream)來實現線程間的通信,JDK中提供4個類可以實現線程間的通信:

PipeInputStream和PipeOutputStream

PipedReader和PipedWriter

流操作與常用IO流無差別,只需要將輸出流與輸入流建立連接即可,如inputStream.connect(outputStream),或者outputStream.connect(inputStream)都可以,字符管道流與此類似

3.3      聯合線程的使用

使用join()方法來實現線程的聯合,例如在線程b中調用a.join()方法,則b線程必須等待線程a執行完畢后才銷毀。

使用場景就是母線程需要子線程執行完畢時才結束。

使用聯合線程可有效的控制已知線程的執行順序,聯合線程具有使線程排隊的功能,類似同步的運行效果,但與synchronized有本質的區別,join()方法在內部調用wait()方法進行等待而synchronized是使用對象監視器的原理,中斷正在聯合中的線程會拋出異常,如上述例子中,線程a未執行完畢,此時b線程調用interrupt()方法會拋出異常。

join(long)方法設置等待時間,超過設置時間母線程繼續執行。

join(long)與sleep(long)區別:

此二方法在某些情況的使用上可達到相同的效果,主要區別來自其同步的原理不同,join(long)方法內部采用的wait(剩余時間),當調用此方法時就會釋放當前對象的鎖,而sleep(long)方法不釋放鎖。

3.4      ThreadLocal類的使用

多個線程共享一個變量值可以使用public static 變量的形式,ThreadLocal可以實現多個線程共用一個ThreadLocal對象但每一個線程都有自己的共享變量。可以理解為ThreadLocal對象是一個線程倉庫,每個線程需要放入自己的共享變量時,倉庫為其分配一個獨立的車間,各車間之間互不影響,這是ThreadLocal的隔離性。

例如:threadLocal是一個ThreadLocal<T>對象

線程A第一次調用threadLocal.get()時為空,線程A可通過threadLocal.set(object)進行倉儲,此時線程B第一次調用threadLocal.get()時也為空,因為A、B線程分配了不同的車間,此時B也可進行倉儲,而兩線程倉儲之后在分別進行取值也都不影響。

 也可通過繼承InheritableThreadLocal類並重寫其初始化值得方法,使倉庫的每個車間都不為空但也都仍然彼此獨立。也可以重寫其childValue(Object parentValue)方法對其倉儲的值進行修改。

四、   Lock的使用

首先說一下為什么已經有synchronized關鍵字還要引入Lock接口這種神奇的東西呢,記得上面提到的假死,提到notify/notifyAll喚醒所有等待在相同鎖上的線程,如果是喚醒同類線程,那么無疑多了一次線程切換增加了系統開銷,而Lock接口的出現,可以手動獲取和釋放指定鎖,比如我們生產者消費者模型中,聲明有生產鎖和消費鎖兩種鎖,每生產完一個物品時喚醒消費鎖,每消費完一個物品時喚醒生產鎖,保證系統切換的線程是異類線程,從而提高系統性能。Lock接口有兩個實現類。

4.1 ReentrantLock

聲明鎖:

Lock lock=new ReentrantLock();

獲取鎖:

lock.lock();

釋放鎖:

lock.unlock();

其中獲取鎖與釋放鎖之間的代碼區即為同步區。

使用Condition實現等待通知機制:

Condition condition=lock.newCondition();

等待:

condition.await();

通知:

condition.siganl()/signalAll();

同樣執行等待通知的操作都必須獲得鎖,也就是執行lock.lock()方法。

公平鎖與非公平鎖:

Lock鎖分為公平鎖與非公平鎖,公平鎖的意思就是CPU根據線程進入就緒狀態的順序調度線程,反之為非公平鎖。

公平鎖聲明方式:

Lock lock=new ReentrantLock(true);//false表示非公平鎖

Lock鎖常用的接口方法:

int getHoldCount();//返回等待在此鎖上的線程數

int getQueueLength();//返回正在等待獲取此鎖的線程估計數

int getWaitQueueLength(Condition condition);//返回等待與此鎖相關的給定條件的線程估計數

boolean hasQueueThread(Thread thread);//查詢指定線程是否正在等待獲取此鎖

boolean hasQueueThreads();//查詢是否有線程正在等待獲取此鎖

boolean hasWaiters(Condition condition);//查詢是否有線程正在等待與此鎖有關的condition條件

boolean isFair();//判斷是否為公平鎖

boolean isHeldByCurrentLock();//查詢當前線程是否獲取了此鎖定

boolean isLock();//查詢是否有線程持有此鎖

void lockInterruptibly();//如果當前線程未被中斷則獲取鎖,如果已經中斷則拋出異常

boolean tryLock();//如果當前鎖未被其他線程保持則由當前線程保持並返回true,否則返回false

Boolean tryLock(long timeout,TimeUnit unit);//timeout 時間長度 ,unit指定timeout類型  TimeUnit.SECONDS 等等,表示如果在指定時間內獲取到鎖就返回true,否則返回false

void awaitUninterruptibly();//通過condition.awaitUninterruptibly()調用,造成當前線程一直處於等待狀態,直到condition條件被喚醒,在等待過程中如果線程被中斷不會拋出異常,這是與線程直接調用interrupt()方法的區別

boolean awaitUntil(Date deadline);//指定condition條件等待到某一時刻,但可通過signal()方法提前喚醒

4.2 ReenTrantReadWriteLock

ReentrantLock的鎖具有強互斥作用,就是同一時間內只有一個線程可以執行同步區代碼,ReenTrantReadWriteLock的出現改善了此效率低下的問題,此鎖稱作讀寫鎖:

Lock lock=new ReenTrantReadWriteLock();

讀鎖獲取與釋放:

lock.readLock().lock();

lock.readLock().unlock();

寫鎖獲取與釋放:

lock.writeLock().lock();

lock.writeLock().unlock();

其中讀與讀之間不互斥,讀與寫互斥,寫與寫互斥

有關synchronized實現同步的地方lock接口都可以代替實現,且lock有一些更為方便的接口方法,而在並發中大量的類使用lock作為同步的處理方式。

五、   定時器Timer

為什么把定時器Timer歸類的線程中,原因是TimerTask是個抽象類,實現其需要重寫run()方法,而恰好可以將定時執行的任務置於run()方法內部,通過timer.schedule(..)調用。

Timer核心的地方就是有多個重載方法方便使用:

 

經過delay(ms)后開始進行調度,僅僅調度一次

public void schedule(TimerTask task, long delay)

 

在指定的時間點time上調度一次

public void schedule(TimerTask task, Date time)

 

在delay(ms)后開始調度,而后以周期period(ms)調度

public void schedule(TimerTask task, long delay, long period)

 

在指定時間firstTime時間調度,而后以周期period(ms)調度

public void schedule(TimerTask task, Date firstTime, long period)

 

timer.scheduleAtFixedRate(…)同樣也有多個重載,與timer.schedule(…)在功能上無差別,唯一的區別就是前者具有追趕性,比如第一次開始執行的時間是9點10分10秒,而現在的時間是9點11分10秒,前者會擠時間追趕執行丟掉時間區間內的任務,而后者則不會。

 

六、   單例模式下的多線程

單例模式就是在整個進程中有且僅有一個實例化對象,多個線程之間共享此對象。

單例模式有兩種方式,分別為餓漢模式和懶漢模式。

餓漢模式就是在任何線程調用之前已經實例化了唯一的對象,在整個系統保證是單例的,缺點是如果沒有線程調用那這個對象的實例化就是多余的,類的加載機制不受人為控制增加了系統的開銷。

懶漢模式就是在線程調用時判斷對象是否已經實例化,若已經實例化則直接返回,若還未實例化則實例化后返回,換句話說可以控制類的加載機制,這樣在沒有線程調用時可節省系統開銷,缺點是在多線程環境下,容易造成非單例的情況。

解決懶漢模式下幾種保證單例模式的方案:

在獲取單例對象的方法加入synchronized關鍵字,無疑可以解決,互斥訪問效率必然低下。

使用DCL雙重檢測模式,具體代碼如下:

 

package singleton;

/**
 * DCL雙重檢查實現單例模式
 * 
 * @author Together
 *
 */
public class MySingleton {
    
    private static MySingleton mySingleton;
    
    //私有構造  限制為單例模式
    private MySingleton() {
        
    }
    
    public static MySingleton getInstance() {
        if(mySingleton!=null) {
            return mySingleton;
        }else {
            synchronized (MySingleton.class) {
                if(mySingleton==null) {
                    mySingleton=new MySingleton();
                }
            }
            return mySingleton;
        }
    }
    
}

 

使用靜態內置類實現單例模式,代碼如下:

package singleton;

/**
 * 使用靜態內部類實現單例模式
 * 
 * @author Together
 *
 */
public class MySingletonStaticInnerClass {

    private static class SingletonHolder {
        private static final MySingletonStaticInnerClass MY_SINGLETON_STATIC_INNER_CLASS
        =new  MySingletonStaticInnerClass();
    }
    
    private  MySingletonStaticInnerClass() {
    }
    
    public static MySingletonStaticInnerClass getInstance() {
        return SingletonHolder.MY_SINGLETON_STATIC_INNER_CLASS;
    }
    
}

 

該方式下的單例由靜態內部類SingletonHolder的餓漢模式保證,由於內部類只有外部類的getInstance()調用,因此內部類被加載的時機也就是第一次調用getInstance()的時候,從內部看是一個餓漢模式,從外部看又的確是懶漢模式。

 使用枚舉特性實現單例模式,代碼如下:

package singleton;

/**
 * 使用枚舉實現單例模式
 * 
 * @author Together
 *
 */
public enum MySingletonEnum {

    singleton;
    private Temp temp;//單例對象
    private  MySingletonEnum() {
        temp=new Temp();
        System.out.println(temp.hashCode());
    }
    public Temp getTemp() {
        return temp;
    }
}

 

利用枚舉在使用時才調用其構造方法的原理,從而控制了單例對象的加載時機,有效實現了懶漢模式下的單例。關於枚舉的特性可以單獨寫篇文章進行論述。由於使用枚舉實現單例模式代碼簡潔、自動序列化機制、線程安全等等諸多優點,此方式成為實現懶漢模式下的單例的最佳選擇。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM