1、概念
進程和線程都是一個時間段的描述,是CPU工作時間段的描述。兩者顆粒度不同。
進程是CPU資源分配的最小單位,可以理解為一個應用程序。
線程是CPU調度的最小單位,是建立在進程的基礎上的一次程序運行單位。
2、三個核心
原子性
一個操作,要么全部執行,要不么全部不執行。
簡單的說,就是在一個線程對共享變量進行操作時,阻塞其他線程對該變量的操作。
可見性
當線程操作某個變量時,順位為:
1、將變量從主內存拷貝到工作內存中。
2、執行代碼,操作共享變量值。
3、將工作內存的數據刷新到主內存中。
多個線程并發訪問共享變量時,一個線程對共享變量的操作,其他線程能夠立刻看到。
每個線程讀取共享變量時,都會將該變量加載進其對應CPU的高速緩存里,修改該變量后,CPU會立即更新該緩存,但并不一定會立即將其寫回主內存(實際上寫回主內存的時間不可預期)。此時其它線程(尤其是不在同一個CPU上執行的線程)訪問該變量時,從主內存中讀到的就是舊的數據,而非第一個線程更新后的數據。
順序性
程序的執行順序按照代碼的先后順序執行。
int a,b;
a++;
b++;
if(b==1){
print(a);
}
在理想情況下,當b=1時,a=1,但是實際情況中,JVM在執行代碼的過程中,并不一定按照代碼的順序執行,有可能先執行b++,后執行a++。
happens-before 原則
1、程序次序規則:一個線程內,按照代碼順序,書寫在前面的操作先行發生于書寫在后面的操作。
2、鎖定規則:一個unlock操作一定發生在lock操作之前。
3、volatile變量規則:對一個變量的寫操作先行發生于后面的讀操作。
4、傳遞規則:如果操作A先行發生于操作B,而操作B又先行發生于操作C,則可以得出操作A先行發生于操作C。
5、線程啟動規則:Thread對象的所有操作都發生在start()之后。
6、線程中斷規則:線程interrupt()方法的調用先行發生于被中斷線程的代碼檢測到中斷事件的發生。
7、線程終結規則:線程中所有的操作都先行發生于線程的終止檢測。
8、對象終結規則:一個對象的初始化完成先行發生于他的finalize()方法的開始。
對于程序次序規則,應該理解為jvm保證最終執行的結果與程序順序執行的結果一致。jvm有可能對不存在數據依賴性的指令進行重排序。實際上,這個規則是用來保證單線程中執行結果的正確性,但無法保證程序在多線程中執行的正確性。
3、線程狀態
- INIT : 線程對象進行new初始化后,此時還未調用start()。
- NEW : 線程對象調用start()方法后,進去可運行狀態。如果處于RUNABLED狀態的線程調用yield()后,會釋放占用的資源,重新進入NEW狀態。
- RUNABLED : 線程獲取到CPU時間片,進入運行狀態。
- BLOCKED : 線程調用sleep()或者join()方法后,進去阻塞狀態,此時線程不釋放所占有的系統資源。當sleep()結束或者join()等到其他線程到來,當前線程進入RUNABLED狀態。
- TIME WAITING : 線程進入到RUNABLED狀態,還未開始運行的時候,發現要獲取的資源處于同步狀態,該線程就會進入TIME WAITING狀態,等待資源釋放;當前線程使用wait()方法后,進入TIME WAITING狀態,只有在獲得notify()或者notifyAll()通知后,才會進入WAITING狀態。
4、關鍵字
synchronized
當synchronized修飾一個方法或者代碼塊的時候,保證同時只有一個線程可以訪問該方法或代碼塊。保證了線程的執行順序性和可見性。
synchronized(鎖){
臨界區代碼
}
public void synchronized method(){
方法體
}
synchronized修飾代碼塊,鎖就是這個對象;
synchronized修飾方法,鎖就是這個class。
理論上所有對象都可以成為鎖,但是能被多個線程共享的鎖才有意義。
每個鎖對象有兩個隊列,一個是就緒隊列,一個是阻塞隊列。就緒隊列存儲了即將獲取鎖的線程,阻塞隊列存儲被阻塞的線程。
java內置鎖是可重入鎖,子類可以獲得父類的鎖資源。
synchronized是一種悲觀鎖,會導致其他所有需要鎖的線程掛起,等待持有鎖的線程釋放鎖。這樣的鎖對性能不夠友好。
volatile
volatile關鍵字可以保證可見性,當使用volatile來修飾某個共享變量時,會保證該變量的修改會立刻更新到主內存中,并且將其他緩存中對該變量的緩存設置為無效,其他線程需要重新從主內存讀取該變量。
volatile關鍵字可以禁止進行指令重排序。
單線程下
x = 1; //語句1
y=0; //語句2
volatile flag = true; //語句3
x = 2; //語句4
y = 4; //語句5
使用volatile修飾flag后,jvm在進行指令重排序時,不會將語句4,5放在語句3之前,也不會將語句1,2放在語句3之后。
多線程下
//線程1
object = loadObject(); //語句1
init = true; //語句2
//線程2
while(!init){
...
}
doSomething(object);
在多線程的情況下,線程1有可能先執行語句2,假如此時線程1進入阻塞,線程2開始執行,但此時語句1還沒執行,obejct沒有被初始化,導致程序出錯。
這里用volatile修飾init,可以保證語句1先執行。
原理
加入volatile關鍵字時,會多出一個lock前綴指令。
lock前綴指令相當于一個內存屏障,有3個功能:
(1)、確保指令重排序時不會把內存屏障之后的指令排在內存屏障之前,也不會把內存屏障之前的指令排在內存屏障之后。
(2)、強制將對緩存的修改操作立即寫入主內存。
(3)、如果是寫操作,會導致其他CPU中對應的緩存行無效。
Lock
java.util.concurrent.lock中的lock框架是鎖定的一個抽象,它允許把鎖定的實現作為java類。它擁有與synchronized相同的并發性和內存語義,但是添加了類似鎖投票、定時鎖等候和中斷鎖等候的一些特性。此外,它在激烈爭用的情況下具有更加的性能。
不要忘了在finally中釋放lock
讀寫鎖
ReentrantReadWriteLock
悲觀鎖和樂觀鎖
共享鎖和排他鎖
CAS
Compare And Swape,比較并交換。目前CAS被廣泛應用于硬件層面的并發操作。
樂觀鎖的機制就是CAS,樂觀鎖就是每次不加鎖,假設沒有沖突的去完成某項操作,如果因為沖突失敗就重試,直到成功為止。
CAS操作包含三個操作數--內存位置V,預期原值A和新值B。如果內存位置的值與預期原值匹配,那么將該位置替換為新值。否則,處理器不作任何處理。
利用CPU的CAS指令,同時借助JNI來完成JAVA的非阻塞算法。其他原子操作都是利用類似的特性完成的。而整個JUC都是建立在CAS的基礎上的。
缺點
CAS雖然具有很高效的原子操作,但是CAS仍然存在三大問題。
(1)、ABA問題。如果一個值原來是A,變成了B,又變成了A,那么使用CAS檢查時會認為它的值沒有發生變化,但是實際上發生了變化。解決思路就是加版本號,1A-2B-3A。
(2)、循環時間長開銷大。CAS是自旋鎖,如果長時間不成功,會給CPU帶來非常大的執行開銷。
(3)、只能保證一個共享變量的原子操作。
Double-Check
在單例模式的懶漢式中,存在雙重檢查,這種方式在多線程中是不安全的。
public Singleton getResource(){
if (resource == null){ //語句1
synchronized(this){
if (resource == null) { //語句2
resource = new Singleton(); //語句3
}
}
}
return resource;
}
假設線程1執行到了語句1,執行了new Resource()指令,但是還未給resource賦值,此時線程1阻塞,線程2開始執行,在判斷resource == null是,因為已經分配了內存空間(未賦值),該語句為false,就返回了未完成初始化的resource,造成程序錯誤。
改進方法
(1)、在方法上加synchronized。
public synchronized Singleton getResource(){
if (resource == null){
resource = new Singleton();
}
return resource;
}
(2)、使用volatile
private valotile Singleton resource = null;
public synchronized Singleton getResource(){
if (resource == null){
resource = new Singleton();
}
return resource;
}