JAVA多線程總結(一)

應用場景


(1) 異步處理,例如:發微博、記錄日誌等;

(2) 分佈式計算

(3) 定期執行一些特殊任務:如定期更新配置文件,任務調度(如quartz),一些監控用於定期信息採集等

(4) TOMCAT處理多用戶請求。

(5) 針對特別耗時的操作。多線程同步執行可以提高速度。例如:定時向大量(100w以上)的用戶發送郵件。

併發編程面臨的挑戰及解決思路

問題一:上下文切換。

併發不一定快於串行,因爲會有切換上下文的開銷。【切換上下文:單核併發時,cpu會使用時間片輪轉實現併發,每一次輪轉,會保留當前執行的狀態】。

 解決上下文切換開銷的辦法:

 無鎖併發編程:多線程競爭鎖時,會引起上下文切換,所以多線程處理數據時,可以用一些辦法來避免使用鎖,如將數據的ID按照Hash算法取模分段,不同的線程處理不同段的數據。

    ·CAS算法:Java的Atomic包使用CAS算法來更新數據,而不需要加鎖。

    ·使用最少線程:避免創建不需要的線程,比如任務很少,但是創建了很多線程來處理,這樣會造成大量線程都處於等待狀態。

    ·協程:在單線程裏實現多任務的調度,並在單線程裏維持多個任務間的切換。

問題二:死鎖。

死鎖是一個比較常見也比較難解決的問題,當多個線程等待同一個不會釋放的資源時,就會發生死鎖。避免死鎖可以參考下面的思路。

避免死鎖的方法:

1. 避免一個線程同時獲取多個鎖。

2. 避免一個線程在鎖內同時佔用多個資源,儘量保證每個鎖只佔用一個資源。

3. 嘗試使用定時鎖,使用lock.tryLock(timeout)來替代使用內部鎖機制。

4. 對於數據庫鎖,加鎖和解鎖必須在一個數據庫連接裏,否則會出現解鎖失敗的情況。


問題三:資源限制。

資源限制是指在進行併發編程時,程序的執行速度受限於計算機硬件資源或軟件資源。例如,服務器的帶寬只有2Mb/s,某個資源的下載速度是1M每秒,系統啓動10個線程下載資源,下載速度不會變成10Mb/s

解決資源限制思路:

1. 對於硬件資源限制,可以考慮使用集羣並行執行程序。

2. 對於軟件資源限制,可以考慮使用資源池將資源複用。比如使用連接池將數據庫和Socket連接複用,或者在調用對方webservice接口獲取數據時,只建立一個連接。

基礎示例

實現多線程基本的實現方式就是如下兩種:

繼承Thread類;

實現Runnable接口;

實際使用時,會用到線程池,還會用spring管理線程池,下面使用多線程完成幾個小例子。

示例一:多線程使用reentrantLock實現交替打印奇數偶數,代碼見壓縮包:

示例二:4個線程,兩個存錢,兩個取錢


示例三:spring管理線程池配置

停止線程

終止線程有三種方式:

(1)使用退出標誌,run()執行完以後退出【拋出異常或者return】

(2)使用stop強行停止線程,不推薦,會導致當前任務執行到一半突然中斷,出現不可預料的問題;而且stop和suspend以及resume一樣是過期作廢的方法

(3)使用interrupt中斷線程

 

interrupt()方法不會真的停止線程,而是會記錄一個標誌,這個標誌,可以由下面的兩個方法檢測到。

Thread.interrupted()測試當前線程是否停止,但是他具有清除線程中斷狀態功能,如第一次返回true,第二次調用會返回false

Thread.isInterrupted(),僅返回結果,不清除狀態。重複調用會結果一致

基於上面的邏輯,可以根據標誌來在run()裏面狀態,然後再使用interrupt()來使代碼停止,停止代碼可以使用拋出異常的方式。

 

如果在sleep裏面拋出異常停止線程,會進入catch,並清除停止狀態,使之變成false;

stop()暴力停止,已經被作廢,建議不使用;

使用stop的方法帶來的問題:1.執行到一半強制停止,可能清理工作來不及;

2.對鎖定的對象進行了解鎖,導致數據不同步,不一致。

 

return方法停止線程:

其實就是使用 打標記+return 替換 打標記+拋異常

 

# 暫停線程 與 恢復線程

suspend()暫停,resume()恢復,已經被棄用,

 缺點:

1. 獨佔,使用不當很容易讓公共的同步對象獨佔,使得其他線程無法訪問。

2. 不同步:線程暫停容易導致不同步。

yield():作用是放棄當前cpu資源,將他讓給其他任務去佔用cpu;但是放棄的時間不確定,有可能剛放棄,馬上又獲得cpu時間片;直接在run方法裏面使用即可。

