要說明線程同步問題首先要說明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-同步問題更完美的處理方式。java
下面一樣用代碼來展現一下線程同步問題。多線程
TraditionalThreadSynchronized.java:建立兩個線程,執行同一個對象的輸出方法。併發
1 public class TraditionalThreadSynchronized { 2 public static void main(String[] args) { 3 final Outputter output = new Outputter(); 4 new Thread() { 5 public void run() { 6 output.output("zhangsan"); 7 }; 8 }.start(); 9 new Thread() { 10 public void run() { 11 output.output("lisi"); 12 }; 13 }.start(); 14 } 15 } 16 class Outputter { 17 public void output(String name) { 18 // TODO 爲了保證對name的輸出不是一個原子操做,這裏逐個輸出name的每一個字符 19 for(int i = 0; i < name.length(); i++) { 20 System.out.print(name.charAt(i)); 21 // Thread.sleep(10); 22 } 23 } 24 }
運行結果: zhlainsigsan this
顯然輸出的字符串被打亂了,咱們指望的輸出結果是zhangsanlisi,這就是線程同步問題,咱們但願output方法被一個線程完整的執行完以後再切換到下一個線程,Java中使用synchronized保證一段代碼在多線程執行時是互斥的,有兩種用法:.net
1. 使用synchronized將須要互斥的代碼包含起來,並上一把鎖。線程
這把鎖必須是須要互斥的多個線程間的共享對象,像下面的代碼是沒有意義的。對象
1 { 2 Object lock = new Object(); 3 synchronized (lock) { 4 for(int i = 0; i < name.length(); i++) { 5 System.out.print(name.charAt(i)); 6 } 7 } 8 }
每次進入output方法都會建立一個新的lock,這個鎖顯然每一個線程都會建立,沒有意義。blog
2. 將synchronized加在須要互斥的方法上。隊列
1 public synchronized void output(String name) { 2 // TODO 線程輸出方法 3 for(int i = 0; i < name.length(); i++) { 4 System.out.print(name.charAt(i)); 5 } 6 }
這種方式就至關於用this鎖住整個方法內的代碼塊,若是用synchronized加在靜態方法上,就至關於用××××.class鎖住整個方法內的代碼塊。使用synchronized在某些狀況下會形成死鎖,死鎖問題之後會說明。使用synchronized修飾的方法或者代碼塊能夠當作是一個原子操做。內存
每一個鎖對(JLS中叫monitor)都有兩個隊列,一個是就緒隊列,一個是阻塞隊列,就緒隊列存儲了將要得到鎖的線程,阻塞隊列存儲了被阻塞的線程,當一個線程被喚醒(notify)後,纔會進入到就緒隊列,等待CPU的調度,反之,當一個線程被wait後,就會進入阻塞隊列,等待下一次被喚醒,這個涉及到線程間的通訊,下一篇博文會說明。看咱們的例子,當第一個線程執行輸出方法時,得到同步鎖,執行輸出方法,剛好此時第二個線程也要執行輸出方法,但發現同步鎖沒有被釋放,第二個線程就會進入就緒隊列,等待鎖被釋放。一個線程執行互斥代碼過程以下:
1. 得到同步鎖;
2. 清空工做內存;
3. 從主內存拷貝對象副本到工做內存;
4. 執行代碼(計算或者輸出等);
5. 刷新主內存數據;
6. 釋放同步鎖。
因此,synchronized既保證了多線程的併發有序性,又保證了多線程的內存可見性。
volatile是第二種Java多線程同步的機制,一個變量能夠被volatile修飾,在這種狀況下內存模型(主內存和線程工做內存)確保全部線程能夠看到一致的變量值,來看一段代碼:
1 class Test { 2 static int i = 0, j = 0; 3 static void one() { 4 i++; 5 j++; 6 } 7 static void two() { 8 System.out.println("i=" + i + " j=" + j); 9 } 10 }
一些線程執行one方法,另外一些線程執行two方法,two方法有可能打印出j比i大的值,按照以前分析的線程執行過程分析一下:
1. 將變量i從主內存拷貝到工做內存;
2. 改變i的值;
3. 刷新主內存數據;
4. 將變量j從主內存拷貝到工做內存;
5. 改變j的值;
6. 刷新主內存數據;
這個時候執行two方法的線程先讀取了主存i原來的值又讀取了j改變後的值,這就致使了程序的輸出不是咱們預期的結果,要阻止這種不合理的行爲的一種方式是在one方法和two方法前面加上synchronized修飾符:
1 class Test { 2 static int i = 0, j = 0; 3 static synchronized void one() { 4 i++; 5 j++; 6 } 7 static synchronized void two() { 8 System.out.println("i=" + i + " j=" + j); 9 } 10 }
根據前面的分析,咱們能夠知道,這時one方法和two方法不再會併發的執行了,i和j的值在主內存中會一直保持一致,而且two方法輸出的也是一致的。另外一種同步的機制是在共享變量以前加上volatile:
1 class Test { 2 static volatile int i = 0, j = 0; 3 static void one() { 4 i++; 5 j++; 6 } 7 static void two() { 8 System.out.println("i=" + i + " j=" + j); 9 } 10 }
one方法和two方法還會併發的去執行,可是加上volatile能夠將共享變量i和j的改變直接響應到主內存中,這樣保證了主內存中i和j的值一致性,然而在執行two方法時,在two方法獲取到i的值和獲取到j的值中間的這段時間,one方法也許被執行了好屢次,致使j的值會大於i的值。因此volatile能夠保證內存可見性,不能保證併發有序性。
儘可能用synchronized來處理同步問題,線程阻塞這玩意簡單粗暴。另外volatile和final不能同時修飾一個字段,能夠想一想爲何。
本文來自:高爽|Coder,原文地址:http://blog.csdn.net/ghsau/article/details/7424694,轉載請註明。