## 一、多線程開發所要平衡的幾個點混混噩噩看了很多多線程的書籍,一直認為自己還不夠資格去閱讀這本書。有種要高登大堂的感覺,被各種網絡上、朋友、同事一頓外加一頓的宣傳與傳頌,多多少少再自我內心中產生了一種敬畏感。2月28好開始看了之后,發現,其實完全沒這個必要。除了翻譯的爛之外(一大段中文下來,有時候你就會罵娘:這tm想說的是個shen me gui),所有的,多線程所必須掌握的知識點,深入點,全部涵蓋其中,只能說,一書在手,萬線程不愁那種!當然,你必須要全部讀懂,並融匯貫通之后,才能有的效果。我推薦,看這本書的中文版本,不要和哪一段的冗長的字句在那過多的糾纏,盡量一段一段的讀,然后獲取這一段中最重要的那句話,否則你會陷入中文閱讀理解的怪圈,而懷疑你的高中語文老師是不是體育老師客串的!!我舉個例子:13頁第八段,我整段讀了三遍硬是沒想明白前面那么多的文字,是干什么用的,就是最后一句話才是核心:告訴你,線程安全性,最正規的定義應該是什么!(情允許我,向上交的幾個翻譯此書的,所謂的“教授”致敬,在你們的引領下,使我們的意志與忍受力更上了一個台階,人生更加完美!)
看了很多次的目錄,外加看了第一部分,發現,要想做好多線程的開發,無非就是平衡好以下的幾點
- 安全性
- 活躍性
- 無限循環問題
- 死鎖問題
- 飢餓問題
- 活鎖問題(這個還沒具體的了解到)
- 性能要求
- 吞吐量的問題
- 可伸縮性的問題
要想平衡好以上幾點,書中循序漸進的將多線程開發最應該修煉的幾個點,娓娓道來:
- 原子性
- 先檢查后執行
- 原子類
- 加鎖機制
- 可見性
- 重排
- 非64位寫入問題
- 對象的發布
- 對象的封閉
- 不變性
在一本國人自己寫的,介紹線程工具api的書中,看到了這么一句話:外練原子,內練可見。感覺這幾點如果在多線程中尤為重要。我在有贊,去年還記得上線多門店的那天凌晨,最后項目啟動報一個類加載的錯誤,一堆人過來看問題,基德大神站在攀哥的后面,最后淡淡的說了句:已經很明顯是可見性問題了,加上volatile,不行的話,我把代碼吃了!!可以見得,多線程這幾個點,在“居家旅行”,生活工作中是多么的常見與重要!不出問題不要緊,只要一出,就會是頭痛的大問題,因為你根本不好排查根本原因在這。所以我們需要平時就練好功底,盡量避免多線程問題的出現!而不是一味的用框架啊用框架、摞代碼啊摞代碼!
## 三、原子性下面的安全問題 **1. 下面代碼有什么問題呢?**public class UnsafeConuntingFactorizer implements Servlet{
private long count = 0;
private long getCount(){
return count;
}
public void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
++count;
encodeIntoResponse(resp,factor);
}
}
**2. 上面代碼分析**思考:如何讓一個普普通通的類變得線程安全呢?一個類什么叫做有狀態,而什么又叫做無狀態呢?
- 一個請求的方法,實例都是一個,所以每次請求都會訪問同一個對象
- 每個請求,使用一個線程,這就是典型的多線程模型
- count是一個對象狀態屬性,被多個線程共享
++count並非一次原子操作(分成:復制count->對復制體修改->使用復制體回寫count,三個步奏)- 多個線程有可能多次修改count值,而結果卻相同
public class UnsafeConuntingFactorizer implements Servlet{
private final AtomicLong count = new AtomicLong(0);
private long getCount(){
return count.get();
}
public void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
count.incrementAndGet();//使用了新的原子類的原子方法
encodeIntoResponse(resp,factor);
}
}
**4. 原子類也不是萬能的**
//在復雜的場景下,使用多個原子類的對象
public class UnsafeConuntingFactorizer implements Servlet{
private final AtomicReference<BigInteger> lastNumber
= new AtomicReference<BigInteger>();
private final AtomicReference<BigInteger[]> lastFactors
= new AtomicReference<BigInteger[]>();
public void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);
if(i.equals(lastNumber.get())){//先判斷再處理,並沒有進行同步,not safe!
encodeIntoResponse(resp,lastFactors.get());
}else{
BigInteger[] factors = factor(i);
lastNumer.set(i);
lastFactors.set(factors);
encodeIntoResponse(resp, factors);
}
}
}
**5. 先列舉一個我們常見的復合型操作**思考:什么叫做復合型操作?
public class LazyInitRace {
private ExpensiveObject instace = null;
public ExpensiveObject getInstace(){
if(instace == null){
instace = new ExpensiveObject();
}
return instace;
}
}
**6. 提高“先判斷再處理”的警覺性**看好了,這就是我們深惡痛絕的一段代碼!如果這段代碼還分析不了的,對不起,出門左轉~
- 如果沒有同步措施,直接對一個狀態進行判斷,然后設值的,都是不安全的
- if操作和下面代碼快中的代碼,遠遠不是原子的
- 如果if判斷完之后,接下來線程掛起,其他線程進入判斷流程,又是同樣的狀態,同樣進入if語句塊
- 當然,只有一個線程執行的程序,請忽略(那還叫能用的程序嗎?)
//在復雜的場景下,使用多個原子類的對象
public class UnsafeConuntingFactorizer implements Servlet{
private final AtomicReference<BigInteger> lastNumber
= new AtomicReference<BigInteger>();
private final AtomicReference<BigInteger[]> lastFactors
= new AtomicReference<BigInteger[]>();
//這下子總算同步了!
public synchronized void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);
if(i.equals(lastNumber.get())){//先判斷再處理,並沒有進行同步,not safe!
encodeIntoResponse(resp,lastFactors.get());
}else{
BigInteger[] factors = factor(i);
lastNumer.set(i);
lastFactors.set(factors);
encodeIntoResponse(resp, factors);
}
}
}
**8. 上訴代碼解析** - 加上了``synchronized``關鍵字的確解決了多線程訪問,類安全性問題 - 可是每次都是一個線程進行計算,所有請求變成了串行 - 請求量低於100/s其實都還能接受,可是再高的話,這就完全有問題的代碼了 - 性能問題,再網絡里面,是永痕的心病~ **9. 一段針對原子性、性能問題的解決方案**思考:有沒有種“關公揮大刀,一砍一大片”的感覺?
//在復雜的場景下,使用多個原子類的對象
public class CacheFactorizer implements Servlet{
private BigInteger lastNumber;
private BigInteger[] lastFactors ;
private long hits;
private long cacheHits;
public synchronized long getHits(){
return hits;
}
public synchronized double getCacheHitRadio(){
return (double) cacheHits / (double) hits;
}
public void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);
BigInteger[] factors = null;
synchronized (this){
++hits;
if(i.equals(lastNumber)){
++cacheHits;
factors = lastFactors.clone();
}
}
if (factors == null){
factors = factor(i);
synchronized (this){
lastNumer = i;
lastFactors = factors.clone();
}
}
encodeIntoResponse(resp, factors);
}
}
## 四、可見性下面的對象共享在修改狀態值的時候,才進行加鎖,平時對狀態值的讀操作可以不用加鎖,當然,最耗時的計算過程,也是要同步的,這種情況下,才會進一步提高性能。
可見性這個話題,在多線程的環境下,是相當棘手的。可能多年之后,你成為萬人心中的老鳥,也同樣會對這個問題,悵然若失!我自己總結了幾點,可見性問題的難處:
- 道理簡單,真實場景代碼錯中復雜,你根本不知道是可見性導致的
- 小概率事件,往往可能只有百分之一,甚至千分之一的出事概率,容易被我們“得過且過”
- 容易直接扔到一個
synchronized加鎖塊里面,進行“大刀”式的處理,而忽略了高效性 - 可見性+原子性的綜合考慮
針對這些問題,我們只能先從基本功抓起,然后在日積月累的開發工作中,多多分析程序運行的場景,多多嘗試,才能大有裨益。
**1. 可見性的發生的必要條件**插曲:昨天看了《戀愛回旋》這部日本電影,其中有個場景讓我記憶深刻:女主是小時候被魔鬼母親常年訓練的乒乓球少年運動員,后來總總原因,放棄了乒乓球,當起了OL,這一別就是15年。當再次碰到男主的時候,男主向女主發起乒乓球挑戰,以為女主是個菜逼,然后賭一些必須要讓女主完成的事情。(女主本人也是覺得乒乓球對自己是一種心理的負擔,並且放棄這么久了,所以沒啥子自信)沒想到,女主一拿球拍,在接發球的那一剎那。。。。。大家應該都懂了。我當時就在影院中說出聲來:基本功太重要了。
可見性,無非就是再多線程環境下,對共享變量的讀寫導致的。可能一個線程修改了共享變量的值,而另一個線程讀取的還是老的值,差不多就是這么大白話的解釋了下來。其中發生的必要條件有:
- 多線程環境訪問同一個共享變量
- 服務器模式下啟動程序
- 共享變量並沒有做什么處理,代碼塊也沒有同步
當然,要分析為什么會有可見性的問題,要結合JVM虛擬機內存模型分析。以后會在《深入理解Java虛擬機》的學習中,做詳細的分析,敬請期待。
**2. 不多說上代碼**public class NoVisibility{
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread{
public void run(){
while(!ready){
Thread.yield();
}
System.out.println(number);
}
}
public static void main(String[] args){
new ReaderThread().start();
number = 42;
ready = true;
}
}
**3. 上訴代碼分析**思考:上面的打印number的值,有可能會有幾種結果呢?什么情況下出現的這些個結果
- number最終打印結果有可能出現42、0,或者根本就不會打印
- 42:這種情況是運行正確的結果
- 0:這種情況發生了指令重排(五星級的問題)
- 不會打印:主線程對ReaderThread線程出現了共享變量不可見
之所以說是“愚鈍”,原因是重排問題,是一個很底層很考驗計算機基礎能力的一個問題,小弟不才,當年分析計算機組成原理與指令結構的時候,枯燥極致,都睡過去了。現在回頭,才知道其重要性。現階段,對重排的分析,我只能舉例個簡單的例子,進行說明,更進一步的分析,同樣是要結合JVM的機制(六大Happens-before)來分析,以后再做進一步,詳盡的分析。下面就是那個簡單的例子:
//簡單例子
public class OrderConfuse{
public static void main(String[] args){
int a = 1;
int b = 2;
int c = a+b;
int d = c+a;
System.out.println(c);
System.out.println(d);
}
}
- 上面程序是正確的,也能正確輸出
- 對a和b的賦值操作,並非先賦值a再賦值b的
- 原因是JVM底層會對指令進行優化,保證程序的快速執行,其實就是一種效率優化
- 變量c會用到a和b變量,所以a和b的操作必須要發生在c之前(happens-before)
- 有可能b進行了賦值,而a還是初始化的狀態,就是值為0
所以結合前面的代碼段:
public class NoVisibility{
......
public static void main(String[] args){
new ReaderThread().start();
number = 42;
ready = true;
}
}
- number和ready之后,並沒有使用它們的變量了
- number和ready會被進行指令重排
- 結果就是:ready已經賦值變成了true,可是number還是0
這就是為啥會為零的原因所在!
**5. 針對可見性,還是要上JVM的內存模型進行簡單分析**- 每個線程都會有自己的線程虛擬機棧
- 棧上面存儲原始類型和對象類型的引用
- 每次啟動一個線程,都會在對共享數據進行一次復制,復制到每個線程的虛擬機棧中
- 上面的number是在主線程中,同時在ReaderThread線程的虛擬機棧中有一個副本
- 各個虛擬機棧最終要進行同步,才能保持一致
- 所以每次修改一個共享變量(原始類型)其實是在本地線程空間里面修改
- number在主線程里面修改了,可是在ReaderThread線程里面並沒有修改,因為兩個線程訪問的空間並不一樣,一個線程對另一個線程空間並不可見。
volatile關鍵字的作用,主要有一下幾點:
- 能把對變量的修改,馬上同步到主存中
- 各個線程立馬更新自己線程棧中的變量值
- 防止指令重排
- 無法保證原子性
對於最底層如何做到這些個點的,具體還可以分析,例如什么內存屏障、狀態過期等等,完全可以聊一個專題,今天再次先不聊,同樣放到《深入理解JVM虛擬機》的學習中來詳盡分析。所以,上面程序可以改成下面這個樣子:
public class Visibility{
private static volatile boolean ready;//注意這個類型
private static volatile int number;//注意這個類型
private static class ReaderThread extends Thread{
public void run(){
while(!ready){
Thread.yield();
}
System.out.println(number);
}
}
public static void main(String[] args){
new ReaderThread().start();
number = 42;
ready = true;
}
}
**7. synchronized關鍵字同樣可以保證可見性**
public class Visibility{
private static boolean ready;
private static int number;
private static Object lock = new Object();
private static class ReaderThread extends Thread{
public void run(){
while(!ready){
Thread.yield();
}
System.out.println(number);
}
}
public static void main(String[] args){
new ReaderThread().start();
synchronized(lock){//這里進行了加鎖
number = 42;
ready = true;
}
}
}
- 加鎖可以同時保證可見性與原子性
- 加鎖同樣可以防止指令重排,內部代碼都會照順序執行
public class VisibleNotAtomic{
private static volatile int number = 1;
private static class ReadThread extends Thread{
public void run(){
if(number == 2){
System.out.println("correct!");
}else{
System.out.println("error!");
}
}
}
public static void main(String[] args){
number++;
}
}
- number是對主線程和ReadThread線程都可見的
- 可是number++不是原子操作
- 加加到了一半,主線程掛起,ReadThread線程運行,number的值還是1,輸出error
我們主要講了線程的原子性和可見性,結合代碼,不知不覺就講了一堆,而且感覺還可以在講~~多線程的話題真的是太恐怖了!未來的可預見性的規划如下:
- 對象的安全發布
- 對象的不變性
- 對象的合理加鎖
- 生產者消費者模型
- 構建高效可伸縮的緩存
恩,敬請期待~
