Java並行程序基礎總結

目錄:

     1-1線程概念

    2-1.線程的基本操作

    2-2 synchronied和同步問題

    2-3 volatile關鍵字的理解和使用JMM

    3-1 jdk併發包-ReentrantLock 

    3-2 常用的線程池模式以及不同線程池的使用場景

    4-2 Thread local

    4-1 提高鎖性能的建議

   5-1 CAS

1-1 線程概念

1. 同步(Synchronous)和異步(Asynchronous

image.png

2. 併發和並行

image.png

3. 臨界區

表示一種公共資源或者說是共享數據,可以被多個線程使用。但是每一次,只能有一個線程使用它,一旦臨界區資源被佔用,其他線程想要使用這個資源,就必須等待。

4. 阻塞與非阻塞

IMG_20180103_110008.jpg

5. 死鎖、飢餓、活鎖

死鎖:在一組線程中每個線程都在等待僅由該組線程中的其他線程才能觸發的條件。

飢餓:是指某一個或者多個線程因爲種種原因無法獲取所需要的資源,導致一直無法執行。(比如線程級別太低)

活鎖:兩個線程在交替掌握資源,但又不能完整使用。出現沒有一個線程可以同時拿到所有資源而正常執行。

6. 併發級別:阻塞、無飢餓、無阻礙、無鎖、無等待

7. 併發的兩個重要定律:AmdahlGustafson定律

2-1.線程的基本操作

2.1 創建線程

創建線程的第一種方式:繼承Thread類。

步驟:

1,定義類繼承Thread

2,複寫Thread類中的run方法。

目的:將自定義代碼存儲在run方法。讓線程運行。

3,調用線程的start方法,

該方法兩個作用:啓動線程,調用run方法。

 

多線程的一個特性:

隨機性。誰搶到誰執行,至於執行多長,cpu說的算。

 

線程都有自己默認的名稱Thread-編號 該編號從0開始

 

創建線程的第二種方式:實現Runable接口

步驟:

1,定義類實現Runnable接口

2,覆蓋Runnable接口中的run方法。

將線程要運行的代碼存放在該run方法中。

3,通過Thread類建立線程對象。

4,將Runnable接口的子類對象作爲實際參數傳遞給Thread類的構造函數。

爲什麼要將Runnable接口的子類對象傳遞給Thread的構造函數。因爲,自定義的run方法所屬的對象是Runnable接口的子類對象。所以要讓線程去指定指定對象的run方法。就必須明確該run方法所屬對象。

5,調用Thread類的start方法開啓線程並調用Runnable接口子類的run方法。


Thread和Runnable的區別:

    避免點繼承的侷限,一個類可以繼承多個接口。

    適合於資源的共享

 

創建線程的第種方式:通過 Callable Future 創建線程

 

創建線程的第種方式:通過線程池創建線程

2.1.1 Java創建線程後直接調用start()run()的區別

01)調用run()方法跟普通方法一樣按序執行,可以重複多次調用。

02)調用start()方法就是創建一個線程和主線程交替執行,不能多次啓動一個線程。

用戶線程和守護線程

 

Start()方法用來啓動一個線程,真正實現多線程,無需等待run方法代碼的執行完畢,通過thread類的start()方法來啓動一個線程,這時線程處於就緒狀態,並沒有執行,而是通過執行thread類的run()方法,執行線程體。

 

1.start()方法來啓動線程,並在新線程中運行 run()方法,真正實現了多線程運行。這時無需等待 run 方法體代碼執行完畢,可以直接繼續執行下面的代碼;通過調用 Thread 類的 start()方法來啓動一個線程,這時此線程是處於就緒狀態,並沒有運行,然後通過此 Thread 類調用方法 run()來完成其運行操作,這裏方法 run()稱爲線程體,它包含了要執行的這個線程的內容,run()方法運行結束,此線程終止。然後 CPU 再調度其它線程。

2.直接調用 run()方法的話,會把 run()方法當作普通方法來調用,會在當前線程中執行 run()方法,而不會啓動新線程來運行 run()方法。程序還是要順序執行,要等待 run 方法體執行完畢後,纔可繼續執行下面的代碼; 程

序中只有主線程——這一個線程, 其程序執行路徑還是隻有一條, 這樣就沒有達到多線程的目的。

 

2.2 終止線程

Stop():當一個線程對文件上鎖,進行寫操作,當stop()後,文件鎖立即被釋放,但操作執行了一半才,另外一個線程進來就會有問題。

線程中斷

建議使用”異常法”來終止線程的繼續運行。在想要被中斷執行的線程中,調用 interrupted()方法,該方法用來檢驗當前線程是否已經被中斷,即該線程是否被打上了中斷的標記,並不會使得線程立即停止運行,如果返回 true,則拋出異常,停止線程的運行。在線程外,調用 interrupt()方法,使得該線程打上中斷的標記。

