1. 程式人生 > >java多執行緒有哪些實際的應用場景?

java多執行緒有哪些實際的應用場景?

多執行緒使用的主要目的在於:

1、吞吐量:你做WEB,容器幫你做了多執行緒,但是他只能幫你做請求層面的。簡單的說,可能就是一個請求一個執行緒。或多個請求一個執行緒。如果是單執行緒,那同時只能處理一個使用者的請求。

2、伸縮性:也就是說,你可以通過增加CPU核數來提升效能。如果是單執行緒,那程式執行到死也就利用了單核,肯定沒辦法通過增加CPU核數來提升效能。鑑於你是做WEB的,第1點可能你幾乎不涉及。那這裡我就講第二點吧。--舉個簡單的例子:假設有個請求,這個請求服務端的處理需要執行3個很緩慢的IO操作(比如資料庫查詢或檔案查詢),那麼正常的順序可能是(括號裡面代表執行時間):

a、讀取檔案1 (10ms)

b、處理1的資料(1ms)

c、讀取檔案2 (10ms)

d、處理2的資料(1ms)

e、讀取檔案3 (10ms)

f、處理3的資料(1ms)

g、整合1、2、3的資料結果 (1ms)

單執行緒總共就需要34ms。

那如果你在這個請求內,把ab、cd、ef分別分給3個執行緒去做,就只需要12ms了。

所以多執行緒不是沒怎麼用,而是,你平常要善於發現一些可優化的點。然後評估方案是否應該使用。假設還是上面那個相同的問題:但是每個步驟的執行時間不一樣了。

a、讀取檔案1 (1ms)

b、處理1的資料(1ms)

c、讀取檔案2 (1ms)

d、處理2的資料(1ms)

e、讀取檔案3 (28ms)

f、處理3的資料(1ms)

g、整合1、2、3的資料結果 (1ms)單執行緒總共就需要34ms。

如果還是按上面的劃分方案(上面方案和木桶原理一樣,耗時取決於最慢的那個執行緒的執行速度),在這個例子中是第三個執行緒,執行29ms。那麼最後這個請求耗時是30ms。比起不用單執行緒,就節省了4ms。但是有可能執行緒排程切換也要花費個1、2ms。

因此,這個方案顯得優勢就不明顯了,還帶來程式複雜度提升。不太值得。那麼現在優化的點,就不是第一個例子那樣的任務分割多執行緒完成。而是優化檔案3的讀取速度。可能是採用快取和減少一些重複讀取。首先,假設有一種情況,所有使用者都請求這個請求,那其實相當於所有使用者都需要讀取檔案3。

那你想想,100個人進行了這個請求,相當於你花在讀取這個檔案上的時間就是28×100=2800ms了。那麼,如果你把檔案快取起來,那隻要第一個使用者的請求讀取了,第二個使用者不需要讀取了,從記憶體取是很快速的,可能1ms都不到。虛擬碼:

看起來好像還不錯,建立一個檔名和檔案資料的對映。如果讀取一個map中已經存在的資料,那麼就不不用讀取檔案了。可是問題在於,Servlet是併發,上面會導致一個很嚴重的問題,死迴圈。因為,HashMap在併發修改的時候,可能是導致迴圈連結串列的構成!!!(具體你可以自行閱讀HashMap原始碼)如果你沒接觸過多執行緒,可能到時候發現伺服器沒請求也巨卡,也不知道什麼情況!好的,那就用ConcurrentHashMap,正如他的名字一樣,他是一個執行緒安全的HashMap,這樣能輕鬆解決問題。

這樣真的解決問題了嗎,這樣雖然只要有使用者訪問過檔案a,那另一個使用者想訪問檔案a,也會從fileName2Data中拿資料,然後也不會引起死迴圈。可是,如果你覺得這樣就已經完了,那你把多執行緒也想的太簡單了,騷年!你會發現,1000個使用者首次訪問同一個檔案的時候,居然讀取了1000次檔案(這是最極端的,可能只有幾百)。What the fuckin hell!!!難道程式碼錯了嗎,難道我就這樣過我的一生!好好分析下。Servlet是多執行緒的,那麼

上面註釋的“偶然”,這是完全有可能的,因此,這樣做還是有問題。因此,可以自己簡單的封裝一個任務來處理。

以上所有程式碼都是直接在bbs打出來的,不保證可以直接執行。

多執行緒最多的場景:web伺服器本身;各種專用伺服器(如遊戲伺服器);多執行緒的常見應用場景:

1、後臺任務,例如:定時向大量(100w以上)的使用者傳送郵件;

