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