Thread.interrupt():是一個實例方法,通知目標線程中斷,也就是設置中斷標誌位。中斷標誌位表示當前線程已經被中斷。

Thread.isInterrupted():實例方法,判斷當前線程是否被中斷(通過檢查中斷標誌位)

Thread.interrupted():判斷當前中斷標誌位,但也同時會清除當前線程的中斷標誌位狀態

 

注:Thread.sleep()會讓當前線程休眠一段時間,會拋出InterruptedException中斷異常。InterruptedException不是運行時異常,也就是說程序必須捕獲並且處理它,當線程在sleep休眠時,如果被中斷,就會產生。

2.3 等待(wait)和通知(notify)

Object.wait()方法不是可以隨便調用,必須包含在synchronized語句中,無論是wait或者notify都需要先獲得目標對象的一個監視器。

圖片1.png

2.4 掛起(suspend)和繼續執行(resume)線程

都是廢棄線程,因爲當掛起意外的在suspend前執行,那麼掛起的線程就可能很那有機會被繼續執行,而且被佔用的鎖不會被釋放,可能導致整個系統工作不正常。

 

2.5 等待線程結束(join)和謙讓(yield)

Join:會讓其他線程等待此線程執行完畢再執行

Yield:會讓出當前CPU,然後重新去競爭

 

2-2 synchronied和同步問題

01)同步問題出現的原因:當多條語句在操作同一個 線程共享數據 時,一個線程對多條語句只執行了一部分,還沒有執行完,另一個線程參與進來執行。導致共享數據的錯誤。

02)同步的方法:

同步代碼塊(鎖是Object對象或其子類)、同步函數(函數需要被對象調用。那麼函數都有一個所屬對象引用。就是this。所以同步函數使用的鎖是this(當前對象))、靜態的同步方法,使用的鎖是該方法所在類的字節碼文件對象:類名.class

03)synchronizedstatic synchronized的區別

正確的加鎖程序:(注意main函數,是同步一個對象,裏面synchronized同步一個實例方法,所以它或得本類對象爲鎖)

圖片2.png 

下面是一個有問題的同步程序:(主要看main方法中的對象,對象不同,synchronized不能對不同實例對象做到同步,所以可以用static synchronized

 

圖片3.png圖片4.png

2-3 volatile關鍵字的理解和使用JMM

JMM(Java內存模型)

    原子性:指一個操作不可中斷,一個線程開始執行,不會被其他線程干擾。

    可見性;是指當一個線程修改了某一個共享變量的值,其他線程可以立即知道這個修改。

    有序性:併發時,程序的執行順序可能出現亂序(比如:寫在前面的代碼,會在後面執行),是因爲程序在執行時,可能會進行指令重排。(volatile修飾的變量不會發生指令重排)

 

volatile 變量提供了線程的可見性,並不能保證線程安全性和原子性。

什麼是線程的可見性:

鎖提供了兩種主要特性:互斥(mutual exclusion 和可見性(visibility)。互斥即一次只允許一個線程持有某個特定的鎖,因此可使用該特性實現對共享數據的協調訪問協議,這樣,一次就只有一個線程能夠使用該共享數據。可見性要更加複雜一些,它必須確保釋放鎖之前對共享數據做出的更改對於隨後獲得該鎖的另一個線程是可見的 —— 如果沒有同步機制提供的這種可見性保證,線程看到的共享變量可能是修改前的值或不一致的值,這將引發許多嚴重問題。

具體看volatile的語義:

1)保證了不同線程對這個變量進行讀取時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。(volatile 解決了線程間共享變量的可見性問題)

第一:使用 volatile 關鍵字會強制將修改的值立即寫入主存;

第二:使用 volatile 關鍵字的話,當線程 2 進行修改時,會導致線程 1 的工作內存中緩存變量 stop 的緩存行無效(反映到硬件層的話,就是 CPU L1或者 L2 緩存中對應的緩存行無效);

第三:由於線程 1 的工作內存中緩存變量 stop 的緩存行無效,所以線程 1再次讀取變量 stop 的值時會去主存讀取。

2)禁止進行指令重排序,阻止編譯器對代碼的優化。

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

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

II)在進行指令優化時,不能把 volatile 變量前面的語句放在其後面執行,也不能把 volatile 變量後面的語句放到其前面執行。

volatile需要配合鎖去使用才能實現原子性,否則在多線程操作的情況下依然不夠用,在程序中,count變量(當前Segment中的key-value對個數)通過volatile修飾,實現內存可見性(關於內存可見性以後會仔細去記錄,這裏列出大概的一個流程)在有鎖保證了原子性的情況下

  • 當我們讀取count變量的時候,會強制從主內存中讀取count的最新值

  • 當我們對count變量進行賦值之後,會強制將最新的count值刷到主內存中去

  • 通過以上兩點,我們可以保證在高併發的情況下,執行這段流程的線程可以讀取到最新值

2-4 Atomic

Atomic類的作用

