摘要:所以接下來,我們需要簡單的介紹下多線程中的并發(fā)通信模型。比如中,以及各種鎖機(jī)制,均為了解決線程間公共狀態(tài)的串行訪問問題。
并發(fā)的學(xué)習(xí)門檻較高,相較單純的羅列并發(fā)編程 API 的枯燥被動學(xué)習(xí)方式,本系列文章試圖用一個(gè)簡單的栗子,一步步結(jié)合并發(fā)編程的相關(guān)知識分析舊有實(shí)現(xiàn)的不足,再實(shí)現(xiàn)邏輯進(jìn)行分析改進(jìn),試圖展示例子背后的并發(fā)工具與實(shí)現(xiàn)原理。
本文是本系列的第一篇文章,提出了一個(gè)簡單的業(yè)務(wù)場景,給出了一個(gè)簡單的串行實(shí)現(xiàn)以及基于原子變量的并發(fā)實(shí)現(xiàn),同時(shí)詳細(xì)分析了 Java多線程通信、 Java 內(nèi)存模型、 happy before 等基本概念。
寫在前面文中所有的代碼筆者均全部實(shí)現(xiàn)了一遍,并上傳到了我的 github 上,多線程這部分源碼位于java-multithread模塊中 ,歡迎感興趣的讀者訪問并給出建議^_^
倉庫地址:java-learning串行實(shí)現(xiàn)
git-clone:[email protected]:The-Hope/java-learning.git
假定有這樣一個(gè)需求,給定一個(gè)目錄和一個(gè)關(guān)鍵字,要求統(tǒng)計(jì)指定的目錄中各文件內(nèi)指定關(guān)鍵字出現(xiàn)的總次數(shù)。
先來看看串行狀態(tài)下該怎么實(shí)現(xiàn):
/** * Description: * 掃描指定目錄下指定關(guān)鍵字的出現(xiàn)次數(shù)——串行版本實(shí)現(xiàn) * * @author The hope * @date 2018/5/20. */ public class KeywordCount1 implements KeywordCount { private String keyword; private File directory; public KeywordCount1(File directory, String keyword) { this.keyword = keyword; this.directory = directory; } public int search() { return search(directory); } private int search(File directory) { int result = 0; for (File file : directory.listFiles()) if (file.isDirectory()) result += search(file); else result += count(file); return result; } private int count(File file) { int result = 0; try (Scanner in = new Scanner(file)) { while (in.hasNextLine()) { String line = in.nextLine(); if (line.contains(keyword)) result++; } } catch (FileNotFoundException e) { e.printStackTrace(); } return result; } @Override public void shutDown() {} }
代碼很簡單,核心實(shí)現(xiàn)是search(File directory) 函數(shù):
private int search(File directory) { int result = 0; for (File file : directory.listFiles()) if (file.isDirectory()) result += search(file); else result += count(file); return result; }
邏輯很簡單,判斷當(dāng)前 file 對象如果是文件夾就遞歸調(diào)用自己,否則統(tǒng)計(jì)關(guān)鍵字出現(xiàn)次數(shù)。(注,為了方便測試函數(shù)的調(diào)用,我抽象了接口 KeywordCount 以規(guī)范暴露出的方法)
為了看看它的執(zhí)行效果我們再來寫個(gè)簡單的測試函數(shù):
/** * Description: * 掃描指定目錄下指定關(guān)鍵字的出現(xiàn)次數(shù)——測試函數(shù) * @author The hope * @date 2018/5/20. */ public class KeywordCountTest { public static void main(String... args) throws Exception{ Scanner in = new Scanner(System.in); System.out.println("Enter base directory (e.g. C:Program FilesJavajdk1.6.0_45src): "); String directory = in.nextLine(); System.out.println("Enter keyword (e.g. java): "); String keyword = in.nextLine(); int execTimes = 5;// 設(shè)定執(zhí)行次數(shù) long start = System.currentTimeMillis();//開始計(jì)時(shí) int totalCount = 0; KeywordCount counter = new KeywordCount1(new File(directory), keyword); for (int i = 0; i < execTimes; i++) { int count = counter.search(); totalCount += count; } long end = System.currentTimeMillis();//結(jié)束計(jì)時(shí) System.out.println("Statistics: " + totalCount/ execTimes); System.out.println("used time: " + (end-start)/ execTimes); counter.shutDown(); } }
(為了消除單次運(yùn)行的波動影響,這里故意寫了個(gè)循環(huán)來做平均)
執(zhí)行效果如下:
Enter base directory (e.g. C:Program FilesJavajdk1.6.0_45src): C:Program FilesJavajdk1.6.0_45src Enter keyword (e.g. java): java Statistics: 43781 used time: 5152 Process finished with exit code 0
可以看到用時(shí)大概在5秒左右
拓展思考我們可以簡單的分析下整個(gè)功能的邏輯,大體上可以分為兩個(gè)部分:
從給定目錄尋找下級文件
從給定文件中統(tǒng)計(jì)指定關(guān)鍵字出現(xiàn)次數(shù)
其中第二步明顯是相互獨(dú)立、互不依賴且耗時(shí)較多的任務(wù),假使我們能夠引入多線程并發(fā)的去執(zhí)行那么就能合理的提升系統(tǒng)的吞吐量進(jìn)而提高系統(tǒng)響應(yīng)時(shí)間。
注意,在分析是否值得利用多線程改進(jìn)一個(gè)需求實(shí)現(xiàn)時(shí),自什么維度來進(jìn)行任務(wù)的拆分是一件比較重要的考慮因素。如果任務(wù)之間存在執(zhí)行順序依賴或者數(shù)據(jù)依賴,那么就很難簡單的對任務(wù)進(jìn)行拆分,而應(yīng)該從更高的維度重新思考任務(wù)的邊界并設(shè)計(jì)相應(yīng)的實(shí)現(xiàn)。比如,針對有執(zhí)行順序依賴的任務(wù),可以從更高維度來對任務(wù)進(jìn)行分組,并將一組任務(wù)放入一個(gè)線程中順序執(zhí)行,并通過 ThreadLocal 來傳遞變量,這樣可以有效減少數(shù)據(jù)爭用的競態(tài)條件。
引入并發(fā)在開始動筆實(shí)現(xiàn)之前,我們先來思考這么兩個(gè)問題:
1. 線程何時(shí)執(zhí)行不受我們控制,我們怎么知道線程何時(shí)能夠執(zhí)行完畢
2. 即便我們知道線程什么時(shí)候執(zhí)行完畢,可是 Java 并沒有提供線程之間顯示的通信方法,那么我們怎么獲取需要的結(jié)果。
其實(shí)這兩個(gè)問題,都是典型的線程間通信問題。比如第一個(gè)問題,換種角度看就是主線程如何接收子線程執(zhí)行完畢的信息。第二個(gè)問題更是一種典型的主線程如何接受子線程計(jì)算結(jié)果的問題。
所以接下來,我們需要簡單的介紹下多線程中的并發(fā)通信模型。
多線程間的并發(fā)通信對于多線程編程來說,最根本的就是解決兩個(gè)問題:
線程之間如何進(jìn)行通信(以何種信息來交換信息)
線程之間如何進(jìn)行同步
我們先來說說如何通信,大體上有這么兩種方式:
基于消息傳遞
基于共享內(nèi)存
消息傳遞的并發(fā)模型基于消息傳遞的并發(fā)模型中,線程之間沒有公共狀態(tài),通信基于顯式消息傳遞實(shí)現(xiàn),由于消息的接收一定存在于消息的發(fā)送之后,此時(shí)同步是隱式進(jìn)行的。
結(jié)合并發(fā)模型的介紹,我們可以很容易的知道,Thread.join() 方法就是一種很典型的線程間消息傳遞機(jī)制。他傳遞的消息就是目標(biāo)線程何時(shí)執(zhí)行完畢的信息,并兼具阻塞代碼執(zhí)行的功能。類似的消息傳遞機(jī)制還有 wait(),notifyAll() 等方法。
舉個(gè)栗子:
針對前文的第一個(gè)問題:
線程何時(shí)執(zhí)行不受我們控制,我們怎么知道線程何時(shí)能夠執(zhí)行完畢?
如下方代碼所示。通過使用 Thread.join()方法,保證代碼阻塞,直到子線程執(zhí)行完畢再繼續(xù)執(zhí)行:
class ThreadA{ public static void main(String... args){ ThreadB b = new Thread(new Runnable{ void run(){...}}); b.start(); b.join(); // join() 方法會等待線程執(zhí)行完畢。如果不加這一行將會繼續(xù)運(yùn)行下去 // do something } }共享內(nèi)存的并發(fā)模型
基于共享內(nèi)存的并發(fā)模型中,線程之間共享程序的公共狀態(tài),通信是通過線程之間串行的對公共狀態(tài)進(jìn)行讀寫來實(shí)現(xiàn)的,因此總是需要(程序員)顯示的指定同步來實(shí)現(xiàn)隱式的通信。
比如 Java 中 volatile,synchronized 以及各種鎖機(jī)制,均為了解決線程間公共狀態(tài)的串行訪問問題。
講到這里,我們還可以再宕開一筆,簡單聊聊為什么基于共享內(nèi)存的并發(fā)模型一定要花大力氣保證線程之間的串行執(zhí)行。
Java 內(nèi)存模型的抽象(JMM)類似現(xiàn)代多核處理器會給每個(gè)核心設(shè)計(jì)自己的 CPU 寄存器緩存主內(nèi)存中的目標(biāo)數(shù)據(jù),以方便處理器的快速存取。當(dāng)多個(gè)處理器的任務(wù)涉及同一塊主內(nèi)存時(shí),就需要利用 MSI、MESI、MOSI 等緩存一致性協(xié)議來協(xié)調(diào)各個(gè)處理器之間的對特定內(nèi)存或者高速緩存的訪問規(guī)則。如下圖:
針對一個(gè)線程對共享變量的寫入何時(shí)對另一個(gè)線程可見問題,Java 利用 JMM 抽象了線程與主內(nèi)存之間的關(guān)系。
我們先來看看Java內(nèi)存模型(JMM)的示意圖:
注意:這里的工作內(nèi)存并不實(shí)際存在,而是涵蓋了緩存,寫緩沖區(qū),寄存器以及其他的硬件和編譯器優(yōu)化等概念的一種抽象
從圖中就可以很清晰的歸納出,如果線程A想要和線程B之間想要通過共享內(nèi)存進(jìn)行通信,那么必須經(jīng)過以下步驟:
線程A將工作內(nèi)存中更新的工作內(nèi)存副本寫回至主內(nèi)存中
線程B從根據(jù)主內(nèi)存中的值重新更新刷新自己的工作內(nèi)存副本
上述兩步必須有序進(jìn)行,否則將會導(dǎo)致通信錯(cuò)誤。
例如考慮以下時(shí)序:
變量X初始值為100
線程 A 將 X 值寫入工作內(nèi)存中,此時(shí)工作內(nèi)存與主內(nèi)存 X 值均為100
線程 A 給 X + 50 然后寫入工作內(nèi)存中,此時(shí) A 的時(shí)間片用完。 X 在工作內(nèi)存值為 150,X在主內(nèi)存中值為100
線程 B 將 X 的值寫入自己的工作內(nèi)存中。此時(shí)線程 B 的工作內(nèi)存值為 100,主內(nèi)存值仍為 100。
線程 B 給 X + 30 然后寫入工作內(nèi)存中,此時(shí) B 的工作內(nèi)存值為 130,主內(nèi)存值為 100。
線程 B 將工作內(nèi)存的值寫回主內(nèi)存,線程 B 運(yùn)行結(jié)束。此時(shí)主內(nèi)存值為 130。
線程 A 從休眠中醒來,將工作內(nèi)存中的 150 同步回主內(nèi)存,此時(shí)主內(nèi)存值為 150。
從上述時(shí)序中,我們可以看到,由于線程 A & B 針對共享狀態(tài) X 寫入并不是串行的,導(dǎo)致中間出現(xiàn)了數(shù)據(jù)覆蓋的錯(cuò)誤情況。同理,讀者可以再繼續(xù)分析思考下寫讀模型中的同步問題。
重排序值得注意的是,除了上述例子中,線程間錯(cuò)誤的時(shí)序會導(dǎo)致并發(fā)錯(cuò)誤,重排序也同樣會導(dǎo)致意想不到的并發(fā)錯(cuò)誤。
重排序的原因大體分為這三種:
編譯期優(yōu)化的重排序(編譯器僅保證不更改單線程運(yùn)行語義)
指令級并行的重排序(處理器僅保證不破壞存在數(shù)據(jù)依賴的指令)
內(nèi)存系統(tǒng)的重排序(讀/寫緩沖區(qū)到主內(nèi)存同步機(jī)制)
關(guān)于這部分的介紹,前人珠玉在前,列舉了大量簡明易懂的例子。這里援引并發(fā)編程網(wǎng)的程曉明在《深入理解Java內(nèi)存模型》系列文章中的一個(gè)例子來給大家做個(gè)簡單介紹:
處理器重排序與內(nèi)存屏障指令現(xiàn)代的處理器使用寫緩沖區(qū)來臨時(shí)保存向內(nèi)存寫入的數(shù)據(jù)。寫緩沖區(qū)可以保證指令流水線持續(xù)運(yùn)行,它可以避免由于處理器停頓下來等待向內(nèi)存寫入數(shù)據(jù)而產(chǎn)生的延遲。同時(shí),通過以批處理的方式刷新寫緩沖區(qū),以及合并寫緩沖區(qū)中對同一內(nèi)存地址的多次寫,可以減少對內(nèi)存總線的占用。雖然寫緩沖區(qū)有這么多好處,但每個(gè)處理器上的寫緩沖區(qū),僅僅對它所在的處理器可見。這個(gè)特性會對內(nèi)存操作的執(zhí)行順序產(chǎn)生重要的影響:處理器對內(nèi)存的讀/寫操作的執(zhí)行順序,不一定與內(nèi)存實(shí)際發(fā)生的讀/寫操作順序一致!為了具體說明,請看下面示例:
Processor A | Processor B |
---|---|
a = 1; //A1 x = b; //A2 |
b = 2; //B1 y = a; //B2 |
初始狀態(tài):a = b = 0 處理器允許執(zhí)行后得到結(jié)果:x = y = 0 |
假設(shè)處理器A和處理器B按程序的順序并行執(zhí)行內(nèi)存訪問,最終卻可能得到x = y = 0的結(jié)果。具體的原因如下圖所示:
這里處理器A和處理器B可以同時(shí)把共享變量寫入自己的寫緩沖區(qū)(A1,B1),然后從內(nèi)存中讀取另一個(gè)共享變量(A2,B2),最后才把自己寫緩存區(qū)中保存的臟數(shù)據(jù)刷新到內(nèi)存中(A3,B3)。當(dāng)以這種時(shí)序執(zhí)行時(shí),程序就可以得到x = y = 0的結(jié)果。
從內(nèi)存操作實(shí)際發(fā)生的順序來看,直到處理器A執(zhí)行A3來刷新自己的寫緩存區(qū),寫操作A1才算真正執(zhí)行了。雖然處理器A執(zhí)行內(nèi)存操作的順序?yàn)椋篈1->A2,但內(nèi)存操作實(shí)際發(fā)生的順序卻是:A2->A1。此時(shí),處理器A的內(nèi)存操作順序被重排序了(處理器B的情況和處理器A一樣,這里就不贅述了)。
這里的關(guān)鍵是,由于寫緩沖區(qū)僅對自己的處理器可見,它會導(dǎo)致處理器執(zhí)行內(nèi)存操作的順序可能會與內(nèi)存實(shí)際的操作執(zhí)行順序不一致。由于現(xiàn)代的處理器都會使用寫緩沖區(qū),因此現(xiàn)代的處理器都會允許對寫-讀操作重排序。
上述引文介紹了一個(gè)簡單的小栗子,說明了重排序問題導(dǎo)致的一個(gè)并發(fā)錯(cuò)誤。既然重排序問題可能導(dǎo)致程序在并發(fā)執(zhí)行時(shí)導(dǎo)致意想不到的錯(cuò)誤發(fā)生,作為程序員我們又該怎么分析定位問題呢?
先行發(fā)生(happens before)原則雖然重排序問題會導(dǎo)致并發(fā)程序的可見性錯(cuò)誤,不過 Java 通過先行發(fā)生的概念重新約定了操作之間的可見性。
換句話說如果一個(gè)操作的執(zhí)行結(jié)果需要對另一個(gè)線程可見,那么這兩個(gè)操作之間一定要存在 happens before 關(guān)系。這里的兩個(gè)操作可以是在一個(gè)線程也可以是兩個(gè)線程。
與我們?nèi)粘i_發(fā)聯(lián)系最緊密的先行發(fā)生原則如下:
程序順序規(guī)則:一個(gè)線程中的每個(gè)操作,happens- before 于該線程中的任意后續(xù)操作。
監(jiān)視器鎖規(guī)則:對一個(gè)監(jiān)視器鎖的解鎖,happens- before 于隨后對這個(gè)監(jiān)視器鎖的加鎖。
volatile變量規(guī)則:對一個(gè)volatile域的寫,happens- before 于任意后續(xù)對這個(gè)volatile域的讀。
傳遞性:如果A happens- before B,且B happens- before C,那么A happens- before C。
注:我們常說的 synchronized,volatile,ReentrantLock 等顯示同步的原理,就是依托于這里的監(jiān)視器鎖規(guī)則實(shí)現(xiàn)的。
小結(jié)這里我們介紹了基于共享狀態(tài)的并發(fā)模型,指出了由于線程工作內(nèi)存與主內(nèi)存的同步,代碼執(zhí)行的重排序等問題,可能導(dǎo)致線程共享狀態(tài)的可見性及原子性錯(cuò)誤。因此,當(dāng)線程之間存在公共狀態(tài)時(shí),需要利用先行發(fā)生原則針對共享狀態(tài)的訪問進(jìn)行合理性分析,確保共享狀態(tài)的訪問/修改操作兩兩符合先行發(fā)生原則。換句話說,需要保證對多線程之間共享狀態(tài)的操作進(jìn)行合理同步。
拓展思考學(xué)了這么多,回到我們最開始的問題:
即便我們知道線程什么時(shí)候執(zhí)行完畢,可是 Java 并沒有提供線程之間顯示的通信方法,那么我們怎么獲取需要的結(jié)果?
在進(jìn)行分析之前,我們回過頭來看看之前版本的核心代碼實(shí)現(xiàn):
int totalCount = 0; KeywordCount counter = new KeywordCount1(new File(directory), keyword); for (int i = 0; i < execTimes; i++) { int count = counter.search(); totalCount += count; }
可以看到,我們最終的結(jié)果是通過 totalCount 變量記錄的,也就是說,如果我們依舊依賴這個(gè)變量作為我們的最重結(jié)果,因?yàn)槊總€(gè)線程都會統(tǒng)計(jì)自己的關(guān)鍵詞,累加到該變量。那么這就是一種典型的共享數(shù)據(jù)的競態(tài)問題,這時(shí)依據(jù)先行發(fā)生原則進(jìn)行分析,我們發(fā)現(xiàn):
因?yàn)椴皇菃尉€程環(huán)境,所以程序順序規(guī)則失效
因?yàn)闆]有用任何鎖,也沒有用 synchronized 關(guān)鍵字,所以監(jiān)視器規(guī)則失效
因?yàn)闆]有用 volatile 關(guān)鍵字,所以volatile規(guī)則失效
因?yàn)樯鲜鲆?guī)則都失效,所以傳遞性規(guī)則也失效
綜上,通過利用先行發(fā)生原則對競態(tài)條件進(jìn)行分析,我們發(fā)現(xiàn)這部分代碼不做改變那么多線程環(huán)境下鐵定會出錯(cuò),那么我們接下來該怎么辦呢?
解決方法我們可以新建一個(gè) Counter 類,將這個(gè) Counter 類傳遞給各個(gè)線程去運(yùn)行計(jì)算相應(yīng)的任務(wù)。同時(shí)在 Counter 類中設(shè)置一個(gè)原子的計(jì)數(shù)器域(AtomicInteger),利用 AtomicInteger 的 incrementAndGet() 來實(shí)現(xiàn)原子的自增操作。等主線程判斷計(jì)算任務(wù)執(zhí)行完畢時(shí),再從 Counter 類獲取計(jì)算結(jié)果即可。核心代碼如下:
class Counter{ private AtomicInteger count = new AtomicInteger(0); counter(File file){ ··· count.incrementAndGet(); ··· } int getCounterNum(){ count.get(); } }
注:這里由于計(jì)數(shù)器的實(shí)現(xiàn)需要依賴變量自身的舊狀態(tài),所以不能使用 volatile 變量。反之,如果業(yè)務(wù)場景只需要共享狀態(tài)的單一更新(不依賴舊狀態(tài)),那么使用 volatile 關(guān)鍵字效率會更高。
拓展來看,如果業(yè)務(wù)操作再復(fù)雜一些,需要確保多個(gè)變量的組合操作的并發(fā)原子性時(shí),更建議使用 ReentrantLock 以及 synchronized 關(guān)鍵字來對方法或者代碼塊進(jìn)行鎖定以保證正確性。
基于上文對并發(fā)編程模型的思考,我們解決了擺在我們面前的兩尊攔路虎,線程何時(shí)結(jié)束 & 變量在線程中如何傳遞。
現(xiàn)在我們終于可以再來看看并發(fā)版本的關(guān)鍵字統(tǒng)計(jì)功能該如何實(shí)現(xiàn)了。代碼實(shí)現(xiàn)如下:
/** * Description: * 掃描指定目錄下指定關(guān)鍵字的出現(xiàn)次數(shù)——多線程+原子變量版本實(shí)現(xiàn) * @author The hope * @date 2018/5/20. */ public class KeywordCount2 implements KeywordCount { private final File directory; private final String keyword; KeywordCount2(File directory, String keyword) { this.keyword = keyword; this.directory = directory; } public int search() throws InterruptedException { Counter counter = new Counter(keyword); FileSearch fileSearch = new FileSearch(directory, counter); Thread t = new Thread(fileSearch); t.start(); t.join(); return counter.getCountNum(); } @Override public void shutDown() {} private static class FileSearch implements Runnable { private File directory; private Counter counter; FileSearch(File file, Counter counter) { this.directory = file; this.counter = counter; } @Override public void run() { ListsubThreads = new ArrayList<>(); for (File file : directory.listFiles()) if (file.isDirectory()) { FileSearch fileSearch = new FileSearch(file, counter); Thread t = new Thread(fileSearch); subThreads.add(t); t.start(); } else { counter.search(file); } for (Thread subThread : subThreads) try { subThread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } } private static class Counter { String keyword; AtomicInteger count = new AtomicInteger(0); Counter(String keyword) { this.keyword = keyword; } int getCountNum() { return count.get(); } void search(File file) { try (Scanner in = new Scanner(file)) { while (in.hasNextLine()) { String line = in.nextLine(); if (line.contains(keyword)) count.incrementAndGet(); } } catch (FileNotFoundException e) { e.printStackTrace(); } } } }
這里我們新創(chuàng)建了兩個(gè)類 FileSearch與Counter。
利用FileSearch來進(jìn)行線程的創(chuàng)建與子計(jì)算的分發(fā)問題:
@Override public void run() { ListsubThreads = new ArrayList<>(); for (File file : directory.listFiles()) if (file.isDirectory()) { FileSearch fileSearch = new FileSearch(file, counter); Thread t = new Thread(fileSearch); subThreads.add(t); t.start(); } else { counter.search(file); } for (Thread subThread : subThreads) try { subThread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } }
利用Counter來解決計(jì)算結(jié)果在線程間的傳遞問題:
··· AtomicInteger count = new AtomicInteger(0); ··· void search(File file) { try (Scanner in = new Scanner(file)) { while (in.hasNextLine()) { String line = in.nextLine(); if (line.contains(keyword)) count.incrementAndGet(); } } catch (FileNotFoundException e) { e.printStackTrace(); } }
執(zhí)行結(jié)果如下:
Enter base directory (e.g. C:Program FilesJavajdk1.6.0_45src): C:Program FilesJavajdk1.6.0_45src Enter keyword (e.g. java): java Statistics: 43781 used time: 2418 Process finished with exit code 0
可以看到時(shí)間降低至2秒半左右,提高了50%,的確是極大的提高了響應(yīng)速度
小結(jié)本文通過提出一個(gè)簡單的業(yè)務(wù)場景(統(tǒng)計(jì)指定目錄下關(guān)鍵字出現(xiàn)數(shù)量),并設(shè)計(jì)了一個(gè)簡單的串行實(shí)現(xiàn)。
針對串行版本響應(yīng)緩慢的問題,筆者以提出問題-解決問題的模式,引入Java多線程通信以及 Java 內(nèi)存模型的相關(guān)知識,一步步解決改造過程中的痛點(diǎn)并最終完成了一個(gè)基于原子變量的并發(fā)版本實(shí)現(xiàn)。
通過測試驗(yàn)證,本輪改造成功解決了串行版本的業(yè)務(wù)痛點(diǎn) :)
拓展思考雖然上述實(shí)現(xiàn)極大的提高了程序的執(zhí)行速度,將執(zhí)行時(shí)間縮短了一半。但是仍然存在下面幾個(gè)問題。
代碼變得更為復(fù)雜: 串行版本50行不到解決問題并發(fā)版本,卻暴增至100行,客觀上增加了復(fù)雜度。
創(chuàng)建線程的數(shù)量不可確定: 本版本的實(shí)現(xiàn)中,線程的創(chuàng)建數(shù)量僅取決于文件數(shù)目,衍生出執(zhí)行效率問題。
多了些額外的對象,比如 Counter:本問題實(shí)際上是問題 1 的具體版本,為了并發(fā)而引入新的類本就客觀增加了復(fù)雜度。
Counter 面臨多個(gè)線程的競態(tài)條件,必須進(jìn)行同步:由于使用Counter來解決線程間的通信問題,因而勢必引出同步問題。
上述問題該如何解決與避免,請看下文:深入理解 Java多線程系列(2)——執(zhí)行器框架
未完待續(xù)~
參考文獻(xiàn)Java 并發(fā)編程實(shí)戰(zhàn)
Java 核心技術(shù)——卷Ⅰ
深入理解 Jvm 虛擬機(jī)——周志明
深入理解 Java 內(nèi)存模型——程曉明
zhihu.com
segmentfault.com
oschina.net
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/69493.html
摘要:虛擬機(jī)所處的區(qū)域,則表示它是屬于新生代收集器還是老年代收集器。虛擬機(jī)總共運(yùn)行了分鐘,其中垃圾收集花掉分鐘,那么吞吐量就是。收集器線程所占用的數(shù)量為。 本文主要從GC(垃圾回收)的角度試著對jvm中的內(nèi)存分配策略與相應(yīng)的垃圾收集器做一個(gè)介紹。 注:還是老規(guī)矩,本著能畫圖就不BB原則,盡量將各知識點(diǎn)通過思維導(dǎo)圖或者其他模型圖的方式進(jìn)行說明。文字僅記錄額外的思考與心得,以及其他特殊情況 內(nèi)存...
摘要:設(shè)備改造上傳結(jié)果數(shù)據(jù)的技術(shù)實(shí)現(xiàn)一項(xiàng)目需求及分析按照領(lǐng)導(dǎo)的要求,要改造一臺儀器,添加點(diǎn)功能,將測量數(shù)據(jù)上傳到服務(wù)器。所以選擇用提交,的通信可以多線程調(diào)度??紤]到新增的上傳功能不能影響之前的測量節(jié)拍,所以要多線程實(shí)現(xiàn)。 **設(shè)備改造——上傳結(jié)果數(shù)據(jù)的技術(shù)實(shí)現(xiàn) 一、項(xiàng)目需求及分析 按照領(lǐng)導(dǎo)的要求,要改造一臺儀器,添加點(diǎn)功能,將測量數(shù)據(jù)上傳到服務(wù)器。儀器測量節(jié)拍大概是20s,數(shù)據(jù)量目前不大,...
摘要:線程之間的通信由內(nèi)存模型本文簡稱為控制,決定一個(gè)線程對共享變量的寫入何時(shí)對另一個(gè)線程可見。為了保證內(nèi)存可見性,編譯器在生成指令序列的適當(dāng)位置會插入內(nèi)存屏障指令來禁止特定類型的處理器重排序。 并發(fā)編程模型的分類 在并發(fā)編程中,我們需要處理兩個(gè)關(guān)鍵問題:線程之間如何通信及線程之間如何同步(這里的線程是指并發(fā)執(zhí)行的活動實(shí)體)。通信是指線程之間以何種機(jī)制來交換信息。在命令式編程中,線程之間的...
摘要:后端好書閱讀與推薦系列文章后端好書閱讀與推薦后端好書閱讀與推薦續(xù)后端好書閱讀與推薦續(xù)二后端好書閱讀與推薦續(xù)三這里依然記錄一下每本書的亮點(diǎn)與自己讀書心得和體會,分享并求拍磚。然后又請求封鎖,當(dāng)釋放了上的封鎖之后,系統(tǒng)又批準(zhǔn)了的請求一直等待。 后端好書閱讀與推薦系列文章:后端好書閱讀與推薦后端好書閱讀與推薦(續(xù))后端好書閱讀與推薦(續(xù)二)后端好書閱讀與推薦(續(xù)三) 這里依然記錄一下每本書的...
閱讀 1587·2021-10-18 13:35
閱讀 2370·2021-10-09 09:44
閱讀 824·2021-10-08 10:05
閱讀 2723·2021-09-26 09:47
閱讀 3577·2021-09-22 15:22
閱讀 441·2019-08-29 12:24
閱讀 2004·2019-08-29 11:06
閱讀 2862·2019-08-26 12:23