2、非同步處理,例如:發微博、記錄日誌等;

3、分散式計算

======================================================================

在java中,每一個執行緒有一塊工作記憶體區,其中存放著被所有執行緒共享的主記憶體中的變數的值的拷貝。當執行緒執行時,它在自己的工作記憶體中操作這些變數。

為了存取一個共享的變數,一個執行緒通常先獲取鎖定並且清除它的工作記憶體區,這保證該共享變數從所有執行緒的共享記憶體區正確地裝入到執行緒的工作記憶體區,當執行緒解鎖時保證該工作記憶體區中變數的值協會到共享記憶體中。

當一個執行緒使用某一個變數時,不論程式是否正確地使用執行緒同步操作,它獲取的值一定是由它本身或者其他執行緒儲存到變數中的值。例如,如果兩個執行緒把不同的值或者物件引用儲存到同一個共享變數中,那麼該變數的值要麼是這個執行緒的,要麼是那個執行緒的,共享變數的值不會是由兩個執行緒的引用值組合而成。

一個變數時Java程式可以存取的一個地址,它不僅包括基本型別變數、引用型別變數,而且還包括陣列型別變數。儲存在主記憶體區的變數可以被所有執行緒共享,但是一個執行緒存取另一個執行緒的引數或者區域性變數時不可能的,所以開發人員不必擔心區域性變數的執行緒安全問題。

volatile變數–多執行緒間可見

由於每個執行緒都有自己的工作記憶體區,因此當一個執行緒改變自己的工作記憶體中的資料時,對其他執行緒來說,可能是不可見的。為此,可以使用volatile關鍵字破事所有執行緒軍讀寫記憶體中的變數,從而使得volatile變數在多執行緒間可見。

宣告為volatile的變數可以做到如下保證:

1、其他執行緒對變數的修改,可以及時反應在當前執行緒中;2、確保當前執行緒對volatile變數的修改,能及時寫回到共享記憶體中,並被其他執行緒所見;3、使用volatile宣告的變數,編譯器會保證其有序性。

同步關鍵字synchronized

同步關鍵字synchronized是Java語言中最為常用的同步方法之一。在JDK早期版本中,synchronized的效能並不是太好,值適合於鎖競爭不是特別激烈的場合。在JDK6中,synchronized和非公平鎖的差距已經縮小。更為重要的是,synchronized更為簡潔明瞭,程式碼可讀性和維護性比較好。

鎖定一個物件的方法:

當method()方法被呼叫時,呼叫執行緒首先必須獲得當前物件所,若當前物件鎖被其他執行緒持有,這呼叫執行緒會等待,犯法結束後,物件鎖會被釋放,以上方法等價於下面的寫法:

其次,使用synchronized還可以構造同步塊,與同步方法相比,同步塊可以更為精確控制同步程式碼範圍。一個小的同步程式碼非常有離與鎖的快進快出,從而使系統擁有更高的吞吐量。

synchronized也可以用於static函式:

這個地方一定要注意,synchronized的鎖是加在當前Class物件上,因此,所有對該方法的呼叫,都必須獲得Class物件的鎖。

雖然synchronized可以保證物件或者程式碼段的執行緒安全,但是僅使用synchronized還是不足以控制擁有複雜邏輯的執行緒互動。為了實現多執行緒間的互動,還需要使用Object物件的wait()和notify()方法。

典型用法:

在使用wait()方法前,需要獲得物件鎖。在wait()方法執行時,當前執行緒或釋放obj的獨佔鎖,供其他執行緒使用。

當等待在obj上執行緒收到obj.notify()時,它就能重新獲得obj的獨佔鎖,並繼續執行。注意了,notify()方法是隨機喚起等待在當前物件的某一個執行緒。

下面是一個阻塞佇列的實現:

synchronized配合wait()、notify()應該是Java開發者必須掌握的基本技能。

Reentrantlock重入鎖

Reentrantlock稱為重入鎖。它比synchronized擁有更加強大的功能,它可以中斷、可定時。在高併發的情況下,它比synchronized有明顯的效能優勢。

Reentrantlock提供了公平和非公平兩種鎖。公平鎖是對鎖的獲取是先進先出,而非公平鎖是可以插隊的。當然從效能上分析,非公平鎖的效能要好得多。因此,在無特殊需要,應該優選非公平鎖,但是synchronized提供鎖業不是絕對公平的。Reentrantlock在構造的時候可以指定鎖是否公平。

在使用重入鎖時,一定要在程式最後釋放鎖。一般釋放鎖的程式碼要寫在finally裡。否則,如果程式出現異常,Loack就永遠無法釋放了。synchronized的鎖是JVM最後自動釋放的。

