上篇通過一個簡單的例子說明了線程安全與不安全,在例子中不安全的情況下輸出的結果恰好是逐個遞增的(其實是巧合,多運行幾次,會產生不同的輸出結果),爲什麼會產生這樣的結果呢,因爲建立的Count對象是線程共享的,一個線程改變了其成員變量num值,下一個線程正巧讀到了修改後的num,所以會遞增輸出。
要說明線程同步問題首先要說明Java線程的兩個特性,可見性和有序性。多個線程之間是不能直接傳遞數據交互的,它們之間的交互只能通過共享變量來實現。拿上篇博文中的例子來說明,在多個線程之間共享了Count類的一個對象,這個對象是被創建在主內存(堆內存)中,每個線程都有自己的工作內存(線程棧),工作內存存儲了主內存Count對象的一個副本,當線程操作Count對象時,首先從主內存複製Count對象到工作內存中,然後執行代碼count.count(),改變了num值,最後用工作內存Count刷新主內存Count。當一個對象在多個內存中都存在副本時,如果一個內存修改了共享變量,其它線程也應該能夠看到被修改後的值,此爲可見性。多個線程執行時,CPU對線程的調度是隨機的,我們不知道當前程序被執行到哪步就切換到了下一個線程,一個最經典的例子就是銀行匯款問題,一個銀行賬戶存款100,這時一個人從該賬戶取10元,同時另一個人向該賬戶匯10元,那麼餘額應該還是100。那麼此時可能發生這種情況,A線程負責取款,B線程負責匯款,A從主內存讀到100,B從主內存讀到100,A執行減10操作,並將數據刷新到主內存,這時主內存數據100-10=90,而B內存執行加10操作,並將數據刷新到主內存,最後主內存數據100+10=110,顯然這是一個嚴重的問題,我們要保證A線程和B線程有序執行,先取款後匯款或者先匯款後取款,此爲有序性。本文講述了JDK5.0之前傳統線程的同步方式,更高級的同步方式可參見Java線程(八):鎖對象Lock-同步問題更完美的處理方式。
下面同樣用代碼來展示一下線程同步問題。
TraditionalThreadSynchronized.java:創建兩個線程,執行同一個對象的輸出方法。
- 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 < name.length(); i++) {
- System.out.print(name.charAt(i));
- // Thread.sleep(10);
- }
- }
- }
- zhlainsigsan
1. 使用synchronized將需要互斥的代碼包含起來,並上一把鎖。
- {
- synchronized (this) {
- for(int i = 0; i < name.length(); i++) {
- System.out.print(name.charAt(i));
- }
- }
- }
- {
- Object lock = new Object();
- synchronized (lock) {
- for(int i = 0; i < name.length(); i++) {
- System.out.print(name.charAt(i));
- }
- }
- }
2. 將synchronized加在需要互斥的方法上。
- public synchronized void output(String name) {
- // TODO 線程輸出方法
- for(int i = 0; i < name.length(); i++) {
- System.out.print(name.charAt(i));
- }
- }
每個鎖對象(JLS中叫monitor)都有兩個隊列,一個是就緒隊列,一個是阻塞隊列,就緒隊列存儲了將要獲得鎖的線程,阻塞隊列存儲了被阻塞的線程,當一個線程被喚醒(notify)後,纔會進入到就緒隊列,等待CPU的調度,反之,當一個線程被wait後,就會進入阻塞隊列,等待下一次被喚醒,這個涉及到線程間的通信,下一篇博文會說明。看我們的例子,當第一個線程執行輸出方法時,獲得同步鎖,執行輸出方法,恰好此時第二個線程也要執行輸出方法,但發現同步鎖沒有被釋放,第二個線程就會進入就緒隊列,等待鎖被釋放。一個線程執行互斥代碼過程如下:
1. 獲得同步鎖;
2. 清空工作內存;
3. 從主內存拷貝對象副本到工作內存;
4. 執行代碼(計算或者輸出等);
5. 刷新主內存數據;
6. 釋放同步鎖。
所以,synchronized既保證了多線程的併發有序性,又保證了多線程的內存可見性。
volatile是第二種Java多線程同步的機制,根據JLS(Java LanguageSpecifications)的說法,一個變量可以被volatile修飾,在這種情況下內存模型(主內存和線程工作內存)確保所有線程可以看到一致的變量值,來看一段代碼:
- class Test {
- static int i = 0, j = 0;
- static void one() {
- i++;
- j++;
- }
- static void two() {
- System.out.println("i=" + i + " j=" + j);
- }
- }
1. 將變量i從主內存拷貝到工作內存;
2. 改變i的值;
3. 刷新主內存數據;
4. 將變量j從主內存拷貝到工作內存;
5. 改變j的值;6. 刷新主內存數據;
這個時候執行two方法的線程先讀取了主存i原來的值又讀取了j改變後的值,這就導致了程序的輸出不是我們預期的結果,要阻止這種不合理的行爲的一種方式是在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);
- }
- }
- 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方法還會併發的去執行,但是加上volatile可以將共享變量i和j的改變直接響應到主內存中,這樣保證了主內存中i和j的值一致性,然而在執行two方法時,在two方法獲取到i的值和獲取到j的值中間的這段時間,one方法也許被執行了好多次,導致j的值會大於i的值。所以volatile可以保證內存可見性,不能保證併發有序性。
沒有明白JLS中爲什麼使用兩個變量來闡述volatile的工作原理,這樣不是很好理解。volatile是一種弱的同步手段,相對於synchronized來說,某些情況下使用,可能效率更高,因爲它不是阻塞的,尤其是讀操作時,加與不加貌似沒有影響,處理寫操作的時候,可能消耗的性能更多些。但是volatile和synchronized性能的比較,我也說不太準,多線程本身就是比較玄的東西,依賴於CPU時間分片的調度,JVM更玄,還沒有研究過虛擬機,從頂層往底層看往往是比較難看透的。在JDK5.0之前,如果沒有參透volatile的使用場景,還是不要使用了,儘量用synchronized來處理同步問題,線程阻塞這玩意簡單粗暴。另外volatile和final不能同時修飾一個字段,可以想想爲什麼。
本文來自:高爽|Coder,原文地址:http://blog.csdn.net/ghsau/article/details/7424694,轉載請註明。