前言
不小心就鴿了幾天沒有更新了,這個星期回家咯。在學校的日子要努力一點才行!
只有光頭才能變強
回顧前面:
本文章的知識主要參考《Java並發編程實戰》這本書的前4章,這本書的前4章都是講解並發的基礎的。要是能好好理解這些基礎,那么我們往后的學習就會事半功倍。
當然了,《Java並發編程實戰》可以說是非常經典的一本書。我是未能完全理解的,在這也僅僅是拋磚引玉。想要更加全面地理解我下面所說的知識點,可以去閱讀一下這本書,總的來說還是不錯的。
首先來預覽一下《Java並發編程實戰》前4章的目錄究竟在講什么吧:
第1章 簡介
- 1.1 並發簡史
- 1.2 線程的優勢
- 1.2.1 發揮多處理器的強大能力
- 1.2.2 建模的簡單性
- 1.2.3 異步事件的簡化處理
- 1.2.4 響應更靈敏的用戶界面
- 1.3 線程帶來的風險
- 1.3.1 安全性問題
- 1.3.2 活躍性問題
- 1.3.3 性能問題
- 1.4 線程無處不在
ps:這一部分我就不講了,主要是引出我們接下來的知識點,有興趣的同學可翻看原書~
第2章 線程安全性
- 2.1 什么是線程安全性
- 2.2 原子性
- 2.2.1 競態條件
- 2.2.2 示例:延遲初始化中的競態條件
- 2.2.3 復合操作
- 2.3 加鎖機制
- 2.3.1 內置鎖
- 2.3.2 重入
- 2.4 用鎖來保護狀態
- 2.5 活躍性與性能
第3章 對象的共享
- 3.1 可見性
- 3.1.1 失效數據
- 3.1.2 非原子的64位操作
- 3.1.3 加鎖與可見性
- 3.1.4 Volatile變量
- 3.2 發布與逸出
- 3.3 線程封閉
- 3.3.1 Ad-hoc線程封閉
- 3.3.2 棧封閉
- 3.3.3 ThreadLocal類
- 3.4 不變性
- 3.4.1 Final域
- 3.4.2 示例:使用Volatile類型來發布不可變對象
- 3.5 安全發布
- 3.5.1 不正確的發布:正確的對象被破壞
- 3.5.2 不可變對象與初始化安全性
- 3.5.3 安全發布的常用模式
- 3.5.4 事實不可變對象
- 3.5.5 可變對象
- 3.5.6 安全地共享對象
第4章 對象的組合
- 4.1 設計線程安全的類
- 4.1.1 收集同步需求
- 4.1.2 依賴狀態的操作
- 4.1.3 狀態的所有權
- 4.2 實例封閉
- 4.2.1 Java監視器模式
- 4.2.2 示例:車輛追蹤
- 4.3 線程安全性的委托
- 4.3.1 示例:基於委托的車輛追蹤器
- 4.3.2 獨立的狀態變量
- 4.3.3 當委托失效時
- 4.3.4 發布底層的狀態變量
- 4.3.5 示例:發布狀態的車輛追蹤器
- 4.4 在現有的線程安全類中添加功能
- 4.4.1 客戶端加鎖機制
- 4.4.2 組合
- 4.5 將同步策略文檔化
那么接下來我們就開始吧~
一、使用多線程遇到的問題
1.1線程安全問題
在前面的文章中已經講解了線程【多線程三分鍾就可以入個門了!】,多線程主要是為了提高我們應用程序的使用率。但同時,這會給我們帶來很多安全問題!
如果我們在單線程中以“順序”(串行-->獨占)的方式執行代碼是沒有任何問題的。但是到了多線程的環境下(並行),如果沒有設計和控制得好,就會給我們帶來很多意想不到的狀況,也就是線程安全性問題
因為在多線程的環境下,線程是交替執行的,一般他們會使用多個線程執行相同的代碼。如果在此相同的代碼里邊有着共享的變量,或者一些組合操作,我們想要的正確結果就很容易出現了問題
簡單舉個例子:
- 下面的程序在單線程中跑起來,是沒有問題的。
public class UnsafeCountingServlet extends GenericServlet implements Servlet {
private long count = 0;
public long getCount() {
return count;
}
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
++count;
// To something else...
}
}
但是在多線程環境下跑起來,它的count值計算就不對了!
首先,它共享了count這個變量,其次來說++count;
這是一個組合的操作(注意,它並非是原子性)
++count
實際上的操作是這樣子的:- 讀取count值
- 將值+1
- 將計算結果寫入count
於是多線程執行的時候很可能就會有這樣的情況:
- 當線程A讀取到count的值是8的時候,同時線程B也進去這個方法上了,也是讀取到count的值為8
- 它倆都對值進行加1
- 將計算結果寫入到count上。但是,寫入到count上的結果是9
- 也就是說:兩個線程進來了,但是正確的結果是應該返回10,而它返回了9,這是不正常的!
如果說:當多個線程訪問某個類的時候,這個類始終能表現出正確的行為,那么這個類就是線程安全的!
有個原則:能使用JDK提供的線程安全機制,就使用JDK的。
當然了,此部分其實是我們學習多線程最重要的環節,這里我就不詳細說了。這里只是一個總覽,這些知識點在后面的學習中都會遇到~~~
1.3性能問題
使用多線程我們的目的就是為了提高應用程序的使用率,但是如果多線程的代碼沒有好好設計的話,那未必會提高效率。反而降低了效率,甚至會造成死鎖!
就比如說我們的Servlet,一個Servlet對象可以處理多個請求的,Servlet顯然是一個天然支持多線程的。
又以下面的例子來說吧:
public class UnsafeCountingServlet extends GenericServlet implements Servlet {
private long count = 0;
public long getCount() {
return count;
}
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
++count;
// To something else...
}
}
從上面我們已經說了,上面這個類是線程不安全的。最簡單的方式:如果我們在service方法上加上JDK為我們提供的內置鎖synchronized,那么我們就可以實現線程安全了。
public class UnsafeCountingServlet extends GenericServlet implements Servlet {
private long count = 0;
public long getCount() {
return count;
}
public void synchronized service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
++count;
// To something else...
}
}
雖然實現了線程安全了,但是這會帶來很嚴重的性能問題:
- 每個請求都得等待上一個請求的service方法處理了以后才可以完成對應的操作
這就導致了:我們完成一個小小的功能,使用了多線程的目的是想要提高效率,但現在沒有把握得當,卻帶來嚴重的性能問題!
在使用多線程的時候:更嚴重的時候還有死鎖(程序就卡住不動了)。
這些都是我們接下來要學習的地方:學習使用哪種同步機制來實現線程安全,並且性能是提高了而不是降低了~
二、對象的發布與逸出
書上是這樣定義發布和逸出的:
發布(publish) 使對象能夠在當前作用域之外的代碼中使用
逸出(escape) 當某個不應該發布的對象被發布了
常見逸出的有下面幾種方式:
- 靜態域逸出
- public修飾的get方法
- 方法參數傳遞
- 隱式的this
靜態域逸出:
public修飾get方法:
方法參數傳遞我就不再演示了,因為把對象傳遞過去給另外的方法,已經是逸出了~
下面來看看該書給出this逸出的例子:
逸出就是本不應該發布對象的地方,把對象發布了。導致我們的數據泄露出去了,這就造成了一個安全隱患!理解起來是不是簡單了一丟丟?
2.1安全發布對象
上面談到了好幾種逸出的情況,我們接下來來談談如何安全發布對象。
安全發布對象有幾種常見的方式:
- 在靜態域中直接初始化 :
public static Person = new Person()
;- 靜態初始化由JVM在類的初始化階段就執行了,JVM內部存在着同步機制,致使這種方式我們可以安全發布對象
- 對應的引用保存到volatile或者AtomicReferance引用中
- 保證了該對象的引用的可見性和原子性
- 由final修飾
- 該對象是不可變的,那么線程就一定是安全的,所以是安全發布~
- 由鎖來保護
- 發布和使用的時候都需要加鎖,這樣才保證能夠該對象不會逸出
三、解決多線程遇到的問題
從上面我們就可以看到,使用多線程會把我們的系統搞得挺復雜的。是需要我們去處理很多事情,為了防止多線程給我們帶來的安全和性能的問題~
下面就來簡單總結一下我們需要哪些知識點來解決多線程遇到的問題。
3.1簡述解決線程安全性的辦法
使用多線程就一定要保證我們的線程是安全的,這是最重要的地方!
在Java中,我們一般會有下面這么幾種辦法來實現線程安全問題:
- 無狀態(沒有共享變量)
- 使用final使該引用變量不可變(如果該對象引用也引用了其他的對象,那么無論是發布或者使用時都需要加鎖)
- 加鎖(內置鎖,顯示Lock鎖)
- 使用JDK為我們提供的類來實現線程安全(此部分的類就很多了)
- 原子性(就比如上面的
count++
操作,可以使用AtomicLong來實現原子性,那么在增加的時候就不會出差錯了!) - 容器(ConcurrentHashMap等等...)
- ......
- 原子性(就比如上面的
- ...等等
3.2原子性和可見性
何為原子性?何為可見性?當初我在ConcurrentHashMap基於JDK1.8源碼剖析中已經簡單說了一下了。不了解的同學可以進去看看。
3.2.1原子性
在多線程中很多時候都是因為某個操作不是原子性的,使數據混亂出錯。如果操作的數據是原子性的,那么就可以很大程度上避免了線程安全問題了!
count++
,先讀取,后自增,再賦值。如果該操作是原子性的,那么就可以說線程安全了(因為沒有中間的三部環節,一步到位【原子性】~
原子性就是執行某一個操作是不可分割的,
- 比如上面所說的count++
操作,它就不是一個原子性的操作,它是分成了三個步驟的來實現這個操作的~
- JDK中有atomic包提供給我們實現原子性操作~
也有人將其做成了表格來分類,我們來看看:
使用這些類相關的操作也可以進他的博客去看看:
3.2.2可見性
對於可見性,Java提供了一個關鍵字:volatile給我們使用~
- 我們可以簡單認為:volatile是一種輕量級的同步機制
volatile經典總結:volatile僅僅用來保證該變量對所有線程的可見性,但不保證原子性
我們將其拆開來解釋一下:
- 保證該變量對所有線程的可見性
- 在多線程的環境下:當這個變量修改時,所有的線程都會知道該變量被修改了,也就是所謂的“可見性”
- 不保證原子性
- 修改變量(賦值)實質上是在JVM中分了好幾步,而在這幾步內(從裝載變量到修改),它是不安全的。
使用了volatile修飾的變量保證了三點:
- 一旦你完成寫入,任何訪問這個字段的線程將會得到最新的值
- 在你寫入前,會保證所有之前發生的事已經發生,並且任何更新過的數據值也是可見的,因為內存屏障會把之前的寫入值都刷新到緩存。
- volatile可以防止重排序(重排序指的就是:程序執行的時候,CPU、編譯器可能會對執行順序做一些調整,導致執行的順序並不是從上往下的。從而出現了一些意想不到的效果)。而如果聲明了volatile,那么CPU、編譯器就會知道這個變量是共享的,不會被緩存在寄存器或者其他不可見的地方。
一般來說,volatile大多用於標志位上(判斷操作),滿足下面的條件才應該使用volatile修飾變量:
- 修改變量時不依賴變量的當前值(因為volatile是不保證原子性的)
- 該變量不會納入到不變性條件中(該變量是可變的)
- 在訪問變量的時候不需要加鎖(加鎖就沒必要使用volatile這種輕量級同步機制了)
參考資料:
- http://www.cnblogs.com/Mainz/p/3556430.html
- https://www.cnblogs.com/Mainz/p/3546347.html
- http://www.dataguru.cn/java-865024-1-1.html
3.3線程封閉
在多線程的環境下,只要我們不使用成員變量(不共享數據),那么就不會出現線程安全的問題了。
就用我們熟悉的Servlet來舉例子,寫了那么多的Servlet,你見過我們說要加鎖嗎??我們所有的數據都是在方法(棧封閉)上操作的,每個線程都擁有自己的變量,互不干擾!
在方法上操作,只要我們保證不要在棧(方法)上發布對象(每個變量的作用域僅僅停留在當前的方法上),那么我們的線程就是安全的
在線程封閉上還有另一種方法,就是我之前寫過的:ThreadLocal就是這么簡單
使用這個類的API就可以保證每個線程自己獨占一個變量。(詳情去讀上面的文章即可)~
3.4不變性
不可變對象一定線程安全的。
上面我們共享的變量都是可變的,正由於是可變的才會出現線程安全問題。如果該狀態是不可變的,那么隨便多個線程訪問都是沒有問題的!
Java提供了final修飾符給我們使用,final的身影我們可能就見得比較多了,但值得說明的是:
- final僅僅是不能修改該變量的引用,但是引用里邊的數據是可以改的!
就好像下面這個HashMap,用final修飾了。但是它僅僅保證了該對象引用hashMap變量
所指向是不可變的,但是hashMap內部的數據是可變的,也就是說:可以add,remove等等操作到集合中~~~
- 因此,僅僅只能夠說明hashMap是一個不可變的對象引用
final HashMap<Person> hashMap = new HashMap<>();
不可變的對象引用在使用的時候還是需要加鎖的
- 或者把Person也設計成是一個線程安全的類~
- 因為內部的狀態是可變的,不加鎖或者Person不是線程安全類,操作都是有危險的!
要想將對象設計成不可變對象,那么要滿足下面三個條件:
- 對象創建后狀態就不能修改
- 對象所有的域都是final修飾的
- 對象是正確創建的(沒有this引用逸出)
String在我們學習的過程中我們就知道它是一個不可變對象,但是它沒有遵循第二點(對象所有的域都是final修飾的),因為JVM在內部做了優化的。但是我們如果是要自己設計不可變對象,是需要滿足三個條件的。
3.5線程安全性委托
很多時候我們要實現線程安全未必就需要自己加鎖,自己來設計。
我們可以使用JDK給我們提供的對象來完成線程安全的設計:
非常多的"工具類"供我們使用,這些在往后的學習中都會有所介紹的~~這里就不介紹了
四、最后
正確使用多線程能夠提高我們應用程序的效率,同時給我們會帶來非常多的問題,這些都是我們在使用多線程之前需要注意的地方。
無論是不變性、可見性、原子性、線程封閉、委托這些都是實現線程安全的一種手段。要合理地使用這些手段,我們的程序才可以更加健壯!
可以發現的是,上面在很多的地方說到了:鎖。但我沒有介紹它,因為我打算留在下一篇來寫,敬請期待~~~
書上前4章花了65頁來講解,而我只用了一篇文章來概括,這是遠遠不夠的,想要繼續深入的同學可以去閱讀書籍~
之前在學習操作系統的時候根據《計算機操作系統-湯小丹》這本書也做了一點點筆記,都是比較淺顯的知識點。或許對大家有幫助
參考資料:
- 《Java核心技術卷一》
- 《Java並發編程實戰》
- 《計算機操作系統-湯小丹》
如果文章有錯的地方歡迎指正,大家互相交流。習慣在微信看技術文章,想要獲取更多的Java資源的同學,可以關注微信公眾號:Java3y。為了大家方便,剛新建了一下qq群:742919422,大家也可以去交流交流。謝謝支持了!希望能多介紹給其他有需要的朋友
文章的目錄導航: