「二哥,上一篇《集合》的反響效果怎麼樣啊?」三妹對她提議的《教妹學 Java》專欄很關心。java
「這篇文章的瀏覽量要比第一篇《泛型》好得多。」算法
「這是個好消息啊,說明更多人接受了二哥的創做。」三妹心花盛開了起來。瀏覽器
「也許沒什麼對比性。」緩存
「沒有對比性?我翻看了一下二哥 7 個月前寫的文章,是真的水啊,嘻嘻。」三妹賣了一個萌,繼續說道,「說實話,居然還有讀者願意看,真的是難以想象。」安全
「你是想捱揍嗎?」網絡
「別啊。我是說,二哥如今的讀者真的很幸運,由於他們看到了更高質量的文章。」三妹繼續肆無忌憚地說着她的真心話。多線程
「是啊,比之前好多了,但我還要更加地努力,此次的主題是《多線程》,三妹你準備好了嗎?」併發
「早準備好了。讓我繼續來提問吧,二哥你繼續回答。」三妹已經躍躍欲試了。ide
三妹,聽哥給你慢慢講啊。性能
要想了解線程,得先了解進程,由於線程是進程的一個單元。你看,我這臺電腦同時開了不少個進程,好比說打字用的這個輸入法、寫做用的這個瀏覽器,聽歌用的這個音樂播放器。
這些進程同時可能幹幾件事,好比說這個音樂播放器,一邊滾動着歌詞,一邊播放着音頻。也就是說,在一個進程內部,可能同時運行着多個線程(Thread),每一個線程負責着不一樣的任務。
因爲每一個進程至少要幹一件事,因此,一個進程至少有一個線程。在 Java 的程序當中,至少會有一個 main 方法,也就是所謂的主線程。
能夠同時執行多個線程,執行方式和多個進程是同樣的,都是由操做系統決定的。操做系統能夠在多個線程之間進行快速地切換,讓每一個線程交替地運行。切換的時間越短,程序的效率就越高。
進程和線程之間的關係能夠用一句通俗的話講,就是「進程是爹媽,管着衆多的線程兒女。」
三妹,先去給哥泡杯咖啡,再來聽哥給你慢慢地講。
多線程做爲一種多任務、併發的工做方式,好處多多。
第一,減小應用程序的響應時間。
對於計算機來講,IO 讀寫和網絡通訊相對是比較耗時的任務,若是不使用多線程的話,其餘耗時少的任務也必需要等待這些任務結束後才能執行。
第二,充分利用多核 CPU 的優點。
操做系統能夠保證當線程數不大於 CPU 數目時,不一樣的線程運行於不一樣的 CPU 上。不過,即使線程數超過了 CPU 數目,操做系統和線程池也會盡最大可能地減小線程切換花費的時間,最大可能地發揮併發的優點,提高程序的性能。
第三,相比於多進程,多線程是一種更「高效」的多任務執行方式。
對於不一樣的進程來講,它們具備獨立的數據空間,數據之間的共享必須經過「通訊」的方式進行。而線程則不須要,同一進程下的線程之間共享數據空間。
固然了,若是兩個線程存取相同的對象,而且每一個線程都調用了一個修改該對象狀態的方法,將會帶來新的問題。
什麼問題呢?咱們來經過下面的示例進行說明。
public class Cmower {
public static int count = 0;
public static int getCount() {
return count;
}
public static void addCount() {
count++;
}
public static void main(String[] args) {
ExecutorService executorService = new ThreadPoolExecutor(10, 1000, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(10));
for (int i = 0; i < 1000; i++) {
Runnable r = new Runnable() {
@Override
public void run() {
Cmower.addCount();
}
};
executorService.execute(r);
}
executorService.shutdown();
System.out.println(Cmower.count);
}
}
複製代碼
咱們建立了一個線程池,經過 for 循環讓線程池執行 1000 個線程,每一個線程調用了一次 Cmower.addCount()
方法,對 count 值進行加 1 操做,當 1000 個線程執行完畢後,在控制檯打印 count 的值。
其結果會是什麼呢?
99八、99七、99八、99六、996
但幾乎不會是咱們想要的答案 1000。
三妹啊,咖啡泡得太濃了。不過,濃一點的好處是更提神了。
程序在運行過程當中,會將運算須要的數據從物理內存中複製一份到 CPU 的高速緩存當中,計算結束以後,再將高速緩存中的數據刷新到物理內存當中。
拿 count++
來講。當線程執行這個語句時,會先從物理內存中讀取 count 的值,而後複製一份到高速緩存當中,CPU 執行指令對 count 進行加 1 操做,再將高速緩存中 count 的最新值刷新到物理內存當中。
在多核 CPU 中,每一個線程可能運行於不一樣的 CPU 中,所以每一個線程在運行時會有專屬的高速緩存。假設線程 A 正在對 count 進行加 1 操做,此時線程 B 的高速緩存中 count 的值仍然是 0 ,進行加 1 操做後 count 的值爲 1。最後兩個線程把最新值 1 刷新到物理內存中,而不是理想中的 2。
這種被多個線程訪問的變量被稱爲共享變量,他們一般須要被保護起來。
三妹啊,等我喝口咖啡提提神。
針對上例中出現的 count,能夠按照下面的方式進行改造。
public static AtomicInteger count = new AtomicInteger();
public static int getCount() {
return count.get();
}
public static void addCount() {
count.incrementAndGet();
}
複製代碼
使用支持原子操做(即一個操做或者多個操做要麼所有執行,而且執行的過程不會被任何因素打斷,要麼就都不執行)的 AtomicInteger
代替基本類型 int。
簡單分析一下 AtomicInteger
類,該類源碼中能夠看到一個有趣的變量 unsafe
。
private static final Unsafe unsafe = Unsafe.getUnsafe();
複製代碼
Unsafe
是一個能夠執行不安全、容易犯錯操做的特殊類。AtomicInteger
使用了 Unsafe
的原子操做方法 compareAndSwapInt()
對數據進行更新,也就是所謂的 CAS。
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
複製代碼
參數 o 是要進行 CAS 操做的對象(好比說 count),參數 offset 是內存位置,參數 expected 是指望的值,參數 x 是須要更新到的值。
通常的同步方法會從地址 offset 讀取值 A,執行一些計算後得到新值 B,而後使用 CAS 將 offset 的值從 A 改成 B。若是 offset 處的值還沒有同時更改,則 CAS 操做成功。
CAS 容許執行「讀-修改-寫」的操做,而無需擔憂其餘線程同時修改了變量,由於若是其餘線程修改變量,那麼 CAS 會檢測它(並失敗),算法能夠對該操做從新計算。
AtomicInteger
類的源碼中還有一個值得注意的變量 value
。
private volatile int value;
複製代碼
value
使用了關鍵字 volatile
來保證可見性——當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其餘線程可以當即看獲得修改的值。
當一個共享變量被 volatile
修飾後,它被修改後的值會當即更新到物理內存中,當有其餘線程須要讀取時,會去物理內存中讀取新值。
而沒有被 volatile
修飾的共享變量不能保證可見性,由於不肯定這些變量會在何時被寫入物理內存中,當其餘線程去讀取時,讀到的可能仍是原來的舊值。
特別須要注意的是,volatile
關鍵字只保證變量的可見性,不能保證原子性。
「二哥,《多線程》就先講到這吧,再多我就吸取不了了!」三妹的態度很誠懇。
「能夠。」
「二哥,我記得上次你說要給大號投稿,結果怎麼樣了?」三妹關切地問。
「唉,都很差意思說,只收獲了兩個點讚的表情符號,可能仍是基於同情心。嚇得我不敢再投稿了,先堅持寫吧!」
「結局這麼慘淡嗎,真的沒有一個號要轉載嗎?我看那個投稿羣有三百多個公號呢。」三妹很傷心。
「《教妹學 Java》系列可能有點標題黨吧?」
「二哥,既然決定要寫,請不要懷疑本身。至少三妹很喜歡這種風格啊。」聽完三妹語重心長的話,我心底的那種自我懷疑又煙消雲散了。