上篇經過一個簡單的例子說明了線程安全與不安全,在例子中不安全的狀況下輸出的結果剛好是逐個遞增的,爲何會產生這樣的結果呢,由於創建的Count對象是線程共享的,一個線程改變了其成員變量num值,下一個線程正巧讀到了修改後的num,因此會遞增輸出。java
要說明線程同步問題首先要說明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線程有序執行,先取款後匯款或者先匯款後取款,此爲有序性。
安全
看代碼,TranditionalThead.java建立兩個線程去訪問同一個方法多線程
package org.thread; public class TranditionalThead { public static void main(String[] args) { final Outputer outputer = new Outputer(); Runnable runnable1 = new Runnable(){ public void run(){ outputer.input("HeinrichChen"); }; }; Runnable runnable2 = new Runnable(){ public void run(){ outputer.input("Heinrich.Shen"); } }; new Thread(runnable1).start(); new Thread(runnable2).start(); } }
package org.thread; public class Outputer { public void input(String name){ for(int i =0;i<name.length();i++){ System.out.print(name.charAt(i)); } } }
運行的結果:併發
HHeinrich.eSheninrichChen 我想要的結果是 HeinrichChenHeinrich.Shen
這個時候他們會出現咱們不想要出現的事情 這就是線程同步問題,咱們但願output方法被一個線程完整的執行完以後在切換到下一個線程, Java中使用synchronized保證一段代碼在多線程執行時是互斥的,有兩種用法:ide
第一種this
synchronized (this) { for(int i =0;i<name.length();i++){ System.out.print(name.charAt(i)); } }
鎖住咱們須要操做的代碼塊spa
使用synchronized將須要互斥的代碼包含起來,並上一把鎖。
線程
運行的結果是code
HeinrichChenHeinrich.Shen
這就是我想要的結果對象
第二種
將synchronized加在須要互斥的方法上。
也便是
HeinrichChenHeinrich.Shen
這種方式就至關於用this鎖住整個方法內的代碼塊,若是用synchronized加在靜態方法上,就至關於用××××.class鎖住整個方法內的代碼塊。使用synchronized在某些狀況下會形成死鎖,死鎖問題之後會說明。
每一個鎖對象都有兩個隊列,一個是就緒隊列,一個是阻塞隊列,就緒隊列存儲了將要得到鎖的線程,阻塞隊列存儲了被阻塞的線程,當一個線程被喚醒(notify)後,纔會進入到就緒隊列,等待CPU的調度,反之,當一個線程被wait後,就會進入阻塞隊列,等待下一次被喚醒,這個涉及到線程間的通訊,下一篇博文會說明。看咱們的例子,當第一個線程執行輸出方法時,得到同步鎖,執行輸出方法,剛好此時第二個線程也要執行輸出方法,但發現同步鎖沒有被釋放,第二個線程就會進入就緒隊列,等待鎖被釋放。一個線程執行互斥代碼過程以下:
1. 得到同步鎖;
2. 清空工做內存;
3. 從主內存拷貝對象副本到工做內存;
4. 執行代碼(計算或者輸出等);
5. 刷新主內存數據;
6. 釋放同步鎖。
因此,synchronized既保證了多線程的併發有序性,又保證了多線程的內存可見性。
volatile是第二種Java多線程同步的手段,根據JLS的說法,一個變量能夠被volatile修飾,
在這種狀況下內存模型確保全部線程能夠看到一致的變量值,來看一段代碼:
package org.thread; public class CountNum { public static int i = 0, j = 0; public static void count() { for (int m = 0; m < 10000; m++) { j++; i++; } } public static void show() { if (i < j) { System.out.println("i==>" + i + "===" + Thread.currentThread().getName()); System.out.println("j==>" + j + "===" + Thread.currentThread().getName()); } } }
一些線程執行one方法,另外一些線程執行count方法,show方法有可能打印出j比i大的值,按照以前分析的線程執行過程分析一下:
1. 將變量i從主內存拷貝到工做內存;
2. 改變i的值;
3. 刷新主內存數據;
4. 將變量j從主內存拷貝到工做內存;
5. 改變j的值;
6. 刷新主內存數據;
這個時候執行show方法的線程先讀取了主存i原來的值又讀取了j改變後的值,這就致使了程序的輸出不是咱們預期的結果,那麼能夠在共享變量以前加上volatile。
package org.thread; public class CountNum { public volatile static int i = 0, j = 0; public static void count() { for (int m = 0; m < 10000; m++) { j++; i++; } } public static void show() { if (i < j) { System.out.println("i==>" + i + "===" + Thread.currentThread().getName()); System.out.println("j==>" + j + "===" + Thread.currentThread().getName()); } } }
package org.thread; public class CountTest { public static void main(String[] args) { Runnable runnable1 = new Runnable() { @Override public void run() { new CountNum().count(); } }; Runnable runnable2 = new Runnable() { @Override public void run() { new CountNum().show(); } }; for(int i=0;i<1000;i++){ new Thread(runnable1).start(); new Thread(runnable2).start(); } } }
i==>4938===Thread-3 j==>7160===Thread-3 i==>7951===Thread-7 j==>8328===Thread-7 i==>8814===Thread-11 j==>9169===Thread-11 i==>18925===Thread-15
加上volatile能夠將共享變量i和j的改變直接響應到主內存中,這樣保證了i和j的值能夠保持一致,然而咱們不能保證執行two方法的線程是在i和j執行到什麼程度獲取到的,因此volatile能夠保證內存可見性,不能保證併發有序性。