線程優先級

多個線程可以設置優先級。

優先級設置:

setPriority()方法;分爲1-10 10個等級,超過這個範圍,會拋出異常。

java線程優先級可以繼承,A線程啓動B線程,那麼B與A的優先級是一樣的。

優先級高的絕大多數會先執行,但結果不是百分之百的。

對象以及變量訪問

在run裏面執行的方法,如果是同步的,則不會有線程安全問題,使用synchronized關鍵字即可保證同步。

synchronized持有的鎖是對象鎖,如果多個線程訪問多個對象,則JVM會創建多個鎖。【多個對象,多個鎖,此處對象是指加了synchronized關鍵字的方法所在的類也就是創建線程時傳入的對象,例如:

Thread a = new Thread(object1);

Thread b= new Thread(object2);

a.start();

b.start();

這種情況下線程a和b持有的是兩個不同的鎖。

# 贓讀:

讀取全局變量時,此變量已經被其他線程修改過了,就會出現贓讀。

# synchronized實際上是對象鎖。

現有A,B兩個線程,C對象,C擁有加了synchronized關鍵字的方法X1()和X2(),以及未加synchronized關鍵字的X3()方法。

當A線程訪問X1方法時,B線程想訪問X1,必須等待A執行完,釋放對象鎖;

當A在訪問X1,B想訪問X3(),無需等待,直接訪問。

當A在訪問X1,B想訪問X2(),需要等待A執行完。

# synchronized鎖重入

在synchronized方法內,調用本類的其他的synchronized方法時,總是可以成功。

如果不可重入的話,會造成死鎖;

可重入鎖,支持在父子類繼承的環境:子類可以通過"可重入鎖"調用父類的同步方法。

#異常會釋放鎖

當一個線程執行出現異常,會釋放他所持有的所有鎖。

#同步不具有繼承性

父類中A()方法是synchronized的,子類中的A方法,不會是同步的,需要手動加上。

#synchronized同步語句塊

synchronized(this){

   ...同步的代碼塊...

}

synchronized聲明方法的弊端:

A線程調用同步方法執行長時間任務時,B線程需要等待很久。

解決辦法:可以使用synchronized同步語句塊。

synchronized可以修飾代碼塊。使用synchronized修飾需要保持同步部分代碼,其餘部分異步,藉此提高運行效率。

#synchronized代碼塊間的同步性

A對象,擁有X1和X2兩個synchronized同步代碼塊,

那麼,B線程在訪問X1時,C線程也無法訪問X2,需要等待B線程釋放對象鎖。

此處與synchronized修飾方法時一樣。他們持有的都是對象鎖。

#任意對象作爲監視器

   synchronized修飾的代碼塊時,如果傳入this,則會監視當前對象,加鎖時會對當前整個對象加鎖;

    例如:

    對象A有方法X1(),X2(),如果在X1和X2裏有一段同步代碼塊,並且synchronized(this)傳入的都是this對象,那麼在B線程訪問X1的同步代碼塊時,C線程也無法X2的同步代碼塊。

如果傳入的不是this,而是另外的對象,則C可以訪問X2的同步代碼塊。

*要保證傳入其他監視對象時的成功同步,必須保證在調用時,監視對象是一致的,不能每次都new一個監視對象,否則會導致變成異步的。*

#髒讀問題

有時候,僅僅使用synchronized修飾方法,並不能保證正確的邏輯。

比如,兩個synchronized修飾的方法add()與getSize(),他們分別是對list進行讀與寫的操作,此時兩個線程先後調用這兩個方法,會導致結果超出預期。

解決:

add()方法中,synchronized改成去修飾代碼塊,並且傳入監視對象list;

synchronized(list){

   --- add  ---

}

 

#靜態同步synchronized方法,與synchronized(class)代碼塊

   synchronized加在static靜態方法上,就是對當前.java文件對應的class類進行持鎖。

   synchronized static等同於synchronized (object.class) 可以對該類的所有對象起作用,

*即:即使需要new不同的對象,也可以保持同步*

#String的常量池特性

一般不使用String變量來作爲鎖的監視對象,當對一個String變量持有鎖時,如果兩個訪問線程傳入的String變量值一樣,會導致鎖不被釋放,其中一個線程無法執行。

可以使用對象來存儲相應的變量解決此問題。

volatile關鍵字

一旦一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾之後,那麼就具備了兩層語義:

1)保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。

2)禁止進行指令重排序。

 

#volatile保證有序性

volatile關鍵字禁止指令重排序有兩層意思:

1)當程序執行到volatile變量的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對後面的操作可見;在其後面的操作肯定還沒有進行;