· 使得讓對單一數據的操作,實現了原子化

· 使用Atomic類構建複雜的,無需阻塞的代碼

訪問對2個或2個以上的atomic變量(或者對單個atomic變量進行2次或2次以上的操作)通常認爲是需要同步的,以達到讓這些操作能被作爲一個原子單元。

 

無鎖定且無等待算法

基於 CAS compare and swap)的併發算法稱爲 無鎖定算法,因爲線程不必再等待鎖定(有時稱爲互斥或關鍵部分,這取決於線程平臺的術語)。無論 CAS 操作成功還是失敗,在任何一種情況中,它都在可預知的時間內完成。如果 CAS 失敗,調用者可以重試 CAS 操作或採取其他適合的操作。


3-1 jdk併發包-ReentrantLock

1.1 什麼是reentrantlock

java.util.concurrent.lock 中的 Lock 框架是鎖定的一個抽象,它允許把鎖定的實現作爲 Java 類,而不是作爲語言的特性來實現。這就爲 Lock 的多種實現留下了空間,各種實現可能有不同的調度算法、性能特性或者鎖定語義。 ReentrantLock 類實現了 Lock ,它擁有與 synchronized 相同的併發性和內存語義,但是添加了類似鎖投票、定時鎖等候和可中斷鎖等候的一些特性。此外,它還提供了在激烈爭用情況下更佳的性能。(換句話說,當許多線程都想訪問共享資源時,JVM 可以花更少的時候來調度線程,把更多時間用在執行線程上。)

1.2 ReentrantLock與synchronized的比較

相同:ReentrantLock提供了synchronized類似的功能和內存語義。

不同:

(1)ReentrantLock功能性方面更全面,比如時間鎖等候,可中斷鎖等候,鎖投票等,因此更有擴展性。在多個條件變量和高度競爭鎖的地方,用ReentrantLock更合適,ReentrantLock還提供了Condition,對線程的等待和喚醒等操作更加靈活,一個ReentrantLock可以有多個Condition實例,所以更有擴展性。

(2)ReentrantLock 的性能比synchronized會好點。

(3)ReentrantLock提供了可輪詢的鎖請求,他可以嘗試的去取得鎖,如果取得成功則繼續處理,取得不成功,可以等下次運行的時候處理,所以不容易產生死鎖,而synchronized則一旦進入鎖請求要麼成功,要麼一直阻塞,所以更容易產生死鎖。

 

可重入鎖:

ReentrantLock 和 synchronized 都是可重入鎖。

如果當前線程已經獲得了某個監視器對象所持有的鎖,那麼該線程在該方法中調用另外一個同步方法也同樣持有該鎖。

1.3 ReentrantLock擴展的功能(三個建鎖方式)

Lock.lock();lock.lock();可以寫多次:可重入

1.3.1 實現可輪詢的鎖請求 tryLock

Lock.lock();lock.lock();可以寫多次:可重入

在內部鎖中,死鎖是致命的——唯一的恢復方法是重新啓動程序,唯一的預防方法是在構建程序時不要出錯。而可輪詢的鎖獲取模式具有更完善的錯誤恢復機制,可以規避死鎖的發生。 
如果你不能獲得所有需要的鎖,那麼使用可輪詢的獲取方式使你能夠重新拿到控制權,它會釋放你已經獲得的這些鎖,然後再重新嘗試。可輪詢的鎖獲取模式,由tryLock()方法實現。此方法僅在調用時鎖爲空閒狀態才獲取該鎖。如果鎖可用,則獲取鎖,並立即返回值true。如果鎖不可用,則此方法將立即返回值false。此方法的典型使用語句如下: 

[java] view plain copy

1. Lock lock = ...;   