經典使用方式如下:

Reentrantlock提供了非常豐富的鎖控制功能,靈活應用這些控制方法,可以提高應用程式的效能。不過這裡並非是極力推薦使用Reentrantlock。重入鎖算是JDK中提供的高階開發工具。

ReadWriteLock讀寫鎖

讀寫分離是一種非常常見的資料處理思想。在sql中應該算是必須用到的技術。ReadWriteLock是在JDK5中提供的讀寫分離鎖。讀寫分離鎖可以有效地幫助減少鎖競爭,以提升系統性能。讀寫分離使用場景主要是如果在系統中,讀操作次數遠遠大於寫操作。使用方式如下:

Condition物件

Conditiond物件用於協調多執行緒間的複雜協作。主要與鎖相關聯。通過Lock介面中的newCondition()方法可以生成一個與Lock繫結的Condition例項。Condition物件和鎖的關係就如用Object.wait()、Object.notify()兩個函式以及synchronized關鍵字一樣。

這裡可以把ArrayBlockingQueue的原始碼摘出來看一下:

此例項簡單實現了一個物件池,物件池最大容量為100。因此,當同時有100個物件請求時,物件池就會出現資源短缺,未能獲得資源的執行緒就需要等待。當某個執行緒使用物件完畢後,就需要將物件返回給物件池。此時,由於可用資源增加,因此,可以啟用一個等待該資源的執行緒。

ThreadLocal執行緒區域性變數

在剛開始接觸ThreadLocal,筆者很難理解這個執行緒區域性變數的使用場景。當現在回過頭去看,ThreadLocal是一種多執行緒間併發訪問變數的解決方案。與synchronized等加鎖的方式不同,ThreadLocal完全不提供鎖,而使用了以空間換時間的手段,為每個執行緒提供變數的獨立副本,以保障執行緒安全,因此它不是一種資料共享的解決方案。

ThreadLocal是解決執行緒安全問題一個很好的思路,ThreadLocal類中有一個Map,用於儲存每一個執行緒的變數副本,Map中元素的鍵為執行緒物件,而值對應執行緒的變數副本,由於Key值不可重複,每一個“執行緒物件”對應執行緒的“變數副本”,而到達了執行緒安全。

特別值得注意的地方,從效能上說,ThreadLocal並不具有絕對的又是,在併發量不是很高時,也行加鎖的效能會更好。但作為一套與鎖完全無關的執行緒安全解決方案,在高併發量或者所競爭激烈的場合,使用ThreadLocal可以在一定程度上減少鎖競爭。

下面是一個ThreadLocal的簡單使用:

輸出結果:

輸出的結果資訊可以發現每個執行緒所產生的序號雖然都共享同一個TestNum例項,但它們並沒有發生相互干擾的情況,而是各自產生獨立的序列號,這是因為ThreadLocal為每一個執行緒提供了單獨的副本。

鎖的效能和優化

“鎖”是最常用的同步方法之一。在平常開發中,經常能看到很多同學直接把鎖加很大一段程式碼上。還有的同學只會用一種鎖方式解決所有共享問題。顯然這樣的編碼是讓人無法接受的。特別的在高併發的環境下,激烈的鎖競爭會導致程式的效能下降德更加明顯。因此合理使用鎖對程式的效能直接相關。

1、執行緒的開銷

在多核情況下,使用多執行緒可以明顯提高系統的效能。但是在實際情況中,使用多執行緒的方式會額外增加系統的開銷。相對於單核系統任務本身的資源消耗外,多執行緒應用還需要維護額外多執行緒特有的資訊。比如,執行緒本身的元資料,執行緒排程,執行緒上下文的切換等。

2、減小鎖持有時間

在使用鎖進行併發控制的程式中,當鎖發生競爭時,單個執行緒對鎖的持有時間與系統性能有著直接的關係。如果執行緒持有鎖的時間很長,那麼相對地,鎖的競爭程度也就越激烈。因此,在程式開發過程中,應該儘可能地減少對某個鎖的佔有時間,以減少執行緒間互斥的可能。比如下面這一段程式碼:

此例項如果只有mutexMethod()方法是有同步需要的,而在beforeMethod(),和afterMethod()並不需要做同步控制。如果beforeMethod(),和afterMethod()分別是重量級的方法,則會花費較長的CPU時間。在這個時候,如果併發量較大時,使用這種同步方案會導致等待執行緒大量增加。因為當前執行的執行緒只有在執行完所有任務後,才會釋放鎖。

