[Java 并發編程:核心理論](http://www.cnblogs.com/paddix/p/5374810.html)
[TOC]
Java并發編程系列:
* [Java 并發編程:核心理論](http://www.cnblogs.com/paddix/p/5374810.html)?
* [Java并發編程:Synchronized及其實現原理](http://www.cnblogs.com/paddix/p/5367116.html)
* [Java并發編程:Synchronized底層優化(輕量級鎖、偏向鎖)](http://www.cnblogs.com/paddix/p/5405678.html)
* [Java 并發編程:線程間的協作(wait/notify/sleep/yield/join)](http://www.cnblogs.com/paddix/p/5381958.html)
* [Java 并發編程:volatile的使用及其原理](http://www.cnblogs.com/paddix/p/5428507.html)
并發編程是Java程序員最重要的技能之一,也是最難掌握的一種技能。它要求編程者對計算機最底層的運作原理有深刻的理解,同時要求編程者邏輯清晰、思維縝密,這樣才能寫出高效、安全、可靠的多線程并發程序。本系列會從線程間協調的方式(wait、notify、notifyAll)、Synchronized及Volatile的本質入手,詳細解釋JDK為我們提供的每種并發工具和底層實現機制。在此基礎上,我們會進一步分析java.util.concurrent包的工具類,包括其使用方式、實現源碼及其背后的原理。本文是該系列的第一篇文章,是這系列中最核心的理論部分,之后的文章都會以此為基礎來分析和解釋。
**一、共享性**
數據共享性是線程安全的主要原因之一。如果所有的數據只是在線程內有效,那就不存在線程安全性問題,這也是我們在編程的時候經常不需要考慮線程安全的主要原因之一。但是,在多線程編程中,數據共享是不可避免的。最典型的場景是數據庫中的數據,為了保證數據的一致性,我們通常需要共享同一個數據庫中數據,即使是在主從的情況下,訪問的也同一份數據,主從只是為了訪問的效率和數據安全,而對同一份數據做的副本。我們現在,通過一個簡單的示例來演示多線程下共享數據導致的問題:
代碼段一:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
`package`?`com.paddx.test.concurrent;`
`public`?`class`?`ShareData {`
`public`?`static`?`int`?`count =?``0``;`
`public`?`static`?`void`?`main(String[] args) {`
`final`?`ShareData data =?``new`?`ShareData();`
`for`?`(``int`?`i =?``0``; i <?``10``; i++) {`
`new`?`Thread(``new`?`Runnable() {`
`@Override`
`public`?`void`?`run() {`
`try`?`{`
`//進入的時候暫停1毫秒,增加并發問題出現的幾率`
`Thread.sleep(``1``);`
`}?``catch`?`(InterruptedException e) {`
`e.printStackTrace();`
`}`
`for`?`(``int`?`j =?``0``; j <?``100``; j++) {`
`data.addCount();`
`}`
`System.out.print(count +?``" "``);`
`}`
`}).start();`
`}`
`try`?`{`
`//主程序暫停3秒,以保證上面的程序執行完成`
`Thread.sleep(``3000``);`
`}?``catch`?`(InterruptedException e) {`
`e.printStackTrace();`
`}`
`System.out.println(``"count="`?`+ count);`
`}`
`public`?`void`?`addCount() {`
`count++;`
`}`
`}`
|
上述代碼的目的是對count進行加一操作,執行1000次,不過這里是通過10個線程來實現的,每個線程執行100次,正常情況下,應該輸出1000。不過,如果你運行上面的程序,你會發現結果卻不是這樣。下面是某次的執行結果(每次運行的結果不一定相同,有時候也可能獲取到正確的結果):
?
可以看出,對共享變量操作,在多線程環境下很容易出現各種意想不到的的結果。
**二、互斥性**
資源互斥是指同時只允許一個訪問者對其進行訪問,具有唯一性和排它性。我們通常允許多個線程同時對數據進行讀操作,但同一時間內只允許一個線程對數據進行寫操作。所以我們通常將鎖分為共享鎖和排它鎖,也叫做讀鎖和寫鎖。如果資源不具有互斥性,即使是共享資源,我們也不需要擔心線程安全。例如,對于不可變的數據共享,所有線程都只能對其進行讀操作,所以不用考慮線程安全問題。但是對共享數據的寫操作,一般就需要保證互斥性,上述例子中就是因為沒有保證互斥性才導致數據的修改產生問題。Java 中提供多種機制來保證互斥性,最簡單的方式是使用Synchronized。現在我們在上面程序中加上Synchronized再執行:
代碼段二:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
`package`?`com.paddx.test.concurrent;`
`public`?`class`?`ShareData {`
`public`?`static`?`int`?`count =?``0``;`
`public`?`static`?`void`?`main(String[] args) {`
`final`?`ShareData data =?``new`?`ShareData();`
`for`?`(``int`?`i =?``0``; i <?``10``; i++) {`
`new`?`Thread(``new`?`Runnable() {`
`@Override`
`public`?`void`?`run() {`
`try`?`{`
`//進入的時候暫停1毫秒,增加并發問題出現的幾率`
`Thread.sleep(``1``);`
`}?``catch`?`(InterruptedException e) {`
`e.printStackTrace();`
`}`
`for`?`(``int`?`j =?``0``; j <?``100``; j++) {`
`data.addCount();`
`}`
`System.out.print(count +?``" "``);`
`}`
`}).start();`
`}`
`try`?`{`
`//主程序暫停3秒,以保證上面的程序執行完成`
`Thread.sleep(``3000``);`
`}?``catch`?`(InterruptedException e) {`
`e.printStackTrace();`
`}`
`System.out.println(``"count="`?`+ count);`
`}`
`/**`
`* 增加 synchronized 關鍵字`
`*/`
`public`?`synchronized`?`void`?`addCount() {`
`count++;`
`}`
`}`
|
現在再執行上述代碼,會發現無論執行多少次,返回的最終結果都是1000。
**三、原子性**
原子性就是指對數據的操作是一個獨立的、不可分割的整體。換句話說,就是一次操作,是一個連續不可中斷的過程,數據不會執行的一半的時候被其他線程所修改。保證原子性的最簡單方式是操作系統指令,就是說如果一次操作對應一條操作系統指令,這樣肯定可以能保證原子性。但是很多操作不能通過一條指令就完成。例如,對long類型的運算,很多系統就需要分成多條指令分別對高位和低位進行操作才能完成。還比如,我們經常使用的整數 i++ 的操作,其實需要分成三個步驟:(1)讀取整數 i 的值;(2)對 i 進行加一操作;(3)將結果寫回內存。這個過程在多線程下就可能出現如下現象:

這也是代碼段一執行的結果為什么不正確的原因。對于這種組合操作,要保證原子性,最常見的方式是加鎖,如Java中的Synchronized或Lock都可以實現,代碼段二就是通過Synchronized實現的。除了鎖以外,還有一種方式就是CAS(Compare And Swap),即修改數據之前先比較與之前讀取到的值是否一致,如果一致,則進行修改,如果不一致則重新執行,這也是樂觀鎖的實現原理。不過CAS在某些場景下不一定有效,比如另一線程先修改了某個值,然后再改回原來值,這種情況下,CAS是無法判斷的。
**四、可見性**
? 要理解可見性,需要先對JVM的內存模型有一定的了解,JVM的內存模型與操作系統類似,如圖所示:

從這個圖中我們可以看出,每個線程都有一個自己的工作內存(相當于CPU高級緩沖區,這么做的目的還是在于進一步縮小存儲系統與CPU之間速度的差異,提高性能),對于共享變量,線程每次讀取的是工作內存中共享變量的副本,寫入的時候也直接修改工作內存中副本的值,然后在某個時間點上再將工作內存與主內存中的值進行同步。這樣導致的問題是,如果線程1對某個變量進行了修改,線程2卻有可能看不到線程1對共享變量所做的修改。通過下面這段程序我們可以演示一下不可見的問題:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
`package`?`com.paddx.test.concurrent;`
`public`?`class`?`VisibilityTest {`
`private`?`static`?`boolean`?`ready;`
`private`?`static`?`int`?`number;`
`private`?`static`?`class`?`ReaderThread?``extends`?`Thread {`
`public`?`void`?`run() {`
`try`?`{`
`Thread.sleep(``10``);`
`}?``catch`?`(InterruptedException e) {`
`e.printStackTrace();`
`}`
`if`?`(!ready) {`
`System.out.println(ready);`
`}`
`System.out.println(number);`
`}`
`}`
`private`?`static`?`class`?`WriterThread?``extends`?`Thread {`
`public`?`void`?`run() {`
`try`?`{`
`Thread.sleep(``10``);`
`}?``catch`?`(InterruptedException e) {`
`e.printStackTrace();`
`}`
`number =?``100``;`
`ready =?``true``;`
`}`
`}`
`public`?`static`?`void`?`main(String[] args) {`
`new`?`WriterThread().start();`
`new`?`ReaderThread().start();`
`}`
`}`
|
從直觀上理解,這段程序應該只會輸出100,ready的值是不會打印出來的。實際上,如果多次執行上面代碼的話,可能會出現多種不同的結果,下面是我運行出來的某兩次的結果:


當然,這個結果也只能說是有可能是可見性造成的,當寫線程(WriterThread)設置ready=true后,讀線程(ReaderThread)看不到修改后的結果,所以會打印false,對于第二個結果,也就是執行if (!ready)時還沒有讀取到寫線程的結果,但執行System.out.println(ready)時讀取到了寫線程執行的結果。不過,這個結果也有可能是線程的交替執行所造成的。Java 中可通過Synchronized或Volatile來保證可見性,具體細節會在后續的文章中分析。
**五、有序性**
為了提高性能,編譯器和處理器可能會對指令做重排序。重排序可以分為三種:
(1)編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。
(2)指令級并行的重排序。現代處理器采用了指令級并行技術(Instruction-Level Parallelism, ILP)來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
(3)內存系統的重排序。由于處理器使用緩存和讀/寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行。
我們可以直接參考一下JSR 133 中對重排序問題的描述:

(1) (2)
先看上圖中的(1)源碼部分,從源碼來看,要么指令 1 先執行要么指令 3先執行。如果指令 1 先執行,r2不應該能看到指令 4 中寫入的值。如果指令 3 先執行,r1不應該能看到指令 2 寫的值。但是運行結果卻可能出現r2==2,r1==1的情況,這就是“重排序”導致的結果。上圖(2)即是一種可能出現的合法的編譯結果,編譯后,指令1和指令2的順序可能就互換了。因此,才會出現r2==2,r1==1的結果。Java 中也可通過Synchronized或Volatile來保證順序性。
**六 總結**
本文對Java 并發編程中的理論基礎進行了講解,有些東西在后續的分析中還會做更詳細的討論,如可見性、順序性等。后續的文章都會以本章內容作為理論基礎來討論。如果大家能夠很好的理解上述內容,相信無論是去理解其他并發編程的文章還是在平時的并發編程的工作中,都能夠對大家有很好的幫助。
- JVM
- 深入理解Java內存模型
- 深入理解Java內存模型(一)——基礎
- 深入理解Java內存模型(二)——重排序
- 深入理解Java內存模型(三)——順序一致性
- 深入理解Java內存模型(四)——volatile
- 深入理解Java內存模型(五)——鎖
- 深入理解Java內存模型(六)——final
- 深入理解Java內存模型(七)——總結
- Java內存模型
- Java內存模型2
- 堆內內存還是堆外內存?
- JVM內存配置詳解
- Java內存分配全面淺析
- 深入Java核心 Java內存分配原理精講
- jvm常量池
- JVM調優總結
- JVM調優總結(一)-- 一些概念
- JVM調優總結(二)-一些概念
- VM調優總結(三)-基本垃圾回收算法
- JVM調優總結(四)-垃圾回收面臨的問題
- JVM調優總結(五)-分代垃圾回收詳述1
- JVM調優總結(六)-分代垃圾回收詳述2
- JVM調優總結(七)-典型配置舉例1
- JVM調優總結(八)-典型配置舉例2
- JVM調優總結(九)-新一代的垃圾回收算法
- JVM調優總結(十)-調優方法
- 基礎
- Java 征途:行者的地圖
- Java程序員應該知道的10個面向對象理論
- Java泛型總結
- 序列化與反序列化
- 通過反編譯深入理解Java String及intern
- android 加固防止反編譯-重新打包
- volatile
- 正確使用 Volatile 變量
- 異常
- 深入理解java異常處理機制
- Java異常處理的10個最佳實踐
- Java異常處理手冊和最佳實踐
- Java提高篇——對象克隆(復制)
- Java中如何克隆集合——ArrayList和HashSet深拷貝
- Java中hashCode的作用
- Java提高篇之hashCode
- 常見正則表達式
- 類
- 理解java類加載器以及ClassLoader類
- 深入探討 Java 類加載器
- 類加載器的工作原理
- java反射
- 集合
- HashMap的工作原理
- ConcurrentHashMap之實現細節
- java.util.concurrent 之ConcurrentHashMap 源碼分析
- HashMap的實現原理和底層數據結構
- 線程
- 關于Java并發編程的總結和思考
- 40個Java多線程問題總結
- Java中的多線程你只要看這一篇就夠了
- Java多線程干貨系列(1):Java多線程基礎
- Java非阻塞算法簡介
- Java并發的四種風味:Thread、Executor、ForkJoin和Actor
- Java中不同的并發實現的性能比較
- JAVA CAS原理深度分析
- 多個線程之間共享數據的方式
- Java并發編程
- Java并發編程(1):可重入內置鎖
- Java并發編程(2):線程中斷(含代碼)
- Java并發編程(3):線程掛起、恢復與終止的正確方法(含代碼)
- Java并發編程(4):守護線程與線程阻塞的四種情況
- Java并發編程(5):volatile變量修飾符—意料之外的問題(含代碼)
- Java并發編程(6):Runnable和Thread實現多線程的區別(含代碼)
- Java并發編程(7):使用synchronized獲取互斥鎖的幾點說明
- Java并發編程(8):多線程環境中安全使用集合API(含代碼)
- Java并發編程(9):死鎖(含代碼)
- Java并發編程(10):使用wait/notify/notifyAll實現線程間通信的幾點重要說明
- java并發編程-II
- Java多線程基礎:進程和線程之由來
- Java并發編程:如何創建線程?
- Java并發編程:Thread類的使用
- Java并發編程:synchronized
- Java并發編程:Lock
- Java并發編程:volatile關鍵字解析
- Java并發編程:深入剖析ThreadLocal
- Java并發編程:CountDownLatch、CyclicBarrier和Semaphore
- Java并發編程:線程間協作的兩種方式:wait、notify、notifyAll和Condition
- Synchronized與Lock
- JVM底層又是如何實現synchronized的
- Java synchronized詳解
- synchronized 與 Lock 的那點事
- 深入研究 Java Synchronize 和 Lock 的區別與用法
- JAVA編程中的鎖機制詳解
- Java中的鎖
- TreadLocal
- 深入JDK源碼之ThreadLocal類
- 聊一聊ThreadLocal
- ThreadLocal
- ThreadLocal的內存泄露
- 多線程設計模式
- Java多線程編程中Future模式的詳解
- 原子操作(CAS)
- [譯]Java中Wait、Sleep和Yield方法的區別
- 線程池
- 如何合理地估算線程池大小?
- JAVA線程池中隊列與池大小的關系
- Java四種線程池的使用
- 深入理解Java之線程池
- java并發編程III
- Java 8并發工具包漫游指南
- 聊聊并發
- 聊聊并發(一)——深入分析Volatile的實現原理
- 聊聊并發(二)——Java SE1.6中的Synchronized
- 文件
- 網絡
- index
- 內存文章索引
- 基礎文章索引
- 線程文章索引
- 網絡文章索引
- IOC
- 設計模式文章索引
- 面試
- Java常量池詳解之一道比較蛋疼的面試題
- 近5年133個Java面試問題列表
- Java工程師成神之路
- Java字符串問題Top10
- 設計模式
- Java:單例模式的七種寫法
- Java 利用枚舉實現單例模式
- 常用jar
- HttpClient和HtmlUnit的比較總結
- IO
- NIO
- NIO入門
- 注解
- Java Annotation認知(包括框架圖、詳細介紹、示例說明)