2. if (lock.tryLock()) {   

3. try {   

4. // manipulate protected state   

5. finally {   

6. lock.unlock();   

7. }   

8. else {   

9. // perform alternative actions   

10. }   

 

1.3.2 實現可定時的鎖請求 tryLock(long, TimeUnit)

 

當使用內部鎖時,一旦開始請求,鎖就不能停止了,所以內部鎖給實現具有時限的活動帶來了風險。爲了解決這一問題,可以使用定時鎖。當具有時限的活動調用了阻塞方法,定時鎖能夠在時間預算內設定相應的超時。如果活動在期待的時間內沒能獲得結果,定時鎖能使程序提前返回。可定時的鎖獲取模式,由tryLock(long, TimeUnit)方法實現。 

 

1.3.3 實現可中斷的鎖獲取請求 lockInterruptibly

可中斷的鎖獲取操作允許在可取消的活動中使用。lockInterruptibly()方法能夠使你獲得鎖的時候響應中斷。

1.3.4 公平鎖reentrantLock(boolean fair)

Public reentrantLock(boolean fair):默認是非公平,如果是公平鎖,要求系統維護一個有序隊列,因爲公平鎖維護成本高,性能低下。

1.4 ReentrantLock不好與需要注意的地方

(1) lock 必須在 finally 塊中釋放。否則,如果受保護的代碼將拋出異常,鎖就有可能永遠得不到釋放!這一點區別看起來可能沒什麼,但是實際上,它極爲重要。忘記在 finally 塊中釋放鎖,可能會在程序中留下一個×××,當有一天×××爆炸時,您要花費很大力氣纔有找到源頭在哪。而使用同步,JVM 將確保鎖會獲得自動釋放

(2) 當 JVM 用 synchronized 管理鎖定請求和釋放時,JVM 在生成線程轉儲時能夠包括鎖定信息。這些對調試非常有價值,因爲它們能標識死鎖或者其他異常行爲的來源。 Lock 類只是普通的類,JVM 不知道具體哪個線程擁有 Lock 對象。

1.5 條件變量Condition

條件變量很大一個程度上是爲了解決Object.wait/notify/notifyAll難以使用的問題。

條件(也稱爲條件隊列 或條件變量)爲線程提供了一個含義,以便在某個狀態條件現在可能爲 true 的另一個線程通知它之前,一直掛起該線程(即讓其“等待”)。因爲訪問此共享狀態信息發生在不同的線程中,所以它必須受保護,因此要將某種形式的鎖與該條件相關聯。等待提供一個條件的主要屬性是:以原子方式 釋放相關的鎖,並掛起當前線程,就像 Object.wait 做的那樣。

每一個Lock可以有任意數據的Condition對象,Condition是與Lock綁定的,所以就有Lock的公平性特性:如果是公平鎖,線程爲按照FIFO的順序從Condition.await中釋放,如果是非公平鎖,那麼後續的鎖競爭就不保證FIFO順序了。 

2.1 await* 操作

上一節中說過多次ReentrantLock是獨佔鎖,一個線程拿到鎖後如果不釋放,那麼另外一個線程肯定是拿不到鎖,所以在lock.lock()和lock.unlock()之間可能有一次釋放鎖的操作(同樣也必然還有一次獲取鎖的操作)。我們再回頭看代碼,不管take()還是put(),在進入lock.lock()後唯一可能釋放鎖的操作就是await()了。也就是說await()操作實際上就是釋放鎖,然後掛起線程,一旦條件滿足就被喚醒,再次獲取鎖!

這裏再回頭介紹Condition的數據結構。我們知道一個Condition可以在多個地方被await*(),那麼就需要一個FIFO的結構將這些Condition串聯起來,然後根據需要喚醒一個或者多個(通常是所有)。所以在Condition內部就需要一個FIFO的隊列。

2.2 signal/signalAll 操作

await*()清楚了,現在再來看signal/signalAll就容易多了。按照signal/signalAll的需求,就是要將Condition.await*()FIFO隊列中第一個Node喚醒(或者全部Node)喚醒。儘管所有Node可能都被喚醒,但是要知道的是仍然只有一個線程能夠拿到鎖,其它沒有拿到鎖的線程仍然需要自旋等待,就上上面提到的第4步(acquireQueued)。

 

1.6 允許多個線程同時訪問:信號量(semaphore)

使用信號量可以允許多個線程同時訪問某一個資源(需要指定線程數量)

1.7 readwritelock

讀寫分離鎖,可以有效減少鎖競爭,提升系統性能。

1.8 倒計時器:countdownlatch

,可以用來控制線程等待,控制線程等待到倒計時結束。

 

1.9循環柵欄(增強倒計時器)

1.10 線程阻塞工具類locksupport

可以在線程內任意位置讓線程阻塞,比起Thread.suspend(),彌補了由於resume()在前發生的危險;和wait相比,不需要先獲得某個對象的鎖,也不會拋出interruptedException

3-2 常用的線程池模式以及不同線程池的使用場景

定義:線程池根據系統自身的環境情況,有效的限制執行線程的數量,使運行效果達到最佳。提高性能,控制響應數量。

風險同步錯誤、死鎖、線程泄露

線程池的注意事項

雖然線程池是構建多線程應用程序的強大機制,但使用它並不是沒有風險的。

(1)線程池的大小。多線程應用並非線程越多越好,需要根據系統運行的軟硬件環境以及應用本身的特點決定線程池的大小。一般來說,如果代碼結構合理的話,線程數目與 CPU數量相適合即可。如果線程運行時可能出現阻塞現象,可相應增加池的大小;如有必要可採用自適應算法來動態調整線程池的大小,以提高 CPU 的有效利用率和系統的整體性能。

    線程池線程數量

    Ncpu = cpu數量

    Ucpu = 目標CPU的使用率

    W/C = 等待時間與計算時間的比率

    Nthreads = ncpu * ucpu * (1 + W/C)


(2)併發錯誤。多線程應用要特別注意併發錯誤,要從邏輯上保證程序的正確性,注意避免死鎖現象的發生。

(3)線程泄漏。這是線程池應用中一個嚴重的問題,當任務執行完畢而線程沒能返回池中就會發生線程泄漏現象。比如線程放在集合類中,當使用完畢,集合類對象沒有清理,但無法回收線程。

 

線程池常用的接口和類

timg.jpg

分類:Java通過Executors提供四種線程池

newFixedThreadPool 創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。

newSingleThreadExecutor 創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。

newCachedThreadPool創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閒線程,若無可回收,則新建線程。

newScheduledThreadPool 創建一個定長線程池,支持定時及週期性任務執行。

兩種提交任務的方法

    ExecutorService 提供了兩種提交任務的方法:

        execute():提交不需要返回值的任務

        submit():提交需要返回值的任務

execute

    void execute(Runnable command);

execute() 的參數是一個 Runnable,也沒有返回值。因此提交後無法判斷該任務是否被線程池執行成功。

ExecutorService executor = Executors.newCachedThreadPool();
executor.execute(new Runnable() {
    @Override
    public void run() {
        //do something
    }
});

submit

<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);

submit() 有三種重載,參數可以是 Callable 也可以是 Runnable。

同時它會返回一個 Funture 對象,通過它我們可以判斷任務是否執行成功。

Callable 和Runnable區別:

    相同點:

    兩者都是接口;

    兩者都可用來編寫多線程程序;

    兩者都需要調用Thread.start()啓動線程;

    不同點:

    兩者最大的不同點是:實現Callable接口的任務線程能返回執行結果;而實現Runnable接口的任務線程不能返回結果;

    Callable接口的call()方法允許拋出異常;而Runnable接口的run()方法的異常只能在內部消化,不能繼續上拋;

    注意點:

    Callable接口支持返回執行結果,此時需要調用FutureTask.get()方法實現,此方法會阻塞主線程直到獲取‘將來’結果;當不調用此方法時,主線程不會阻塞!


關閉線程池

    線程池即使不執行任務也會佔用一些資源,所以在我們要退出任務時最好關閉線程池。


有兩個方法關閉線程池:

    shutdown() 

        將線程池的狀態設置爲 SHUTDOWN,然後中斷所有沒有正在執行的線程

    shutdownNow() 

        將線程池設置爲 STOP,然後嘗試停止所有線程,並返回等待執行任務的列表

    它們的共同點是:都是通過遍歷線程池中的工作線程,逐個調用 Thread.interrup() 來中斷線程,所以一些無法響應中斷的任務可能永遠無法停止(比如 Runnable)。

3.1 線程池內部實現

可以發現,下面幾種線程池內部均使用了ThreadPoolExecutor。

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}