下面是優化後的方案,只在必要的時候進行同步,這樣就能明顯減少執行緒持有鎖的時間,提高系統的吞吐量。程式碼如下:

3、減少鎖粒度

減小鎖粒度也是一種削弱多執行緒鎖競爭的一種有效手段,這種技術典型的使用場景就是ConcurrentHashMap這個類。在普通的HashMap中每當對集合進行add()操作或者get()操作時,總是獲得集合物件的鎖。這種操作完全是一種同步行為,因為鎖是在整個集合物件上的,因此,在高併發時,激烈的鎖競爭會影響到系統的吞吐量。

如果看過原始碼的同學應該知道HashMap是陣列+連結串列的方式做實現的。ConcurrentHashMap在HashMap的基礎上將整個HashMap分成若干個段(Segment),每個段都是一個子HashMap。如果需要在增加一個新的表項,並不是將這個HashMap加鎖,二十搜線根據hashcode得到該表項應該被存放在哪個段中,然後對該段加鎖,並完成put()操作。這樣,在多執行緒環境中,如果多個執行緒同時進行寫入操作,只要被寫入的項不存在同一個段中,那麼執行緒間便可以做到真正的並行。具體的實現希望讀者自己花點時間讀一讀ConcurrentHashMap這個類的原始碼,這裡就不再做過多描述了。

4、鎖分離

在前面提起過ReadWriteLock讀寫鎖,那麼讀寫分離的延伸就是鎖的分離。同樣可以在JDK中找到鎖分離的原始碼LinkedBlockingQueue。

這裡需要說明一下的就是,take()和put()函式是相互獨立的,它們之間不存在鎖競爭關係。只需要在take()和put()各自方法內部分別對takeLock和putLock發生競爭。從而,削弱了鎖競爭的可能性。

5、鎖粗化

上面說到的減小鎖時間和粒度,這樣做就是為了滿足每個執行緒持有鎖的時間儘量短。但是,在粒度上應該把握一個度,如果對用一個鎖不停地進行請求、同步和釋放,其本身也會消耗系統寶貴的資源,反而加大了系統開銷。

我們需要知道的是,虛擬機器在遇到一連串連續的對同一鎖不斷進行請求和釋放的操作時,便會把所有的鎖操作整合成對鎖的一次請求,從而減少對鎖的請求同步次數,這樣的操作叫做鎖的粗化。下面是一段整合例項演示:

JVM整合後的形式:

因此,這樣的整合給我們開發人員對鎖粒度的把握給出了很好的演示作用。

無鎖的平行計算

上面花了很大篇幅在說鎖的事情,同時也提到過鎖是會帶來一定的上下文切換的額外資源開銷,在高併發時,”鎖“的激烈競爭可能會成為系統瓶頸。因此,這裡可以使用一種非阻塞同步方法。這種無鎖方式依然能保證資料和程式在高併發環境下保持多執行緒間的一致性。

1、非阻塞同步/無鎖非阻塞同步方式其實在前面的ThreadLocal中已經有所體現,每個執行緒擁有各自獨立的變數副本,因此在平行計算時,無需相互等待。這裡筆者主要推薦一種更為重要的、基於比較並交換(Compare And Swap)CAS演算法的無鎖併發控制方法。

CAS演算法的過程:它包含3個引數CAS(V,E,N)。V表示要更新的變數,E表示預期值,N表示新值。僅當V值等於E值時,才會將V的值設為N,如果V值和E值不同,則說明已經有其他執行緒做了更新,則當前執行緒什麼都不做。最後CAS返回當前V的真實值。CAS操作時抱著樂觀的態度進行的,它總是認為自己可以成功完成操作。當多個執行緒同時使用CAS操作一個變數時,只有一個會勝出,併成功更新,其餘俊輝失敗。失敗的執行緒不會被掛起,僅是被告知失敗,並且允許再次嘗試,當然也允許失敗的執行緒放棄操作。基於這樣的原理,CAS操作及時沒有鎖,也可以發現其他執行緒對當前執行緒的干擾,並且進行恰當的處理。

2、原子量操作

JDK的java.util.concurrent.atomic包提供了使用無鎖演算法實現的原子操作類,程式碼內部主要使用了底層native程式碼的實現。有興趣的同學可以繼續跟蹤一下native層面的程式碼。這裡就不貼表層的程式碼實現了。

下面主要以一個例子來展示普通同步方法和無鎖同步的效能差距:

測試結果如下:

相信這樣的測試結果將內部鎖和非阻塞同步演算法的效能差異體現的非常明顯。因此筆者更推薦直接視同atomic下的這個原子類