2)在進行指令優化時,不能將在對volatile變量的讀操作或者寫操作的語句放在其後面執行,也不能把volatile變量後面的語句放到其前面執行。

可能上面說的比較繞,舉個簡單的例子:

//x、y爲非volatile變量

//flag爲volatile變量

x = 2;        //語句1

y = 0;        //語句2

flag = true;  //語句3

x = 4;         //語句4

y = -1;       //語句5

由於flag變量爲volatile變量,那麼在進行指令重排序的過程的時候,不會將語句3放到語句1、語句2前面,也不會講語句3放到語句4、語句5後面。但是要注意語句1和語句2的順序、語句4和語句5的順序是不作任何保證的。

並且volatile關鍵字能保證,執行到語句3時,語句1和語句2必定是執行完畢了的,且語句1和語句2的執行結果對語句3、語句4、語句5是可見的。

那麼我們回到前面舉的一個例子:

//線程1:

context =loadContext();   //語句1

inited = true;             //語句2

//線程2:

while(!inited ){

  sleep()

}

doSomethingwithconfig(context);

前面舉這個例子的時候,提到有可能語句2會在語句1之前執行,那麼久可能導致context還沒被初始化,而線程2中就使用未初始化的context去進行操作,導致程序出錯。

這裏如果用volatile關鍵字對inited變量進行修飾,就不會出現這種問題了,因爲當執行到語句2時,必定能保證context已經初始化完畢。

 

 

#與synchronized對比

1. volatile是線程同步的輕量實現,只能修飾變量,性能高於synchronized

2. volatile保證可見性,不保證原子性【一旦其修飾的變量改變,其餘的線程都能發現,因爲會強制從公共堆棧取值】,synchronized保證原子性,間接保證可見性,因爲他會將私有內存和公共內存的值同步

例如:i++操作,實際上不是原子操作,他有3步:

(1).從內存取i值

(2).計算i的值

(3).將i的新值寫到內存

多個線程執行時,使用volatile,可能導致數據髒讀,進而出現錯誤。

3. 多線程訪問volatile不會阻塞,而synchronized會

4. volatile是解決變量在多個線程之間的可見性,synchronized是保證多個線程之間資源的同步性。

 

# volatile的實現原理

1.可見性

處理器爲了提高處理速度,不直接和內存進行通訊,而是將系統內存的數據獨到內部緩存後再進行操作,但操作完後不知什麼時候會寫到內存。

如果對聲明瞭volatile變量進行寫操作時,JVM會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫會到系統內存。 這一步確保瞭如果有其他線程對聲明瞭volatile變量進行修改,則立即更新主內存中數據。

但這時候其他處理器的緩存還是舊的,所以在多處理器環境下,爲了保證各個處理器緩存一致,每個處理會通過嗅探在總線上傳播的數據來檢查 自己的緩存是否過期,當處理器發現自己緩存行對應的內存地址被修改了,就會將當前處理器的緩存行設置成無效狀態,當處理器要對這個數據進行修改操作時,會強制重新從系統內存把數據讀到處理器緩存裏。 這一步確保了其他線程獲得的聲明瞭volatile變量都是從主內存中獲取最新的。

2.有序性

Lock前綴指令實際上相當於一個內存屏障(也成內存柵欄),它確保指令重排序時不會把其後面的指令排到內存屏障之前的位置,也不會把前面的指令排到內存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操作已經全部完成。

# volatile的應用場景

synchronized關鍵字是防止多個線程同時執行一段代碼,那麼就會很影響程序執行效率,而volatile關鍵字在某些情況下性能要優於synchronized,但是要注意volatile關鍵字是無法替代synchronized關鍵字的,因爲volatile關鍵字無法保證操作的原子性。通常來說,使用volatile必須具備以下2個條件:

1)對變量的寫操作不依賴於當前值

2)該變量沒有包含在具有其他變量的不變式中

下面列舉幾個Java中使用volatile的幾個場景:

①     .狀態標記量

volatile booleanflag = false;

 //線程1

while(!flag){

    doSomething();

}

  //線程2

public voidsetFlag() {

    flag = true;

}

根據狀態標記,終止線程。

②.單例模式中的doublecheck

class Singleton{

    private volatile static Singleton instance= null;

    private Singleton() {

    }

    public static Singleton getInstance() {

        if(instance==null) {

            synchronized (Singleton.class) {

                if(instance==null)

                    instance = new Singleton();

            }

        }

        return instance;

    }

}

爲什麼要使用volatile 修飾instance?

主要在於instance= new Singleton()這句,這並非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情:

 

1.給 instance分配內存

2.調用Singleton 的構造函數來初始化成員變量

3.將instance對象指向分配的內存空間(執行完這步 instance就爲非 null 了)。

