線程的控制,所白了就是控制線程間的一些時序問題,下面開始講可以控制線程時序的幾個方法:
一、睡眠函數 sleep
(1)Thread.sleep();
(2)TimeUnit.XXX.sleep();
睡眠函數在上一節結束的時候已經講過了,這裡就balabala了
二、等待函數 Join
join 函數在邏輯上理解起來可能有些繞,其實都怪這個名字起的太爛。join 方法的api原文解釋是:Waits for this thread to die;這樣一來,就很好理解了。
就是說誰調用了這個方法,那麼當前線程就要等到它死才能獲得cpu
舉個例子
class A extends Thread{
...
b.join(); //b 是B類的一個執行個體
do jobA;
}
class B extends Thread{
do jobB;
}
a.start(); b.start();
看這個例子,線上程A中,有一條語句:b.join(); 這句話就是告訴當前線程(線程a,因為線程a執行到了這句話)要等到a死了才能做工作jobA
這樣,運行起來的效果就是 jobB 先完成後,jobA 才會執行。
另外要注意的是:線程A 要等待線程B ,那麼線程A中必須要維護一個B線程的引用,這樣才能在合適的時機等待B執行完。(從商務邏輯的角度看,線程A之所以要等待B很可能是要使用線程B的運算結果,A和B有關連,且是A要等B,就是說只有A知道要在什麼時候哪個地方等B,所以A中必須要有一個B線程的引用)
好了,join 方法就差不多了,下面貼上一個小demo,鞏固下:
import java.util.concurrent.TimeUnit;public class JoinMethodTest {class Thread1 extends Thread{Thread2 t2;public void run(){try {System.out.println("Thread1 will wait for t2 awake!");t2.join();} catch (InterruptedException e1) {// TODO Auto-generated catch blocke1.printStackTrace();}for( int i = 0; i < 5; i++){try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {}System.out.println("Thread1111 said : "+i);}}public Thread1(Thread2 t2){this.t2 = t2;} }class Thread2 extends Thread{public void run(){try {System.out.println("Thread2 sleeped!");TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e1) {e1.printStackTrace();}System.out.println("Thread2 awaked!");}}public Thread1 getThread1(Thread2 t2){return new Thread1(t2);}public Thread2 getThread2(){return new Thread2();}public static void main(String args[]) throws InterruptedException{JoinMethodTest jmt = new JoinMethodTest();Thread2 t2 = jmt.getThread2();Thread1 t1 = jmt.getThread1(t2);t2.start();t1.start();}}
結果:
Thread2 sleeped!Thread1 will wait for t2 awake!Thread2 awaked!Thread1111 said : 0Thread1111 said : 1Thread1111 said : 2Thread1111 said : 3Thread1111 said : 4
三、使用synchronized給臨界區代碼隱式的加鎖(這裡之所以成為隱式的加鎖是因為你只要給方法或變數加了synchronized標誌,那麼線程並發時的同步互斥控制都有系統來幫你完成了)
java 對防止資源衝突提供了內在支援,既synchronized。
學習synchronized 要注意一下幾點:
1、如果某個線程調用了某個synchronized標誌的方法,那麼在這個線程從該方法返回前,其他所有要調用synchronized方法的線程都會阻塞,注意千萬不要想成是其他線程暫時不執行該方法,先執行其他方法。
2、每個對象有且僅有一把鎖,如果一個對象同時有多個synchronized方法,那麼當其中一個方法獲得鎖時,其他方法都不能被調用,或者說調用其他方法的線程都會阻塞。
樣本:
public class Worker {public synchronized void printEvenNumber(String tag){for(int i = 0 ; i < 100;i +=2){System.out.println(tag+" is printing even number : "+i);}}public synchronized void printOddNumber(String tag){for(int i = 1 ; i < 100 ; i++){if( i % 2 != 0){System.out.println(tag+" is printing odd Number : "+i);}}}}
public class Thread1 extends Thread{private Worker w;private String curThreadName;public void run(){w.printEvenNumber(curThreadName);//w.printOddNumber(curThreadName);try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}}public Thread1(Worker w,String name){this.w = w;this.curThreadName = name;}}
import java.util.concurrent.TimeUnit;public class Thread2 extends Thread{private Worker w;private String curThreadName;public void run(){//w.printEvenNumber(curThreadName);w.printOddNumber(curThreadName);try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}}public Thread2(Worker w,String name){this.w = w;this.curThreadName = name;}}
太懶了,可以在Thread1 和Thread2 中注釋或添加 printEvenNumber() 和 printOddNumber方法來測試上面講的2個注意;
四、使用Lock 來顯式的給臨界資源加鎖
Lock 這個東西就像我們在作業系統學訊號量時的一個互斥訊號量。使用lock.lock () 來給臨界區加鎖。使用Lock.unlock()來給臨界區解鎖.
使用 lock 來進行臨界資源的控制和使用synchronized的區別是:
1、如果在調用synchronized標誌的方法時在方法內出現了一個異常,此時你是沒有辦法做任何清理工作的。但使用顯示的lock上鎖再配上try+finally 你就可以去處理出現的異常,從而將系統維護在正確的狀態了。
2、使用lock來上鎖,你還可以擷取更細粒度的控制力。
五、使用synchronized構成同步控制塊來保護臨界區
這種方法效果是和lock一樣的,也可以擷取耕細粒度的控制力,且代碼更加簡潔
格式:
synchronized(synObj)
{
....臨界區代碼
}
臨界區代碼是需要上鎖的,前面已經講過只有對象有且僅有一把鎖。所以臨界區要上鎖必須要從某個對象獲得,所以同步控制塊的構成還需要一個synObj對象。
eg:
synchronized(this) //借用對象的鎖來構成同步控制塊{ i++; i++;}
六、使用 volatile 關鍵字來保證事物的原子性
實際上,volatile並不總能保證事物的原子性,先看個例子:
public class Counter { public volatile static int count = 0; public static void inc() { //這裡延遲1毫秒,使得結果明顯 try { Thread.sleep(1); } catch (InterruptedException e) { } count++; } public static void main(String[] args) { //同時啟動1000個線程,去進行i++計算,看看實際結果 for (int i = 0; i < 1000; i++) { new Thread(new Runnable() { @Override public void run() { Counter.inc(); } }).start(); } //這裡每次啟動並執行值都有可能不同,可能為1000 System.out.println("運行結果:Counter.count=" + Counter.count); }}
運行結果:
運行結果:Counter.count=981
原因就是:對於volatile修飾的變數,jvm虛擬機器只是保證從主記憶體載入到線程工作記憶體的值是最新的。
解釋這句話就要分析線程運行時的記憶體問題了。下面的內容摘自網路:
每一個線程運行時都有一個線程棧,線程棧儲存了線程運行時候變數值資訊。當線程訪問某一個對象時候值的時候,首先通過對象的引用找到對應在堆記憶體的變數的值,然後把堆記憶體變數的具體值load到執行緒區域記憶體中,建立一個變數副本,之後線程就不再和對象在堆記憶體變數值有任何關係,而是直接修改副本變數的值,在修改完之後的某一個時刻(線程退出之前),自動把線程變數副本的值回寫到對象在堆中變數。這樣在堆中的對象的值就產生變化了。下面一幅圖描述這些互動
如果,當線程把count變數的值從主記憶體拷貝到自己的線程棧之後,線程對該變數的所有操作都是對這個臨時副本的操作,出現上面例子中運行結果:Counter.count=981 這個結果就是,有20個線程同時在自己的本地棧中對同一count值進行了修改,這樣寫回到主記憶體後當然結果是不會遞增的。