(三)線程式控制制–java 多線程編程的那點小事

來源:互聯網
上載者:User

   線程的控制,所白了就是控制線程間的一些時序問題,下面開始講可以控制線程時序的幾個方法:

一、睡眠函數 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值進行了修改,這樣寫回到主記憶體後當然結果是不會遞增的。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.