但是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是後者,則在 3 執行完畢、2 未執行之前,被線程二搶佔了,這時 instance已經是非 null 了(但卻沒有初始化),所以線程二會直接返回 instance,然後使用,然後順理成章地報錯。

線程間通信

# 1.等待通知機制:

 wait使線程暫停,而notify使線程繼續運行。還有notifyAll()方法。

 wait()和notify(),兩個方法來實現等待通知機制;

 注意:(1)兩個方法在調用時都需要持有當前對象的對象鎖,所以都只能在同步代碼塊或者同步方法裏面調用,如果不是會拋出異常。

# wait:

 (2)wait方法會將當前線程置入“預執行隊列”,並在wait()所在代碼行停止執行,直到接到notify(),或者被中斷;

 (3)執行wait()後,當前線程釋放鎖;

#notify:

(1)如果多個線程在wait,那麼會由線程規劃器,挑選一個執行notify,並使他獲取該對象的對象鎖;

(2)noitfy執行之後,當前線程不會立馬釋放該對象鎖,wait狀態的線程也不能立馬獲得該對象鎖,要等執行notify()方法的線程將程序執行完,也就是退出synchronized代碼塊之後纔會釋放鎖,並讓wait獲得。

(3)多個wait的線程,第一個獲取到notify並執行完之後,其餘的wait狀態的線程如果沒有被通知,還是會一直阻塞。

#wait之後自動釋放鎖,notify之後不會立馬釋放鎖

 

#interrupt方法與wait

當線程在wait狀態時,調用對象的interrupt()方法,會拋出異常。

(1)執行完同步代碼塊之後,會釋放當前對象的鎖

(2)執行同步代碼塊過程中,拋出異常也會釋放鎖

(3)執行wait()之後,也會釋放鎖

#wait(long)

執行wait(5000)後,首先會等待5秒,如果5秒內沒有收到通知,會自動喚醒線程,退出wait狀態。

#通過管道進行線程間通信

4個類進行線程間通信:

(1)字節流:PipedInputStream和PipedOuputStream

(2)字符流:PipedReader和PipedWriter

使用語法:

輸出:PipedOuputStream

PipedOuputStream out;

out.write();

out.close();

#join方法

在主線程中調用子線程的join方法,可以讓主線程等待子線程結束之後,

再開始執行join()之後的代碼。

join可以使線程排隊運行,類似於synchronized的同步;區別在於join在內部使用wait()等待,而synchronized使用對象監視器原理同步。

#注意

在join過程中,如果當前線程對象被中斷,則當前線程出現異常,子線程會繼續運行;

#join(long)

long參數是設定等待時間,使用sleep(long)也可以等待,但二者是有區別的:

join(long),內部是使用的wait(long),等待時會釋放鎖;

sleep(long)等待時不會釋放鎖。

#ThreadLocal

變量值的共享可以使用public static;

如果想讓每個線程都有自己的共享變量。可以使用ThreadLocal;ThreadLocal可以看做全局存放數據的盒子,盒子中可以存儲每個線程的私有數據;

使用時,只需新建一個類繼承ThreadLocal即可實現,不同的線程在這個類中取到各自隔離的變量。

#InheritableThreadlocal

InheritableThreadlocal可以在子線程中取得父線程繼承下來的值。

使用注意:如果子線程取得值的同時,主線程將值進行了修改,那麼取到的還是舊值。


LOCK的使用

ReenTrantLock可以和synchronized一樣實現多線程之間的同步互斥,ReenTrantLock類在功能上還更加強大,有嗅探鎖定,多路分支通知等。

使用:

privateLock lock =  new ReenTrantLock();

try{

//加鎖

lock.lock();

//解鎖

lock.unlock();

}catch{

}

#ReenTrantLock結合Condition實現等待/通知

功能上與synchronized結合wait/notify一樣,而且更加靈活;

    一個Lock對象可以創建多個Condition(即對象監視器)實例,線程對象可以註冊在指定的condition中,從而可以有選擇性的進行線程通知,在線程調度上更加靈活。

而在wait/notify時,被通知的線程是JVM隨機選擇的,不如ReenTrantLock來得靈活。

synchronized相當於整個lock對象中只有一個單一的condition,所有的線程都註冊在它上面,線程開始notify時,需要通知所有的waitting線程,沒有選擇權,效率不高。

#使用

使用之前,必須使用lock.lock()獲取對象鎖。

privateCondition condition = lock.newCondition();

try{

    condition.await();

}catch{}

其實使用上wait()/notify()/notifyAll()相當於Condition類

裏面的await()/signal()/signalAll()

wait(longtimeout)相當於await(long time,TimeUnit unit)


















發佈了40 篇原創文章 · 獲贊 27 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章