這一章主要講述線程之間數據的共享,數據共享最大的難點就是資源競爭
3.1 Synchronized關鍵字的使用 (The Synchronized Keyword)
書中例子太繁瑣了,我找了一個簡單的例子
package com.yellow.chapteThree; public class Test implements Runnable { public void run() { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + " synchronized loop " + i); } } public static void main(String[] args) { Test t = new Test(); Thread ta = new Thread(t, "A"); Thread tb = new Thread(t, "B"); ta.start(); tb.start(); } }
我們讓兩個線程從1打印到5,結果我們會發現,在A打印的過程中B也在打印,兩個線程都進入了這個方法,那怎麼辦呢~
第一個辦法,使用synchronized代碼塊
package com.yellow.chapteOne; public class Test implements Runnable { public void run() { synchronized (this) { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + " synchronized loop " + i); } } } public static void main(String[] args) { Test t = new Test(); Thread ta = new Thread(t, "A"); Thread tb = new Thread(t, "B"); ta.start(); tb.start(); } }
synchronized是由監聽器(monitors)實現的,每個對象都有一個monitors,所以synchronized()的括號裏面可以填任意對象,當一個線程試圖進入一個synchronized代碼塊的時候,必須得到這個代碼塊的monitors,一旦有一個線程得到了這個代碼塊的monitors後,其他所有在同一monitors下的線程都必須等待,獲得monitors的線程執行完代碼後會自動釋放monitors的所有權,好讓其他線程進入
我們讓synchronized使用t對象的monitors,很明顯,A,B兩個線程處於同一個monitor下面,當A搶到資源執行run方法,B只能等待
如果我們稍作變化如下:
package com.yellow.chapteOne; public class Test implements Runnable { public void run() { synchronized (this) { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + " synchronized loop " + i); } } } public static void main(String[] args) { Test t1 = new Test(); Test t2 = new Test(); Thread ta = new Thread(t1, "A"); Thread tb = new Thread(t2, "B"); ta.start(); tb.start(); } }
我們會發現結果並沒有還是會交替打印,沒有起到同步的作用,因爲,A,B兩個線程處於不同對象的monitor,可以同時進入synchronized代碼塊
第二種方式就是使用synchronized方法,如下
package com.yellow.chapteOne; public class Test implements Runnable { public synchronized void run() { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + " synchronized loop " + i); } } public static void main(String[] args) { Test t = new Test(); Thread ta = new Thread(t, "A"); Thread tb = new Thread(t, "B"); ta.start(); tb.start(); } }
非static的synchronized方法用的是this的monitor,static的synchronized方法用類的class對象的monitor
3.2 Volatile 關鍵字(The Volatile Keyword)
把一個變量聲明成volatile意味着這個變量的值將不會換成在線程的本地空間,而是直接操作main memory
直接上個例子來解釋:
public class VolatileObjectTest { /** * 相信絕大多數使用JAVA的人都沒試出volatile變量的區別。獻給那些一直想知道volatile是如何工作的而又試驗不出區別的人。 * 成員變量boolValue使用volatile和不使用volatile會有明顯區別的。 本程序需要多試幾次,就能知道兩者之間的區別的。 * * @param args */ public static void main(String[] args) { final VolatileObjectTest volObj = new VolatileObjectTest(); Thread t2 = new Thread() { public void run() { System.out.println("t1 start"); for (;;) { volObj.waitToExit(); } } }; t2.start(); Thread t1 = new Thread() { public void run() { System.out.println("t2 start"); for (;;) { volObj.swap(); } } }; t1.start(); } boolean boolValue;// 加上volatile 修飾的是時候,程序會很快退出,因爲volatile // 保證各個線程工作內存的變量值和主存一致。所以boolValue == !boolValue就成爲了可能。 public void waitToExit() { if (boolValue == !boolValue) System.exit(0);// 非原子操作,理論上應該很快會被打斷。實際不是,因爲此時的boolValue在線程自己內部的工作內存的拷貝,因爲它不會強制和主存區域同步,線程2修改了boolValue很少有機會傳遞到線程一的工作內存中。所以照成了假的“原子現象”。 } public void swap() {// 不斷反覆修改boolValue,以期打斷線程1. try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } boolValue = !boolValue; System.out.println(boolValue); } }
我們用volatile用的最廣泛的地方是 作爲停止請求的標誌( "stop request" flag ),好吧,我也知道翻譯的不像人話,看代碼
public class StoppableTask extends Thread { private volatile boolean pleaseStop; public void run() { while (!pleaseStop) { // do some stuff... } } public void tellMeToStop() { pleaseStop = true; } }
如果pleaseStop變量沒有被聲明爲
volatile
的話,別的線程改變了pleaseStop的值,而這個線程不知道(因爲它不會從主存裏面同步,會讀自己本地空間),就會一直循環下去
volatile和synchronized的區別:
1)volatile可以修飾primitive變量,synchronized不能
2)線程進入synchronized的時候會獲得鎖,但是volatile不會獲得鎖
3)因爲進入volatile沒有獲得鎖,所以不要試圖用volatile實現原子操作
4)volatile可以修飾null
然後我查資料的時候發現了一個有意思的東西,可以用 volatile和雙重檢查加鎖可以實現多線程單例模式
1
/** * 雙重檢查加鎖 單例模式,據說JDK1.5之後可以用 volatile 和 雙重檢查加速來實現單例模式 * @author yellowbaby * */ public class SingletonOne { /** * volatile 的作用是讓變量不再緩存在當前線程的本地空間,而是直接去操作main memory * 我的問題是,這裏爲什麼要使用 volatile? */ private volatile static SingletonOne singleton = null; public SingletonOne() { } public static SingletonOne getInstance() { if (null == singleton) {// 1 檢查實例,如果不存在就進入同步區塊 synchronized (SingletonOne.class) {// 2 注意,只有第一次才徹底執行這裏的代碼 if (null != singleton) {// 3 singleton = new SingletonOne(); } } } return singleton; } }
假如不用 volatile會發生什麼呢?
假設同時有兩個線程(A和B)進入了1的if塊,A進入2,B等待,A出了Syn塊,B進入,B判斷3,這個時候它會直接從自己的線程內存中讀取singleton的值,發現爲空然後就會又new一個出來
然後分享另一種多線程單例模式~
/** * 使用內部靜態類來得到實例,因爲只有在調用InnerSingleFactory.SINGLETON的時候纔會加載SingletonTwo,所以也是懶漢型 * @author yellowbaby * */ public class SingletonTwo { private volatile static SingletonTwo singleton = null; private SingletonTwo() { } public static SingletonTwo getInstance() { if(singleton == null){ singleton = InnerSingleFactory.SINGLETON; } return singleton; } private final static class InnerSingleFactory { final static SingletonTwo SINGLETON = new SingletonTwo(); } }
用static來解決同步是個好辦法,但是常規的使用static的寫法是餓漢型的,但是如果丟在內部類裏面就可以解決這個麻煩的問題了
3.3 資源競爭(More on Race Conditions)
什麼是資源競爭?
race condition 發送在 兩個線程共享同一個數據,並且試圖同時修改它,因爲線程調度算法讓線程執行的先後不是固定的,數據最終改變的結果取決於線程的執行順序
問題往外出現在 “check-then-act”的操作中
舉個例子
if (x == 5) // The "Check"檢查 1 { y = x * 2; // The "Act" 操作 2 // 如果另一個線程在 1 和 2 直接修改了 x 的值,那結果就不會是 10 了 }
爲了解決這個問題,我們需要在合適的地方加上鎖來確保只有一個線程修改這個數據
// Obtain lock for x if (x == 5) { y = x * 2; // Now, nothing can change x until the lock is released. // Therefore y = 10 } // release lock for x
一般都是使用synchronized代碼塊或者synchronized方法來同步,前面我面提到過的~
3.4 顯示鎖(Explicit Locking)
上面說了synchronized的一些用法,synchronized是好,但是並不是完美的,比如你不能中斷一個正在等待的線程,有可能一個線程得不到鎖就一直傻傻的等下去,JDK 5 之後出現了一個新東西既可以實現synchronized的作用也可以實現這些它做不到的東西,這就是 顯示鎖
看例子
Lock lock = new ReentrantLock(); lock.lock(); try { // 等價於加上了synchronized代碼塊 } finally { lock.unlock(); }
顯示鎖在性能上面比synchronized強,而且可以實現一些synchronized不能實現的,但是也有缺點
1)必須要手動釋放鎖,使用synchronized會自動的釋放鎖,一旦忘記就能難查出問題出現在什麼地方
2)還有,顯示鎖不兼容 JDK 1.5 以前的版本
那什麼時候用顯示鎖呢,答案就是,在你真正需要某些synchronized無法實現的功能的時候,大部分時候synchronized是可以足夠的
3.8 死鎖(Deadlock)
死鎖就是兩個線程都在等待對方釋放釋放資源,導致兩個線程一直堵塞
看一個例子
public class MyDeadLock{ public static void main(String[] args) { final Robber robber = new Robber(); final Victim victim = new Victim(); robber.setVictim(victim); victim.setRobber(robber); new Thread(new Runnable() { @Override public void run() { robber.rob(); } }).start(); new Thread(new Runnable() { @Override public void run() { // TODO Auto-generated method stub victim.beRobbed(); } }).start(); } } class Robber { Victim victim; public Robber() { } synchronized void rob(){ System.out.println("我是劫匪"); victim.giveYouMoney();//給錢 letYouGo();//讓你走 } synchronized void letYouGo(){ System.out.println("放人"); } public void setVictim(Victim victim) { this.victim = victim; } } class Victim { Robber robber; public Victim() { } synchronized void beRobbed(){ System.out.println("我是被搶劫的人"); robber.letYouGo();//讓我走 giveYouMoney();//給錢 } synchronized void giveYouMoney(){ System.out.println("給你錢"); } public void setRobber(Robber robber) { this.robber = robber; } }
劫匪和受害者一個想要對方先給錢,一個想讓對方先放人,一直在等待,然後就死鎖了
當第一個robber對象進入rob方法時,得到了robber的對象鎖,試圖調用victim的方法,因爲giveYouMoney是synchronized的,所以需要等待得到victim的對象鎖,但是victim的對象鎖被在調用beRobbed的時候被victim獲得了,而也想得到robber的對象鎖,兩邊互不相讓,然後就死鎖了~