Java高階知識點:平行計算(外部排序) 及 死鎖分析
一. 平行計算(外部排序)
通常單機運算時將資料放入記憶體中進行計算,但隨著資料量的增大,最好是使用平行計算的方法。
1. 如何設計並行排序演算法?
在平行計算的工作中,將每一個參與的電腦當作一個節點,節點通常是比較廉價的,可通過增加節點來提高效率。分為以下三個步驟及待解決問題:
- 將資料拆分到每個節點上(如何拆分,禁止拆分資料依賴)
- 每個節點平行計算得出結果(每個節點要算出什麼結果?)
- 將結果彙總(如何彙總?)
接下來以一個典型例子——外部排序,如何排序10G個元素?
熟悉演算法的肯定知道排序演算法中效率較優的快速排序、歸併排序,它們時間複雜度為O(n*logn)。使用這些高效演算法來對10g個元素排序,也只需要幾分鐘的時間,但是問題在於記憶體大小限制,無法一次性將所有元素放入一個大陣列中!只能一部分放在放在記憶體陣列中,另一部分放在記憶體之外(硬碟或網路其它節點),這就是所謂的外部排序。
2. 歸併排序演算法
其中外部排序會使用到擴充套件的歸併排序,來簡單回顧歸併排序核心:將資料分為左右兩部分,分別排序,再把兩個有序子陣列進行歸併。此演算法重點就是歸併過程,就是兩個已排序好的陣列來比較頭部的元素,取最小值放入最終陣列中。檢視以下動畫理解:
3. 外部排序演算法
(1)三個步驟
其實擴充套件後的歸併排序演算法思想可用於外部排序中,核心演算法分為以下三個步驟:
- 第一步:將資料進行切分,例如以100m或1g元素為1一組,將每一段資料分配到節點進行排序,切分的大小符合節點記憶體大小限制。
- 第二步:這樣每個節點各自對分配資料進行排序,採用歸併排序或快速排序皆可。
- 第三步:將每個排序好的節點按照歸併演算法整合到一個節點。
(2)k路歸併演算法實現 —– 堆(PriorityQueue)
其中第一、二步實現較容易,重點在於如何將多個節點歸併到一個節點,也就是 k路歸併。如下圖所示,歸併演算法為不斷比較各個節點的頭元素,取最小值放入最終節點中,可是如何比較k個頭結點(下圖中的2,1,3,4)?
逐個比較則效率較低,熟悉資料結構的朋友此時應該想到一個資料結構——堆!堆是一棵二叉樹,具有以下特點:
- 在二叉樹上任何一個子節點都不大(或小於)於其父節點。
- 必須是一棵完全的二叉樹,即除了最後一層外,以上層數的節點都必須存在並且要集中在左側。
所以依據堆的特點,我們可以構造一棵最小二叉堆,使得頭結點一定是最小值,於是構造一個大小為k的堆,先將k個節點的頭元素插入到堆中,然後每次取出頭結點,取出來的元素屬於哪個子陣列,再新增這個子陣列的下一個元素進入堆中,來維護這個堆。
一般在編碼實現中無需重新構造堆結構,可直接使用庫中對應的 PriorityQueue優先佇列,將k個頭結點push進佇列,然後pop出頭結點,同時push進該頭結點對應子陣列的一個元素。以上就是演算法核心,其中push、pop操作都是O(logk)。
(3)緩衝區
但是,仍然存在一個問題:每個節點的記憶體可以分別容納資料,可是將所有節點歸併到一個節點時,又回到了10G,不可能全放入記憶體,到底在記憶體中放入多少?
其實只需將每個節點的最小值放入記憶體即可,例如上圖中2,1,3,4放入記憶體,但是把最小值1拿掉之後需要補充一個元素,將外部記憶體的2拿到記憶體裡來,可是外部記憶體可能在硬碟或網路,此過程相比記憶體操作會很慢,不斷讀取外部記憶體效率很低,所以採用快取區,每次讀取k個節點前部分資料到快取區(幾k或幾M)。
4. 思考編碼實現
以上思想理解後,考慮發現其程式碼實現並不容易,首先要實現歸併演算法,還要維護PriorityQueue的資料結構,獲取資料的資料來源有記憶體、外部記憶體(硬碟)。
所以歸併通過Iterable介面實現,可實現以下功能:
- 可以不斷獲取下一個元素的能力;
- 元素儲存/獲取方式被抽象,與歸併節點無關;
- Iterable merge(List< Iterable> soretedData);
- Iterable是架與記憶體和檔案層次上;
為了展示Iterable介面的功能強大,舉個例子Iterable.next()作用如下:
歸併資料來源來自 Iterable.next(),首先從每個節點呼叫Iterable<T>.next()
獲取它們最小的元素,然後push到PriorityQueue中,完畢後不斷pop元素,同時補充上對應資料來源的後續元素。具體檢視Iterable<T>.next()
操作:
- 如果快取區空,讀取下一批元素放入快取區;
- 給出快取區第一個元素;
- 可配置項:快取區大小,如何讀取下一批元素;
優點:首先歸併節點的時候不需要考慮緩衝區的問題,只需要呼叫其next()
方法,在可配置項中設定如何讀取,這樣歸併函式只需寫一次,可用在檔案上或網路上,主要實現被抽象出來。
5. 總結
將歸併排序擴充套件到外部排序的場景中,實現上有兩大重點:採用堆的資料結構(庫中的PriorityQueue)來實現歸併過程;使用Iterable介面實現資料來源的緩衝區。
二. 死鎖分析
多執行緒中最值得關注的是執行緒安全性,同一個資料在不同執行緒中被同時讀寫會出現問題,需要保證資料的安全性對其執行緒進行加鎖操作,但是鎖太多效率會降低,因此將鎖的範圍縮小,可是又會產生死鎖的問題。以一個常見的銀行取錢例子來分析防止死鎖。
void transfer(Account from, Account to, int amount){
from.setAmount(from.getAmount() - amount);
from.setAmount(to.getAmount() + amount);
}
一個很簡單的transfer
函式,模擬銀行轉賬需求,此函式在單執行緒上絕對安全,但是在多執行緒下會出現問題,例如兩個人同時在這個賬號轉錢,可是此賬號只扣了一次錢,必然是不合理的,所說對其進行加鎖,如下程式碼:
void transfer(Account from, Account to, int amount){
synchronized(from){
synchronized(to){
from.setAmount(from.getAmount() - amount);
from.setAmount(to.getAmount() + amount);
}
}
}
將from、to加鎖,這樣第一個在操作賬戶轉錢結束之前,第二個人是無法操作的。synchronized是針對物件的,同一個賬戶不可多執行緒操作。但是以上程式碼是會產生死鎖的:
- 在任何地方都可以執行緒切換,甚至在一句語句中間。
例如from.setAmount(from.getAmount() - amount);
這樣一行程式碼,在from.getAmount()
地方或減號之後會斷掉,但是在上鎖之後即使斷掉,別的使用者也進不來。
- 盡力設想最壞的情況
當from物件被別人鎖住的,我們無法上鎖,這種情況是有利的,只需等待別的執行緒做完即可。但是我們剛鎖完from物件,別的執行緒就在等待,這才是不利的情況。那to物件同理嗎?不是!如果to物件被我們上鎖,這樣我們同時擁有兩個物件的鎖,可以進行操作了;而to物件的鎖被別的執行緒鎖了,我們需要等待,這才是不利的!
死鎖出現的場景
根據以上分析總結一下最壞的情況:
- synchronized(from):別的執行緒在等待from物件;
- synchronized(to):別的執行緒已經鎖住了to物件;
因此,可能出現死鎖的情況就是: transfer(a,b,100) 和 transfer(b,a,100)同時進行,這是對雙方都很不利的情況:左邊的搶走了a的鎖,右邊的搶走了b的鎖。
形成死鎖的條件
- 互斥等待:說白了也就是要在有鎖的情況。
- hold and wait:拿到一個鎖去等待另一個鎖的狀態,其實鎖是很珍貴的資源,最好得到鎖後儘快處理完畢將其釋放。
- 迴圈等待:更槽糕的情況:例如執行緒1獲得鎖A在等待鎖B,而執行緒2獲取鎖B在等待鎖A。
- 無法剝奪的等待:在出現迴圈等待情況後,有的鎖會出現超時後自動釋放,但是若是一直等待,則必定死鎖。
防止死鎖的辦法
若要避免死鎖,根據以上四個產生死鎖的原因,逐一破解即可:
破除互斥等待:不可!鎖是保證執行緒安全的基本方法,無法實現。
破除hold and wait:可以!最關鍵的一步,就是一次性獲取所有資源。例子中的from、to物件是分成兩步獲取的,從而會形成hold and wait情況,但是通常不允許同時鎖兩個物件,因此需要對程式碼做比較大的修改:
- 暴露一個鎖名為getAmountLock,它是針對Amount的,from、to物件都可以getAmountLock,鎖的時候可以帶上一個短的超時,先鎖住from再鎖住to,當to鎖不住的時候,把from鎖放掉,過段時間再嘗試。
- 或者在這兩行的外面加一個全域性的鎖,保證可以同時拿到這兩個鎖,拿到這兩個鎖之後再將全域性的鎖釋放掉。但是需要結合實際,銀行系統中Amount的量很大,全域性鎖未必好,第一個方案較好。
破除迴圈等待:可以!按順序獲取資源。
- 讓例子中的Amount之間有序,不要先synchronized物件from,再synchronized物件to,銀行中AmountID肯定是惟一值,所以定製一個規則先處理較小值,這樣即使同時互相轉賬,也不會出現死鎖情況。
破除無法剝奪的等待:可以!加入超時。
- 設定超時時間5秒或者其它,但此方法並不理想,因為超時需要時間等待,耗時長,使用者體驗差。
總結
根據以上的分析,也許你認為第四種加入超時措施相對簡單實現,但是如此一來不能使用synchronized,還要暴露一個鎖;第二種 from.getAmountLock()
方法實現較複雜。
因此,第二種解決方法較好,即破除迴圈等待—–按順序獲取資源,出現併發時根據AmountID值先處理值較小的使用者,但是這並不是最好的解決方法,因為此解決方法重點為按順序獲取資源,而銀行賬戶中的ID順序性是我假設出來的,並非實際。
所以,最理想的解決方法還是破除hold and wait,就是一次性獲取所有資源!但是通常不允許同時鎖兩個物件,所以還是先鎖住A再鎖住B,當B鎖不住的時候,把A鎖放掉,過段時間再嘗試。
完美的解決辦法不存在!所以只能根據實際問題具體分析,選擇一個折中的辦法實現。
若有錯誤,虛心指教~
相關文章
- java知識點-高階Java
- 高階 Java 面試通關知識點整理Java面試
- 死鎖(ora-00060)以及死鎖相關的知識點
- (小白學JAVA之)Java高階特性知識點梳理Java
- JAVA高階面試必過知識點彙總Java面試
- Android高階知識點Android
- HPC高效能運算知識: 異構平行計算
- 高階 Java 必須突破的 10 個知識點!Java
- 高階Java必須突破的10個知識點!Java
- 網頁高階知識點(二)網頁
- 關於處理死鎖的一點小知識
- java第一階段知識點Java
- 高階Java程式設計師要具備哪些知識Java程式設計師
- Java常見知識點彙總(⑳)——鎖Java
- Python高階知識點學習(五)Python
- JVM知識點總覽:高階Java工程師面試必備JVMJava工程師面試
- java8平行計算Java
- python面試中較常問及的知識點梳理---高階特性Python面試
- 計算機網路知識點計算機網路
- 高階儲存知識
- [心得]APUE高階程式設計知識整理程式設計
- Java鎖——死鎖Java
- 死鎖分析
- 後端知識點總結——NODE.JS(高階)後端Node.js
- Java併發基礎-Fork、Join方式的平行計算研究分析Java
- 資料結構篇_知識點板塊_第九章外部排序資料結構排序
- MySql 三大知識點,索引、鎖、事務,原理分析MySql索引
- MySQL高階知識——Show ProfileMySql
- 高階前端知識架構前端架構
- Java高階特性增強-鎖Java
- SQ死鎖及死鎖的解決
- 平行計算π值
- Oracle平行計算Oracle
- 平行計算cuda
- 好程式設計師大資料高階班分享 Spark知識點集合程式設計師大資料Spark
- 作業系統知識回顧(4)-死鎖作業系統
- 前端知識點總結——JS高階(持續更新中)前端JS
- 計算機網路知識點總結計算機網路