每個線程都有自己的執行空間(即工作內存),線程執行的時候用到某變量,首先要將變量從主內存拷貝的自己的工作內存空間,然后對變量進行操作:讀取,修改,賦值等,這些均在工作內存完成,操作完成后再將變量寫回主內存。當多個線程同時讀寫某個內存數據時,就會涉及到線程并發的問題。涉及到三個特 性:原子性,有序性,可見性。
簡單說下這個三個特性的概念:
switch(線程特性){
case (可見性):
一個數據在多個線程中都存在副本的時候,任何一個線程對共享變量的修改,其它線程都應該看到被修改之后的值。
break;
case(有序性):
線程的有序性即按代碼的先后順序執行。很經典的就是銀行的存錢取錢問題,比如A線程負責取錢,B線程負責取錢,賬戶里面有100塊,這時候B和A都讀取了賬戶余額,100塊,這時B取出了10塊,寫入主內存后這時候賬戶還有90塊,但A知道的是100塊然后存了10塊,再寫入主內存就是110塊,這顯然是不對的,沒有保存線程的有序性。
break;
case ( 原子性):
原子性是指一個操作是不可中斷的。即使是在多個線程一起執行的時候,一個操作一旦開始,就不會被其它線程干擾。
Java中的原子操作包括:
1)除long和double之外的基本類型的賦值操作
2)所有引用reference的賦值操作
3)java.concurrent.Atomic.* 包中所有類的一切操作。
線程之間的交互只能通過共享數據來實現,而不能直接傳遞數據。
同步是為了解決多個線程對共享數據的訪問和操作混亂達不到預期效果這種情況而引入的機制。
break;
}
Synchronized
看如下代碼:在main方法中:
for (int index = 0; index < 3; index++) {
new Thread() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace(); }
incTestNum(); }
} }.start();
}
new Thread() {
@Override
public void run() {
for (int i = 0; i < 300; i++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace(); }
getTestNum(); }
}}.start();
private static void incTestNum() {
i++; j++;}
private static void getTestNum() {
System.out.println("i===========" + i + ";j============" + j);}
我們得到 的結果是:
i===========63;j============63
i===========93;j============93
i===========121;j============122
i===========151;j============152
i===========180;j============182
i===========210;j============212
i===========240;j============242
i===========267;j============270
可以看到j有可能會比i大,這是在多線程并發操作i和j 的時候,并沒有同步線程,此時同時操作i和j不具有原子性。并且i++本身也不是原子操作,先讀取i,再執行i+1;然后再賦值給i;然后再寫入內存。但直觀來說應該i比j大,這是應該在讀取i之后和讀取j之前加操作又執行了多次。導致看到的j比i大。
當我們加上在線程里面加上synchronized之后:
private synchronized static void incTestNum() {
i++; j++;}
private synchronized static void getTestNum() {
System.out.println("i===========" + i + ";j============" + j);}
結果:
i===========92;j============92
i===========121;j============121
i===========150;j============150
i===========182;j============182
i===========209;j============209
i===========240;j============240
i===========269;j============269
......
i===========3000;j============3000
從結果可以看出,用synchronized鎖住的方法是同步執行的,并且得到了正確的結果。
使用synchronized修飾的方法或者代碼塊可以看成是一個原子操作。如果一個線程獲取了鎖,其它線程需要獲取鎖來執行的時候,其它線程就進入了等待鎖的釋放。這個過程是阻塞的。
一個線程執行互斥代碼過程如下:
- 獲得同步鎖;
- 清空工作內存;
- 從主內存拷貝對象副本到工作內存;
- 執行代碼(計算或者輸出等);
- 刷新主內存數據;
- 釋放同步鎖。
所以,synchronized既保證了多線程的并發有序性,又保證了多線程的內存可見性。
如果在靜態方法上加synchronized,那么作用等同于:
void method{
synchronized(Obl.class)
}
}
既然要同步我們就要用線程之間的同享對象作為鎖,所以下面方式是錯誤的使用:
Object lock=new Object();
synchronized(lock){
}
使用同步方法的時候:
private synchronized void test(){ }
等價于:
private void test(){
synchronized(this){
}
}
jdK1.5之后,對synchronized同步機制做了很多優化,如:自適應的自旋鎖、鎖粗化、鎖消除、輕量級鎖等,使得它的性能明顯有了很大的提升。
volatile
關于volatile的實現原理可以看看這篇文章:
深入分析Volatile的實現原理
volatile告訴jvm, 它所修飾的變量不保留拷貝,直接訪問主內存中的。這就保證了可見性。
我們在上面個例子的共享變量加上volatile關鍵:
static volatile int i = 0, j = 0;
再來看看運行結果:
i===========241;j============241
i===========272;j============271
i===========301;j============301
i===========329;j============330
i===========359;j============360
i===========390;j============390
......
i===========2984;j============2993
這看起來加了volatile和沒有加是一樣的效果,看起來線程都沒有同步。原因是volatile不能保證操作的原子性。也就不能保證i++和j++的原子性,當A,B線程讀取i的值假設此時i=10,然后A線程執行+1再寫入,刷入主內存中,此時主內存i的值是11,然后現在B線程再執行+1寫入主內存,此時主內存中i的值被還是11,但此時正常情況應該是12 的。這就是為什么最后的結果i和j都比3000小。
聲明為volatile的簡單變量如果當前值與該變量以前的值相關,那么volatile關鍵字不起作用。如i++;i=i+1;
當且僅當滿足以下所有條件時,才應該使用volatile變量:
- 對變量的寫入操作不依賴變量的當前值,或者你能確保只有單個線程更新變量的值。
- 該變量沒有包含在具有其他變量的不變式中。
- 訪問變量不需要加鎖
通常使用在如下情況:
static class StopTester implements Runnable {
private boolean stop = false;
public void stopMe() {
stop=true;
}
@Override
public void run() {
while(!stop){
//TODO
}
}
}
volatile與synchronized的區別
- volatile修飾的變量存取時比一般變量消耗的資源要多一點,因為線程有它自己的變量拷貝更為高效。
- volatile本質是在告訴jvm當前變量在寄存器中的值是不確定的,需要從主存中讀取,synchronized則是鎖定當前變量,只有當前線程可以訪問該變量,其他線程被阻塞住.
- volatile僅能使用在變量級別,synchronized則可以使用在變量,方法.
- volatile僅能實現變量的修改可見性,但不具備原子特性,而synchronized則可以保證變量的修改可見性和原子性.
- volatile不會造成線程的阻塞,而synchronized可能會造成線程的阻塞.
- volatile標記的變量不會被編譯器優化,而synchronized標記的變量可以被編譯器優化.