上一篇:Java線程(一)
上篇通過一個簡單的例子說明了安全執行緒與不安全,在例子中不安全的情況下輸出的結果恰好是逐個遞增的,為什麼會產生這樣的結果呢,因為建立的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線程有序執行,先取款後匯款或者先匯款後取款,此為有序性。
下面同樣用代碼來展示一下線程同步問題。
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));}}}
運行結果:
zhlainsigsan
顯然輸出的字串被打亂了,我們期望的輸出結果是zhangsanlisi,這就是線程同步問題,我們希望output方法被一個線程完整的執行完之後在切換到下一個線程,Java中使用synchronized保證一段代碼在多線程執行時是互斥的,有兩種用法:
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)); }}
每次進入output方法都會建立一個新的lock,這個鎖顯然每個線程都會建立,沒有意義。
2. 將synchronized加在需要互斥的方法上。
public synchronized void output(String name) { // TODO 線程輸出方法 for(int i = 0; i < name.length(); i++) { System.out.print(name.charAt(i)); }}
這種方式就相當於用this鎖住整個方法內的代碼塊,如果用synchronized加在靜態方法上,就相當於用××××.class鎖住整個方法內的代碼塊。使用synchronized在某些情況下會造成死結,死結問題以後會說明。
每個鎖對象都有兩個隊列,一個是就緒隊列,一個是阻塞隊列,就緒佇列儲存體了將要獲得鎖的線程,阻塞佇列儲存體了被阻塞的線程,當一個線程被喚醒(notify)後,才會進入到就緒隊列,等待CPU的調度,反之,當一個線程被wait後,就會進入阻塞隊列,等待下一次被喚醒,這個涉及到線程間的通訊,下一篇博文會說明。看我們的例子,當第一個線程執行輸出方法時,獲得同步鎖,執行輸出方法,恰好此時第二個線程也要執行輸出方法,但發現同步鎖沒有被釋放,第二個線程就會進入就緒隊列,等待鎖被釋放。一個線程執行互斥代碼過程如下:
1. 獲得同步鎖;
2. 清空工作記憶體;
3. 從主記憶體拷貝對象副本到工作記憶體;
4. 執行代碼(計算或者輸出等);
5. 重新整理主記憶體資料;
6. 釋放同步鎖。
所以,synchronized既保證了多線程的並發有序性,又保證了多線程的記憶體可見度。
volatile是第二種Java多線程同步的手段,根據JLS的說法,一個變數可以被volatile修飾,在這種情況下記憶體模型確保所有線程可以看到一致的變數值,來看一段代碼:
class Test {static int i = 0, j = 0;static void one() {i++;j++;}static void two() {System.out.println("i=" + i + " j=" + j);}}
一些線程執行one方法,另一些線程執行two方法,two方法有可能列印出j比i大的值,按照之前分析的線程執行過程分析一下:
1. 將變數i從主記憶體拷貝到工作記憶體;
2. 改變i的值;
3. 重新整理主記憶體資料;
4. 將變數j從主記憶體拷貝到工作記憶體;
5. 改變j的值;
6. 重新整理主記憶體資料;
這個時候執行two方法的線程先讀取了主存i原來的值又讀取了j改變後的值,這就導致了程式的輸出不是我們預期的結果,那麼可以在共用變數之前加上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);}}
加上volatile可以將共用變數i和j的改變直接響應到主記憶體中,這樣保證了i和j的值可以保持一致,然而我們不能保證執行two方法的線程是在i和j執行到什麼程度擷取到的,所以volatile可以保證記憶體可見度,不能保證並發有序性。
下一篇:Java線程(三)
本文來自:高爽|Coder,原文地址:http://blog.csdn.net/ghsau/article/details/7424694。