內部均使用了ThreadPoolExecutor:這個的構造函數裏面有

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)


1. corePoolSize:(線程池中的線程數量)

2. Maximumpoolsize:線程最大數量,

3. long keepAliveTime:當線程池線程數量超過corePoolSize時,多餘的空閒線程的存活時間。

4. TimeUnit unit:keepAliveTime的單位

5. workQueue:一個重要的就是用來存放線程的隊列BlockingQueue

常見的隊列有:直接提交的隊列(SychronousQueue)、有界隊列(ArrayBlockingQueue)、***的任務隊列(LinkedBlockingQueue)、優先任務隊列(priorityBlockingQueue

6. threadFactory:線程工廠(用來創建線程)

7. Handler(拒絕策略)

當線程池和等待隊列都滿了以後,可以使用拒絕策略:abortpolicy(拋出異常)、callerrunspolicy(會直接在調用者線程中,運行當前被丟棄的任務)、丟棄最老請求,也就是即將被執行的任務、丟棄無法處理的任務

當上面四種不滿足需求,可以通過rejectExecutionHandler接口自己定義


自定義線程:

    public static void main(String[] args) {
		MyTask task = new MyTask();
		ExecutorService es = new ThreadPoolExecutor(5,5,
				0L,TimeUnit.SECONDS,
                new SynchronousQueue<Runnable>(),  
                new ThreadFactory() {
					public Thread newThread(Runnable r) {
						Thread t = new Thread(r);
						return t;
					}
				},
                new ThreadPoolExecutor.DiscardOldestPolicy()
         );  
  
        for (int i = 1; i < 5 ;i++){  
            es.submit(task);
        }  
    }


3.2 擴展線程池

ThreadpollExecutor可以擴展,提供了beforeExecutor()、afterExecutor()、terminated()三個接口對線程池進行控制。


3.3 fork/join框架

Linux中Fork()用來創建子進程,使得系統進程可以多一個執行分支。

3.4 newFixedThreadPool此種線程池如果線程數達到最大值後會怎麼辦

創建一個可重用固定線程數的線程池,以共享的***隊列方式來運行這些線程。在任意點,在大多數 nThreads 線程會處於處理任務的活動狀態。如果在所有線程處於活動狀態時提交附加任務,則在有可用線程之前,附加任務將在隊列中等待。如果在關閉前的執行期間由於失敗而導致任何線程終止,那麼一個新線程將代替它執行後續的任務(如果需要)。在某個線程被顯式地關閉之前,池中的線程將一直存在。


3.5 線程池使用案例


定長線程池案例:

import kafka.consumer.Consumer;
import kafka.consumer.ConsumerConfig;
import kafka.consumer.ConsumerIterator;
import kafka.consumer.KafkaStream;
import kafka.javaapi.consumer.ConsumerConnector;
import kafka.message.MessageAndMetadata;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class KafkaConsumerSimple implements Runnable {
    public String title;
    public KafkaStream<byte[], byte[]> stream;
    public KafkaConsumerSimple(String title, KafkaStream<byte[], byte[]> stream) {
        this.title = title;
        this.stream = stream;
    }
    @Override
    public void run() {
        System.out.println("開始運行 " + title);
        ConsumerIterator<byte[], byte[]> it = stream.iterator();
        /**
         * 不停地從stream讀取新到來的消息,在等待新的消息時,hasNext()會阻塞
         * 如果調用 `ConsumerConnector#shutdown`,那麼`hasNext`會返回false
         * */
        while (it.hasNext()) {
            MessageAndMetadata<byte[], byte[]> data = it.next();
            String topic = data.topic();
            int partition = data.partition();
            long offset = data.offset();
            String msg = new String(data.message());
            System.out.println(String.format(
                    "Consumer: [%s],  Topic: [%s],  PartitionId: [%d], Offset: [%d], msg: [%s]",
                    title, topic, partition, offset, msg));
        }
        System.out.println(String.format("Consumer: [%s] exiting ...", title));
    }

    public static void main(String[] args) throws Exception{
        Properties props = new Properties();
        props.put("group.id", "dashujujiagoushi");
        props.put("zookeeper.connect", "zk01:2181,zk02:2181,zk03:2181");
        props.put("auto.offset.reset", "largest");
        props.put("auto.commit.interval.ms", "1000");
        props.put("partition.assignment.strategy", "roundrobin");
        ConsumerConfig config = new ConsumerConfig(props);
        String topic1 = "orderMq";
        String topic2 = "paymentMq";
        //只要ConsumerConnector還在的話,consumer會一直等待新消息,不會自己退出
        ConsumerConnector consumerConn = Consumer.createJavaConsumerConnector(config);
        //定義一個map
        Map<String, Integer> topicCountMap = new HashMap<>(); 
        topicCountMap.put(topic1, 3);
        //Map<String, List<KafkaStream<byte[], byte[]>> 中String是topic, List<KafkaStream<byte[], byte[]>是對應的流
        Map<String, List<KafkaStream<byte[], byte[]>>> topicStreamsMap = consumerConn.createMessageStreams(topicCountMap);
        //取出 `kafkaTest` 對應的 streams
        List<KafkaStream<byte[], byte[]>> streams = topicStreamsMap.get(topic1);
        //創建一個容量爲4的線程池
        ExecutorService executor = Executors.newFixedThreadPool(3);
        //創建20個consumer threads
        for (int i = 0; i < streams.size(); i++)
            executor.execute(new KafkaConsumerSimple("消費者" + (i + 1), streams.get(i)));
    }
}



週期調度任務

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

class ScheduledTask1 implements Runnable{
    AtomicInteger index = new AtomicInteger(0);
    public void run() {
        int tmp_index = index.addAndGet(1);
        System.out.println("Thread name:"+Thread.currentThread().getName()+". Task1 begin running. index:"+tmp_index+" Current time:"+System.currentTimeMillis());
        try {
            Thread.sleep(10*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Task1 end running. index:"+tmp_index+" Current time:"+System.currentTimeMillis());
    }
}

class ScheduledTask2 implements Runnable{
    AtomicInteger index = new AtomicInteger(100);
    public void run() {
        int tmp_index = index.addAndGet(1);
        System.out.println("Thread name:"+Thread.currentThread().getName()+". Task2 begin running. index:"+tmp_index+" Current time:"+System.currentTimeMillis());
        try {
            Thread.sleep(10*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Task2 end running. index:"+tmp_index+" Current time:"+System.currentTimeMillis());
    }
}

class ScheduledTask3 implements Runnable{
    AtomicInteger index = new AtomicInteger(1000);
    public void run() {
        int tmp_index = index.addAndGet(1);
        System.out.println("Thread name:"+Thread.currentThread().getName()+". Task3 begin running. index:"+tmp_index+" Current time:"+System.currentTimeMillis());
        try {
            Thread.sleep(10*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Task3 end running. index:"+tmp_index+" Current time:"+System.currentTimeMillis());
    }
}

public class ScheduledPool {
    static public void addTask() throws InterruptedException {
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
        executor.schedule(new ScheduledTask1(), 20, TimeUnit.SECONDS);
        executor.scheduleAtFixedRate(new ScheduledTask2(), 0, 5, TimeUnit.SECONDS);

        ScheduledExecutorService executor2 = Executors.newScheduledThreadPool(1);
        executor2.scheduleWithFixedDelay(new ScheduledTask3(), 0, 5, TimeUnit.SECONDS);

        Thread.sleep(50*1000);
        executor.shutdown();
        executor.shutdown();
    }

    public static void main( String[] args ) throws InterruptedException {
        addTask();
    }
}



定長線程池以及Future接口和Callable接口的代碼示例

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

class Job1 implements Callable<Boolean> {
    int id = 0;
    public Job1(int id ){
        this.id = id;
    }
    public Boolean call() throws Exception {
        System.out.println("Job1 running.id:"+id+" Current time:"+System.currentTimeMillis());
        return false;
    }
}

class Job2 implements Callable<Boolean> {
    int id =0;
    public Job2(int id ){
        this.id = id;
    }
    public Boolean call() throws Exception {
        System.out.println("Job2 running.id:"+id+" Current time:"+System.currentTimeMillis());
        Thread.sleep(15*1000);
        System.out.println("Job2 end.id:"+id+" Current time:"+System.currentTimeMillis());
        return true;
    }
}

class Job3 implements Callable<Boolean> {
    int id =0;
    public Job3(int id ){
        this.id = id;
    }
    public Boolean call() throws Exception {
        System.out.println("Job3 running.id:"+id+" Current time:"+System.currentTimeMillis());
        throw new RuntimeException("Job3 throw exception.");
    }
}

public class ThreadPool {
    static void addTask() throws InterruptedException, ExecutionException {
        int id = 0;
        ExecutorService executor = Executors.newFixedThreadPool(2);
        Future<Boolean> future1 = executor.submit(new Job1(++id));
        Future<Boolean> future2 = executor.submit(new Job2(++id));
        Future<Boolean> future3 = executor.submit(new Job3(++id));
        System.out.println("job1 is done:"+future1.isDone());
        System.out.println("job1 result:"+future1.get());
        System.out.println("job2 is done:"+future2.isDone());
        System.out.println("job2 result:"+future2.get());
        try {
            System.out.println("job3 result:"+future3.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
            e.getCause();
        }

        List<Callable<Boolean>> taskList = new ArrayList<Callable<Boolean>>();
        taskList.add(new Job1(++id));
        taskList.add(new Job2(++id));
        List<Future<Boolean>> futures = executor.invokeAll(taskList);
        for (Future<Boolean> future : futures) {
            System.out.println(future.get());
        }

        executor.shutdown();
        executor.submit(new Job1(2));
    }

    public static void main( String[] args ) throws InterruptedException, ExecutionException {
        addTask();
    }
}


工作中使用線程池的案例:

關於GlobalProperties使用配置文件,可以參考:http://blog.51cto.com/qinbin/2052625

這段程序是,在kafka上拉取數據,然後將數據放在BlockingQueue隊列中(最後一個方法),然後創建線程池使用線程去消費隊列,

關於如何創建線程:本類也是一個線程類,在構造方法中配置了線程池參數,並且建立線程池對象;同時執行本類的start方法,開啓線程去消費,在run代碼塊中,線程池中加入線程,去消費。


@Service
public class AdLogsTemplete extends Thread {
	private Logger logger = LoggerFactory.getLogger(AdLogsTemplete.class);
	private AdBlockQueue<JSONObject> adqueue;

	@Resource(type = AdlogRealTime.class)
	private AdlogRealTime adlogRealTime;
   
	// 主發送http請求的線程池參數
    private ExecutorService threadPoolMain;
    private int corePoolSize_main;
    private int maximumPoolSize_main;
    private long threadKeepAliveTime_main;
    private int threadPoolQueueSize_main;
    private  SimpleDateFormat sf = new SimpleDateFormat("yyyyMMdd");
    // ---------------------------
   
    public AdLogsTemplete() {
        // ----------------------
        // 主發送線程池參數
        corePoolSize_main = GlobalProperties.getInteger("thread.main.pool.corePoolSize");
        maximumPoolSize_main = GlobalProperties.getInteger("thread.main.pool.maxPoolSize");
        threadKeepAliveTime_main = GlobalProperties.getInteger("thread.main.pool.keepAliveTime");
        threadPoolQueueSize_main = GlobalProperties.getInteger("thread.main.pool.queueSize");
        threadPoolMain = new ThreadPoolExecutor(corePoolSize_main, maximumPoolSize_main, threadKeepAliveTime_main,
                TimeUnit.MILLISECONDS, new LimitedBlockingQueue<Runnable>(threadPoolQueueSize_main),
                new ThreadFactoryBuilder().setNameFormat("Sending Pool").build());
        
        adqueue = new AdBlockQueue<JSONObject>();
        this.start();
    }
	
	//在spring配置文件配置程序啓動就執行該類的init方法,同時開始多個線程消費BlockingQueue中的數據
	public void run() {
		while (true) {
            this.handler();
        }
	}
	
	public void handler() {
		threadPoolMain.execute(new Runnable() {
            @Override
            public void run() {
            	try {
            		//JSONObject adlog = new JSONObject();
            		JSONObject adlog = adqueue.getData();
            		//JSONObject adlog2= JSONObject.fromObject(adlog.toString());
            		//JSONObject adlog = adqueue.getData();
	    			if (adlog != null) {
	    				//System.out.println("------------------------------------");
	    				adlogRealTime.statistics(adlog);
	    			}
            	}catch (Exception e) {
                    logger.error("read queue data exception", e);
                }
            	
            }
        });
    }
	
	//將數據以list集合形式放在BlockingQueue中
	public void statistics(String json) {
		//logger.info("megJson:"+json);
		JSONObject adlog = JSONObject.fromObject(json);
		try {
			adqueue.putqueue(adlog);
			//System.out.println("adQueue.size**** = " + adqueue.getSize());
        } catch (InterruptedException e) {
            logger.error("put adUrl To queue exception,data=" + e);
        }
	}
	
}


4-1 提高鎖性能的建議

1.減少鎖持有時間

2.減小鎖粒度(concurrenthashmap

3.讀寫分離鎖來替換獨佔鎖(ReadWriteLock

4.鎖分離(LinkedBlockingQueue

5.鎖粗化:有一連串不斷請求的時候,就會合並請求

4-2 Thread local

線程局部變量,就是爲每個使用該變量的線程提供一個變量的副本,是Java中的一種較爲特殊的線程綁定機制,是每個線程都可以獨立改變自己的副本,而不會與其他線程的副本衝突。爲線程的併發問題提供了一種隔離機制。

Threadlocal通常用在一個類的成員上,多個線程訪問它時,每個線程都有自己的副本,互不干擾:private static ThreadLocal<Connection> t1=new ThreadLocal<Connection>();

內部實現原理:

ThreadLocal內部其實是一個ThreadLocalMap(可以當做map,但是不是map)來保存數據。雖然在使用ThreadLocal時只給出了值,沒有給出鍵,其實內部使用當前線程作爲鍵。

Get方法:

/**

     * Returns the value in the current thread's copy of this

     * thread-local variable.  If the variable has no value for the

     * current thread, it is first initialized to the value returned

     * by an invocation of the {@link #initialValue} method.

     *

     * @return the current thread's value of this thread-local

     */

    public T get() {

        Thread t = Thread.currentThread();

        ThreadLocalMap map = getMap(t);

        if (map != null) {

            ThreadLocalMap.Entry e = map.getEntry(this);

            if (e != null) {

                @SuppressWarnings("unchecked")

                T result = (T)e.value;

                return result;

            }

        }

        return setInitialValue();

    }

當線程退出時,Thread類會進行一些清理工作,包括清理ThreadLocalMap ,由系統執行exit()方法。

 

線程泄露,當使用連接池的時候,裏面線程數量固定,將大量的對象設置到ThreadLocal中,但是沒有清理,線程就回不去線程池,導致線程泄露。

 

想要及時回收對象,最好使用ThreadLocal.remove()方法。或者JDK允許像釋放普通變量釋放ThreadLocal。比如直接obj= null.

 

適用場景:如果共享對象對於競爭的處理容易引起性能損失,就可以,考慮使用ThreadLocal爲每個線程分配單獨的對象。


5-1 CAS

與鎖相比,使用比較交換(CAS)會使程序看起來更加複雜一些。但由於其非阻塞性,它對死鎖問題天生免疫,並且,線程間的相互影響也遠遠比基於鎖的方式要小。更爲重要的是,使用無鎖的方式完全沒有鎖競爭帶來的系統開銷,也沒有線程間頻繁調度帶來的開銷,因此,它要基於鎖的方式擁有更優越的性能。

CAS算法的過程:

IMG_20180108_095241.jpg



CAS缺點

     CAS雖然很高效的解決原子操作,但是CAS仍然存在三大問題。ABA問題,循環時間長開銷大和只能保證一個共享變量的原子操作

    1.  ABA問題。因爲CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A。

    從Java1.5開始JDK的atomic包裏提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法作用是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。

    關於ABA問題參考文檔: http://blog.hesey.net/2011/09/resolve-aba-by-atomicstampedreference.html

    

    2. 循環時間長開銷大。自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。如果JVM能支持處理器提供的pause指令那麼效率會有一定的提升,pause指令有兩個作用,第一它可以延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它可以避免在退出循環的時候因內存順序衝突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執行效率。

    

    3. 只能保證一個共享變量的原子操作。當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候就可以用鎖,或者有一個取巧的辦法,就是把多個共享變量合併成一個共享變量來操作。比如有兩個共享變量i=2,j=a,合併一下ij=2a,然後用CAS來操作ij。從Java1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,你可以把多個變量放在一個對象裏來進行CAS操作。








發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章