# Java線程(二):線程同步synchronized和volatile
[上篇](http://blog.csdn.net/ghsau/article/details/7421217)通過一個簡單的例子說明了線程安全與不安全,在例子中不安全的情況下輸出的結(jié)果恰好是逐個遞增的(其實是巧合,多運行幾次,會產(chǎn)生不同的輸出結(jié)果),為什么會產(chǎn)生這樣的結(jié)果呢,因為建立的Count對象是線程共享的,一個線程改變了其成員變量num值,下一個線程正巧讀到了修改后的num,所以會遞增輸出。
要說明線程同步問題首先要說明Java線程的兩個特性,可見性和有序性。多個線程之間是不能直接傳遞數(shù)據(jù)交互的,它們之間的交互只能通過共享變量來實現(xiàn)。拿上篇博文中的例子來說明,在多個線程之間共享了Count類的一個對象,這個對象是被創(chuàng)建在主內(nèi)存(堆內(nèi)存)中,每個線程都有自己的工作內(nèi)存(線程棧),工作內(nèi)存存儲了主內(nèi)存Count對象的一個副本,當線程操作Count對象時,首先從主內(nèi)存復(fù)制Count對象到工作內(nèi)存中,然后執(zhí)行代碼count.count(),改變了num值,最后用工作內(nèi)存Count刷新主內(nèi)存Count。當一個對象在多個內(nèi)存中都存在副本時,如果一個內(nèi)存修改了共享變量,其它線程也應(yīng)該能夠看到被修改后的值,**此為可見性**。多個線程執(zhí)行時,CPU對線程的調(diào)度是隨機的,我們不知道當前程序被執(zhí)行到哪步就切換到了下一個線程,一個最經(jīng)典的例子就是銀行匯款問題,一個銀行賬戶存款100,這時一個人從該賬戶取10元,同時另一個人向該賬戶匯10元,那么余額應(yīng)該還是100。那么此時可能發(fā)生這種情況,A線程負責(zé)取款,B線程負責(zé)匯款,A從主內(nèi)存讀到100,B從主內(nèi)存讀到100,A執(zhí)行減10操作,并將數(shù)據(jù)刷新到主內(nèi)存,這時主內(nèi)存數(shù)據(jù)100-10=90,而B內(nèi)存執(zhí)行加10操作,并將數(shù)據(jù)刷新到主內(nèi)存,最后主內(nèi)存數(shù)據(jù)100+10=110,顯然這是一個嚴重的問題,我們要保證A線程和B線程有序執(zhí)行,先取款后匯款或者先匯款后取款,**此為有序性**。本文講述了JDK5.0之前傳統(tǒng)線程的同步方式,更高級的同步方式可參見[Java線程(八):鎖對象Lock-同步問題更完美的處理方式](http://blog.csdn.net/ghsau/article/details/7461369)。
下面同樣用代碼來展示一下線程同步問題。
TraditionalThreadSynchronized.java:創(chuàng)建兩個線程,執(zhí)行同一個對象的輸出方法。
~~~
public?class?TraditionalThreadSynchronized?{??
????public?static?void?main(String[]?args)?{??
????????final?Outputter?output?=?new?Outputter();??
????????new?Thread()?{??
????????????public?void?run()?{??
????????????????output.output("zhangsan");??
????????????}??
????????}.start();????????
????????new?Thread()?{??
????????????public?void?run()?{??
????????????????output.output("lisi");??
????????????}??
????????}.start();??
????}??
}??
class?Outputter?{??
????public?void?output(String?name)?{??
????????//?TODO?為了保證對name的輸出不是一個原子操作,這里逐個輸出name的每個字符??
????????for(int?i?=?0;?i?
????????????System.out.print(name.charAt(i));??
????????????//?Thread.sleep(10);??
????????}??
????}??
}??
~~~
運行結(jié)果:
1. zhlainsigsan??
顯然輸出的字符串被打亂了,我們期望的輸出結(jié)果是zhangsanlisi,這就是線程同步問題,我們希望output方法被一個線程完整的執(zhí)行完之后再切換到下一個線程,Java中使用synchronized保證一段代碼在多線程執(zhí)行時是互斥的,有兩種用法:
1\. 使用synchronized將需要互斥的代碼包含起來,并上一把鎖。
~~~
{??
????synchronized?(this)?{??
????????for(int?i?=?0;?i?
????????????System.out.print(name.charAt(i));??
????????}??
????}??
}??
~~~
這把鎖必須是需要互斥的多個線程間的共享對象,像下面的代碼是沒有意義的。
~~~
{??
????Object?lock?=?new?Object();??
????synchronized?(lock)?{??
????????for(int?i?=?0;?i?
????????????System.out.print(name.charAt(i));??
????????}??
????}??
}??
~~~
每次進入output方法都會創(chuàng)建一個新的lock,這個鎖顯然每個線程都會創(chuàng)建,沒有意義。
2\. 將synchronized加在需要互斥的方法上。
~~~
public?synchronized?void?output(String?name)?{??
????//?TODO?線程輸出方法??
????for(int?i?=?0;?i?
????????System.out.print(name.charAt(i));??
????}??
}??
~~~
這種方式就相當于用this鎖住整個方法內(nèi)的代碼塊,如果用synchronized加在靜態(tài)方法上,就相當于用××××.class鎖住整個方法內(nèi)的代碼塊。使用synchronized在某些情況下會造成死鎖,死鎖問題以后會說明。**使用synchronized修飾的方法或者代碼塊可以看成是一個原子操作**。
每個鎖對(JLS中叫monitor)都有兩個隊列,一個是就緒隊列,一個是阻塞隊列,就緒隊列存儲了將要獲得鎖的線程,阻塞隊列存儲了被阻塞的線程,當一個線程被喚醒(notify)后,才會進入到就緒隊列,等待CPU的調(diào)度,反之,當一個線程被wait后,就會進入阻塞隊列,等待下一次被喚醒,這個涉及到線程間的通信,下一篇博文會說明??次覀兊睦?,當?shù)谝粋€線程執(zhí)行輸出方法時,獲得同步鎖,執(zhí)行輸出方法,恰好此時第二個線程也要執(zhí)行輸出方法,但發(fā)現(xiàn)同步鎖沒有被釋放,第二個線程就會進入就緒隊列,等待鎖被釋放。一個線程執(zhí)行互斥代碼過程如下:
1\. 獲得同步鎖;
2\. 清空工作內(nèi)存;
3\. 從主內(nèi)存拷貝對象副本到工作內(nèi)存;
4\. 執(zhí)行代碼(計算或者輸出等);
5\. 刷新主內(nèi)存數(shù)據(jù);
6\. 釋放同步鎖。
所以,synchronized既保證了多線程的并發(fā)有序性,又保證了多線程的內(nèi)存可見性。
volatile是第二種Java多線程同步的機制,根據(jù)[JLS(Java LanguageSpecifications)](http://docs.oracle.com/javase/specs/jls/se7/jls7.pdf)的說法,一個變量可以被volatile修飾,在這種情況下內(nèi)存模型(主內(nèi)存和線程工作內(nèi)存)確保所有線程可以看到一致的變量值,來看一段代碼:
~~~
class?Test?{??
????static?int?i?=?0,?j?=?0;??
????static?void?one()?{??
????????i++;??
????????j++;??
????}??
????static?void?two()?{??
????????System.out.println("i="?+?i?+?"?j="?+?j);??
????}??
}??
~~~
一些線程執(zhí)行one方法,另一些線程執(zhí)行two方法,two方法有可能打印出j比i大的值,按照之前分析的線程執(zhí)行過程分析一下:
1\. 將變量i從主內(nèi)存拷貝到工作內(nèi)存;
2\. 改變i的值;
3\. 刷新主內(nèi)存數(shù)據(jù);
4\. 將變量j從主內(nèi)存拷貝到工作內(nèi)存;
5\. 改變j的值;
6\. 刷新主內(nèi)存數(shù)據(jù);
這個時候執(zhí)行two方法的線程先讀取了主存i原來的值又讀取了j改變后的值,這就導(dǎo)致了程序的輸出不是我們預(yù)期的結(jié)果,要阻止這種不合理的行為的一種方式是在one方法和two方法前面加上synchronized修飾符:
~~~
class?Test?{??
????static?int?i?=?0,?j?=?0;??
????static?synchronized?void?one()?{??
????????i++;??
????????j++;??
????}??
????static?synchronized?void?two()?{??
????????System.out.println("i="?+?i?+?"?j="?+?j);??
????}??
}??
~~~
根據(jù)前面的分析,我們可以知道,這時one方法和two方法再也不會并發(fā)的執(zhí)行了,i和j的值在主內(nèi)存中會一直保持一致,并且two方法輸出的也是一致的。另一種同步的機制是在共享變量之前加上volatile:
~~~
class?Test?{??
????static?volatile?int?i?=?0,?j?=?0;??
????static?void?one()?{??
????????i++;??
????????j++;??
????}??
????static?void?two()?{??
????????System.out.println("i="?+?i?+?"?j="?+?j);??
????}??
}??
~~~
one方法和two方法還會并發(fā)的去執(zhí)行,但是加上volatile可以將共享變量i和j的改變直接響應(yīng)到主內(nèi)存中,這樣保證了**主內(nèi)存中i和j的值一致性**,然而在執(zhí)行two方法時,在two方法獲取到i的值和獲取到j(luò)的值中間的這段時間,one方法也許被執(zhí)行了好多次,導(dǎo)致j的值會大于i的值。所以volatile可以保證內(nèi)存可見性,不能保證并發(fā)有序性。
沒有明白JLS中為什么使用兩個變量來闡述volatile的工作原理,這樣不是很好理解。volatile是一種弱的同步手段,相對于synchronized來說,某些情況下使用,可能效率更高,因為它不是阻塞的,尤其是讀操作時,加與不加貌似沒有影響,處理寫操作的時候,可能消耗的性能更多些。但是volatile和synchronized性能的比較,我也說不太準,多線程本身就是比較玄的東西,依賴于CPU時間分片的調(diào)度,JVM更玄,還沒有研究過虛擬機,從頂層往底層看往往是比較難看透的。在JDK5.0之前,如果沒有參透volatile的使用場景,還是不要使用了,盡量用synchronized來處理同步問題,線程阻塞這玩意簡單粗暴。另外**volatile和final不能同時修飾一個字段**,可以想想為什么。
- 前言
- Java線程(一):線程安全與不安全
- Java線程(二):線程同步synchronized和volatile
- Java線程(三):線程協(xié)作-生產(chǎn)者/消費者問題
- Java線程(四):線程中斷、線程讓步、線程睡眠、線程合并
- Java線程(五):Timer和TimerTask
- Java線程(六):線程池
- Java線程(七):Callable和Future
- Java線程(八):鎖對象Lock-同步問題更完美的處理方式
- Java線程(九):Condition-線程通信更高效的方式
- Java線程(十):CAS
- Java線程(十一):Fork/Join-Java并行計算框架
- Java線程(篇外篇):阻塞隊列BlockingQueue
- Java線程(篇外篇):線程本地變量ThreadLocal
- Java線程(篇外篇):線程和鎖
