并发编程基础知识点
上下文切換
CPU通過時間片分配算法來循環執行任務,當前任務執行一個時間片后會切換到下一個
任務。但是,在切換前會保存上一個任務的狀態,以便下次切換回這個任務時,可以再加載這個任務的狀態。所以任務從保存到再加載的過程就是一次上下文切換。
這就像我們同時讀兩本書,當我們在讀一本英文的技術書時,發現某個單詞不認識,于是便打開中英文字典,但是在放下英文技術書之前,大腦必須先記住這本書讀到了多少頁的第多少行,等查完單詞之后,能夠繼續讀這本書。這樣的切換是會影響讀書效率的,同樣上下文切換也會影響多線程的執行速度。
線程的優勢
1、發揮多處理器的強大能力。可以使多線程在不同的CPU上執行,充分利用多CPU的優勢。
2、能夠充分的利用cpu空閑時間。比如當程序在等待某個IO操作,完成時,CPU將出于空閑狀態,這時CPU可以運行別的線程,提高CPU的利用率。
3、簡化開發流程,可以使用不同的線程開發不同的業務功能,代碼邏輯更清晰。
線程帶來的風險
1、安全性問題。
安全性問題其實就是線程安全性,這一點是非常復雜的,因為在沒有同步的情況下,多個線程同時執行,執行順序是不可預測的,可能會出現奇怪的結果。
2、活躍性問題。
關注的是某件正確事情最終會發生。由于線程的引入,會出現A線程在等待線程B釋放其持有的資源,而B線程永遠都不釋放該資源,那么A就永久的無法執行。
3、性能問題。
在多線程中,線程調度器臨時掛起活躍線程轉而運行另一個線程就會出現上下文切換,會保存和恢復執行上下文,讓cpu會開銷在線程調度上而不是運行商。
Daemon線程
Daemon線程是一種支持型線程,因為它主要被用作程序中后臺調度以及支持性工作。這
意味著,當一個Java虛擬機中不存在非Daemon線程的時候,Java虛擬機將會退出。可以通過調用Thread.setDaemon(true)將線程設置為Daemon線程。
注意 Daemon屬性需要在啟動線程之前設置,不能在啟動線程之后設置。
Daemon線程被用作完成支持性工作,但是在Java虛擬機退出時Daemon線程中的finally塊
并不一定會執行,示例如下代碼所示。
運行Daemon程序,可以看到在終端或者命令提示符上沒有任何輸出。main線程(非
Daemon線程)在啟動了線程DaemonRunner之后隨著main方法執行完畢而終止,而此時Java虛擬機中已經沒有非Daemon線程,虛擬機需要退出。Java虛擬機中的所有Daemon線程都需要立即終止,因此DaemonRunner立即終止,但是DaemonRunner中的finally塊并沒有執行。
注意 在構建Daemon線程時,不能依靠finally塊中的內容來確保執行關閉或清理資源
的邏輯。
線程安全
如果你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。如果每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。
或者說:一個類或者程序所提供的接口對于線程來說是原子操作或者多個線程之間的切換不會導致該接口的執行結果存在二義性,也就是說我們不用考慮同步的問題。
線程安全問題都是由全局變量及靜態變量引起的。 若每個線程中對全局變量、靜態變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執行寫操作,一般都需要考慮線程同步,否則就可能影響線程安全。
原子操作
原子(atom)本意是“不能被進一步分割的最小粒子”,而原子操作(atomic operation)意為”不可被中斷的一個或一系列操作” 。
例如 count=5
下面的兩個例子在java中不是原子操作
反例1:
看上去只是一個操作,但這個操作并不是原子操作。實際上它包含了三個獨立的操作
1. 讀取count的值
2. 將count值加1
3. 將計算結果寫入count。
這是一個讀取、修改、寫入的操作序列,并且其結果依賴于之前的狀態。
反例2:
private Object obj = new Object();該操作可以分解成如下3個步驟:
1. 分配對象的內存空間
2. 初始化對象
3. 設置obj 指向剛分配的內存地址
java中原子操作
示例1:
通過加鎖方式實現原子操作。
示例2:
private volatile Object obj = new Object();通過volatile 實現原子操作
競態條件
當某個計算的正確性取決于多個線程的交替執行順序時,那么就會發生靜態條件。
例如下面例子:
多線程在執行add方法時,就會出現競態條件。
根據上面原子操作中的示例,把 this.count = this.count + num; 分解成三步(其實機器碼不止三步,這里只是為了說明產生競態條件)
1. 讀取count的值
2. 將count值加上num
3. 將計算結果寫入count。
下面通過分析2個線程同時并發訪問add方法可能執行的順序。
| 1 | A: 從主內存中讀取 this.count 到工作內存 (0) |
| 2 | B: 從主內存中讀取 this.count 到工作內存 (0) |
| 3 | B: 將工作內存中的值加2 |
| 4 | B: 回寫工作內存中的值(2)到主內存. this.count 現在等于 2 |
| 5 | A: 將工作內存中的值加3 |
| 6 | A: 回寫工作內存中的值(3)到主內存. this.count 現在等于 3 |
兩個線程分別在count變量上加了2和3,兩個線程執行結束后count變量的值應該等于5。然而由于兩個線程是交叉執行的,兩個線程從內存中讀出的初始值都是0。然后各自加了2和3,并分別寫回內存。最終的值并不是期望的5,而是最后寫回內存的那個線程的值,上面例子中最后寫回內存的是線程A,但實際中也可能是線程B。如果沒有采用合適的同步機制,線程間的交叉執行情況就無法預料。
這樣執行結果依賴多線程的交替執行順序而使得結果不確定,可能是2、3、5三種結果。
臨界區
,add方法就是臨界區。
導致競態條件發生的代碼區稱作臨界區。上面競態條件中例子中的add()方法就是一個臨界區,它會產生競態條件。在臨界區中使用適當的同步就可以避免競態條件。
參考和摘抄java并發編程藝術、java并發編程藝術4
本人簡書blog地址:http://www.jianshu.com/u/1f0067e24ff8????
點擊這里快速進入簡書
總結
- 上一篇: JVM基于栈的解释器执行原理
- 下一篇: AbstractQueuedSynchr