Java 多線程

1榜聂、概述

Java 給多線程編程提供了內(nèi)置的支持麻裁。 一條線程指的是進(jìn)程中一個(gè)單一順序的控制流断国,一個(gè)進(jìn)程中可以并發(fā)多個(gè)線程撕彤,每條線程并行執(zhí)行不同的任務(wù)朽砰。多線程是多任務(wù)的一種特別的形式,但多線程使用了更小的資源開銷喉刘。這里定義和線程相關(guān)的另一個(gè)術(shù)語 - 進(jìn)程:一個(gè)進(jìn)程包括由操作系統(tǒng)分配的內(nèi)存空間,包含一個(gè)或多個(gè)線程漆弄。一個(gè)線程不能獨(dú)立的存在睦裳,它必須是進(jìn)程的一部分。一個(gè)進(jìn)程一直運(yùn)行撼唾,直到所有的非守護(hù)線程都結(jié)束運(yùn)行后才能結(jié)束廉邑。多線程能滿足程序員編寫高效率的程序來達(dá)到充分利用 CPU 的目的。

2倒谷、線程狀態(tài)

線程狀態(tài)圖

一個(gè)線程在創(chuàng)建到最后消亡中間可能處于許多不同的狀態(tài)蛛蒙,一共大致可分為如下幾個(gè)狀態(tài):

① 新建狀態(tài)(New):新創(chuàng)建了一個(gè)線程對(duì)象。

② 就緒狀態(tài)(Runnable):線程對(duì)象創(chuàng)建后渤愁,其他線程調(diào)用了該對(duì)象的start()方法牵祟。該狀態(tài)的線程位于可運(yùn)行線程池中,變得可運(yùn)行抖格,等待獲取CPU的使用權(quán)诺苹。

③ 運(yùn)行狀態(tài)(Running):就緒狀態(tài)的線程獲取了CPU,執(zhí)行程序代碼雹拄。

④ 阻塞狀態(tài)(Blocked):阻塞狀態(tài)是線程因?yàn)槟撤N原因放棄CPU使用權(quán)收奔,暫時(shí)停止運(yùn)行。直到線程進(jìn)入就緒狀態(tài)滓玖,才有機(jī)會(huì)轉(zhuǎn)到運(yùn)行狀態(tài)坪哄。阻塞的情況分三種:

等待阻塞:運(yùn)行的線程執(zhí)行wait()方法,JVM會(huì)把該線程放入等待池中。(wait會(huì)釋放持有的鎖)

同步阻塞:運(yùn)行的線程在獲取對(duì)象的同步鎖時(shí)翩肌,若該同步鎖被別的線程占用模暗,則JVM會(huì)把該線程放入鎖池中。

其他阻塞:運(yùn)行的線程執(zhí)行sleep()或join()方法摧阅,或者發(fā)出了I/O請(qǐng)求時(shí)汰蓉,JVM會(huì)把該線程置為阻塞狀態(tài)。當(dāng)sleep()狀態(tài)超時(shí)棒卷、join()等待線程終止或者超時(shí)顾孽、或者I/O處理完畢時(shí),線程重新轉(zhuǎn)入就緒狀態(tài)比规。(注意sleep是不會(huì)釋放持有的鎖)

⑤ 死亡狀態(tài)(Dead):線程執(zhí)行完了或者因異常退出了run()方法若厚,該線程結(jié)束生命周期。

3蜒什、創(chuàng)建線程3種方式

3.1 通過繼承 Thread 類本身

創(chuàng)建一個(gè)新的類测秸,該類繼承 Thread 類,然后創(chuàng)建一個(gè)該類的實(shí)例灾常。繼承類必須重寫 run() 方法霎冯,該方法是新線程的入口點(diǎn)。它也必須調(diào)用 start() 方法才能執(zhí)行钞瀑。


class ThreadDemo extends Thread {

  private Thread t;

  private String threadName;

  ThreadDemo( String name) {

      threadName = name;

      System.out.println("Creating " +  threadName );

  }

  public void run() {

      System.out.println("Running " +  threadName );

      try {

        for(int i = 4; i > 0; i--) {

            System.out.println("Thread: " + threadName + ", " + i);

            // 讓線程睡眠一會(huì)

            Thread.sleep(50);

        }

      }catch (InterruptedException e) {

        System.out.println("Thread " +  threadName + " interrupted.");

      }

      System.out.println("Thread " +  threadName + " exiting.");

  }

  public void start () {

      System.out.println("Starting " +  threadName );

      if (t == null) {

        t = new Thread (this, threadName);

        t.start ();

      }

  }

}

public class TestThread {

  public static void main(String args[]) {

      ThreadDemo T1 = new ThreadDemo( "Thread-1");

      T1.start();

      ThreadDemo T2 = new ThreadDemo( "Thread-2");

      T2.start();

  } 

}

3.2 實(shí)現(xiàn) Runnable 接口

為了實(shí)現(xiàn) Runnable沈撞,一個(gè)類只需要執(zhí)行一個(gè)方法調(diào)用 run(), run() 可以調(diào)用其他方法雕什,使用其他類缠俺,并聲明變量,就像主線程一樣贷岸。在創(chuàng)建一個(gè)實(shí)現(xiàn) Runnable 接口的類之后壹士,你可以在類中實(shí)例化一個(gè)線程對(duì)象。Thread 定義了幾個(gè)構(gòu)造方法偿警,下面的這個(gè)是我們經(jīng)常使用的:


Thread(Runnable threadOb,String threadName);

這里躏救,threadOb 是一個(gè)實(shí)現(xiàn) Runnable 接口的類的實(shí)例,并且 threadName 指定新線程的名字螟蒸。新線程創(chuàng)建之后落剪,你調(diào)用它的 start() 方法它才會(huì)運(yùn)行,例子如下:


class RunnableDemo implements Runnable {

  private Thread t;

  private String threadName;

  RunnableDemo( String name) {

      threadName = name;

      System.out.println("Creating " +  threadName );

  }

  public void run() {

      System.out.println("Running " +  threadName );

      try {

        for(int i = 4; i > 0; i--) {

            System.out.println("Thread: " + threadName + ", " + i);

            // 讓線程睡眠一會(huì)

            Thread.sleep(50);

        }

      }catch (InterruptedException e) {

        System.out.println("Thread " +  threadName + " interrupted.");

      }

      System.out.println("Thread " +  threadName + " exiting.");

  }

  public void start () {

      System.out.println("Starting " +  threadName );

      if (t == null) {

        t = new Thread (this, threadName);

        t.start ();

      }

  }

}

public class TestThread {

  public static void main(String args[]) {

      RunnableDemo R1 = new RunnableDemo( "Thread-1");

      R1.start();

      RunnableDemo R2 = new RunnableDemo( "Thread-2");

      R2.start();

  } 

}

3.3 通過 Callable 和 Future 創(chuàng)建線程

創(chuàng)建 Callable 接口的實(shí)現(xiàn)類,并實(shí)現(xiàn) call() 方法尿庐,該 call() 方法將作為線程執(zhí)行體忠怖,并且有返回值。創(chuàng)建 Callable 實(shí)現(xiàn)類的實(shí)例抄瑟,使用 FutureTask 類來包裝 Callable 對(duì)象凡泣,該 FutureTask 對(duì)象封裝了該 Callable 對(duì)象的 call() 方法的返回值枉疼。使用 FutureTask 對(duì)象作為 Thread 對(duì)象的 target 創(chuàng)建并啟動(dòng)新線程。調(diào)用 FutureTask 對(duì)象的 get() 方法來獲得子線程執(zhí)行結(jié)束后的返回值鞋拟。若此時(shí)線程還沒運(yùn)行結(jié)束骂维,則調(diào)用get() 方法的線程會(huì)阻塞,知道得到返回值贺纲。


public class CallableThreadTest implements Callable<Integer>{ 

     public static void main(String[] args)

     { 

     CallableThreadTest ctt = new CallableThreadTest(); FutureTask ft = new FutureTask<>(ctt);

        for(int i = 0;i < 100;i++) 

        { 

            System.out.println(Thread.currentThread().getName()+" 的循環(huán)變量i的值"+i); 

            if(i==20) 

            { 

                new Thread(ft,"有返回值的線程").start(); 

            } 

        } 

        try 

        { 

            System.out.println("子線程的返回值:"+ft.get()); 

        } catch (InterruptedException e) 

        { 

            e.printStackTrace(); 

        } catch (ExecutionException e) 

        { 

            e.printStackTrace(); 

        } 

    }

    @Override 

    public Integer call() throws Exception 

    { 

        int i = 0; 

        for(;i<100;i++) 

        { 

            System.out.println(Thread.currentThread().getName()+" "+i); 

        } 

        return i; 

    } 

}

4航闺、Thread相關(guān)方法

4.1 public void start()

使該線程開始執(zhí)行;Java 虛擬機(jī)調(diào)用該線程的 run 方法猴誊。

4.2 public void run()

如果該線程是使用獨(dú)立的 Runnable 運(yùn)行對(duì)象構(gòu)造的潦刃,則調(diào)用該 Runnable 對(duì)象的 run 方法;否則懈叹,該方法不執(zhí)行任何操作并返回乖杠。一般不通過調(diào)用該方法來運(yùn)行線程,而是通過start()方法澄成。

4.3 public final void setPriority(int priority)

更改線程的優(yōu)先級(jí)胧洒。 java 中的線程優(yōu)先級(jí)的范圍是1~10,默認(rèn)的優(yōu)先級(jí)是5墨状∥缆“高優(yōu)先級(jí)線程”會(huì)優(yōu)先于“低優(yōu)先級(jí)線程”執(zhí)行。線程的優(yōu)先級(jí)只能確保CPU盡量將執(zhí)行的資源讓給優(yōu)先級(jí)高的線程用肾砂,但不保證定義的高優(yōu)先級(jí)的線程的大部分都能先于低優(yōu)先級(jí)的線程執(zhí)行完汛兜。線程的優(yōu)先級(jí)具有隨機(jī)性,也就是高優(yōu)先級(jí)的線程不一定每一次都先執(zhí)行完通今。

4.4 public final void setDaemon(boolean on)

將該線程標(biāo)記為守護(hù)線程或用戶線程。Java中有兩類線程:User Thread(用戶線程)肛根、Daemon Thread(守護(hù)線程)辫塌。用個(gè)比較通俗的比如,任何一個(gè)守護(hù)線程都是整個(gè)JVM中所有非守護(hù)線程的保姆派哲。當(dāng)最后一個(gè)非守護(hù)線程結(jié)束時(shí)臼氨,守護(hù)線程隨著JVM一同結(jié)束工作。Daemon的作用是為其他線程的運(yùn)行提供便利服務(wù)芭届,守護(hù)線程最典型的應(yīng)用就是 GC (垃圾回收器)储矩,它就是一個(gè)很稱職的守護(hù)者。User和Daemon兩者幾乎沒有區(qū)別褂乍,唯一的不同之處就在于虛擬機(jī)的離開:如果 User Thread已經(jīng)全部退出運(yùn)行了持隧,只剩下Daemon Thread存在了,虛擬機(jī)也就退出了逃片。 因?yàn)闆]有了被守護(hù)者屡拨,Daemon也就沒有工作可做了,也就沒有繼續(xù)運(yùn)行程序的必要了。

4.5 public final void join(long millisec)/public final void join()

等待該線程終止的時(shí)間最長為 millis 毫秒呀狼。如果不傳參數(shù)就代表一直等待直到調(diào)用join線程運(yùn)行完畢裂允。才繼續(xù)向下運(yùn)行。例子如下:


class Thread1 extends Thread{

    private String name; 

    public Thread1(String name) { 

        super(name); 

      this.name=name; 

    } 

    public void run() { 

        System.out.println(Thread.currentThread().getName() + " 線程運(yùn)行開始!"); 

        for (int i = 0; i < 5; i++) { 

            System.out.println("子線程"+name + "運(yùn)行 : " + i); 

            try { 

                sleep((int) Math.random() * 10); 

            } catch (InterruptedException e) { 

                e.printStackTrace(); 

            } 

        } 

        System.out.println(Thread.currentThread().getName() + " 線程運(yùn)行結(jié)束!"); 

    } 

public static void main(String[] args) {

        System.out.println(Thread.currentThread().getName()+"主線程運(yùn)行開始!"); 

        Thread1 mTh1=new Thread1("A"); 

        Thread1 mTh2=new Thread1("B"); 

        mTh1.start(); 

        mTh2.start(); 

        try { 

            mTh1.join(); 

        } catch (InterruptedException e) { 

            e.printStackTrace(); 

        } 

        try { 

            mTh2.join(); 

        } catch (InterruptedException e) { 

            e.printStackTrace(); 

        } 

        System.out.println(Thread.currentThread().getName()+ "主線程運(yùn)行結(jié)束!"); 

    } 

} 

其運(yùn)行結(jié)果如下:


main主線程運(yùn)行開始!

A 線程運(yùn)行開始!

子線程A運(yùn)行 : 0

B 線程運(yùn)行開始!

子線程B運(yùn)行 : 0

子線程A運(yùn)行 : 1

子線程B運(yùn)行 : 1

子線程A運(yùn)行 : 2

子線程B運(yùn)行 : 2

子線程A運(yùn)行 : 3

子線程B運(yùn)行 : 3

子線程A運(yùn)行 : 4

子線程B運(yùn)行 : 4

A 線程運(yùn)行結(jié)束!

B 線程運(yùn)行結(jié)束!

main主線程運(yùn)行結(jié)束!

主線程一定會(huì)等子線程都結(jié)束了才結(jié)束哥艇。

4.6 public static void yield()

暫停當(dāng)前正在執(zhí)行的線程對(duì)象绝编,并執(zhí)行其他線程。該方法是個(gè)靜態(tài)方法貌踏。讓當(dāng)前運(yùn)行線程回到可運(yùn)行狀態(tài)十饥,以允許具有相同優(yōu)先級(jí)的其他線程獲得運(yùn)行機(jī)會(huì)。因此哩俭,使用yield()的目的是讓相同優(yōu)先級(jí)的線程之間能適當(dāng)?shù)妮嗈D(zhuǎn)執(zhí)行绷跑。但是,實(shí)際中無法保證yield()達(dá)到讓步目的凡资,因?yàn)樽尣降木€程還有可能被線程調(diào)度程序再次選中砸捏。yield()方法對(duì)應(yīng)了如下操作:先檢測(cè)當(dāng)前是否有相同優(yōu)先級(jí)的線程處于同可運(yùn)行狀態(tài),如有隙赁,則把 CPU 的占有權(quán)交給此線程垦藏,否則,繼續(xù)運(yùn)行原來的線程伞访。所以yield()方法稱為“退讓”掂骏,它把運(yùn)行機(jī)會(huì)讓給了同等優(yōu)先級(jí)的其他線程。

例子如下:


class ThreadYield extends Thread{

    public ThreadYield(String name) { 

        super(name); 

    } 

    @Override 

    public void run() { 

        for (int i = 1; i <= 50; i++) { 

            System.out.println("" + this.getName() + "-----" + i); 

            // 當(dāng)i為30時(shí)厚掷,該線程就會(huì)把CPU時(shí)間讓掉弟灼,讓其他或者自己的線程執(zhí)行(也就是誰先搶到誰執(zhí)行) 

            if (i ==30) { 

                this.yield(); 

            } 

        } 

} 

} 

public class Main { 

    public static void main(String[] args) { 

        ThreadYield yt1 = new ThreadYield("張三"); 

        ThreadYield yt2 = new ThreadYield("李四"); 

        yt1.start(); 

        yt2.start(); 

    } 

} 

上述代碼會(huì)有兩種運(yùn)行結(jié)果:

第一種情況:李四(線程)當(dāng)執(zhí)行到30時(shí)會(huì)CPU時(shí)間讓掉,這時(shí)張三(線程)搶到CPU時(shí)間并執(zhí)行冒黑。

第二種情況:李四(線程)當(dāng)執(zhí)行到30時(shí)會(huì)CPU時(shí)間讓掉田绑,這時(shí)李四(線程)搶到CPU時(shí)間并執(zhí)行。

4.7 public static void sleep(long millisec)

在指定的毫秒數(shù)內(nèi)讓當(dāng)前正在執(zhí)行的線程休眠(暫停執(zhí)行)抡爹,此操作受到系統(tǒng)計(jì)時(shí)器和調(diào)度程序精度和準(zhǔn)確性的影響掩驱。sleep 方法使當(dāng)前運(yùn)行中的線程睡眼一段時(shí)間,進(jìn)入不可運(yùn)行狀態(tài)冬竟,這段時(shí)間的長短是由程序設(shè)定的欧穴。sleep 方法允許較低優(yōu)先級(jí)的線程獲得運(yùn)行機(jī)會(huì)。sleep相當(dāng)于讓線程睡眠泵殴,交出CPU涮帘,讓CPU去執(zhí)行其他的任務(wù)。但是有一點(diǎn)要非常注意笑诅,sleep方法不會(huì)釋放鎖焚辅,也就是說如果當(dāng)前線程持有對(duì)某個(gè)對(duì)象的鎖映屋,則即使調(diào)用sleep方法,其他線程也無法訪問這個(gè)對(duì)象同蜻。

4.8 public void interrupt()/public static boolean interrupted()/public boolean isInterrupted()

一個(gè)線程在未正常結(jié)束之前, 被強(qiáng)制終止是很危險(xiǎn)的事情. 因?yàn)樗赡軒硗耆A(yù)料不到的嚴(yán)重后果比如會(huì)帶著自己所持有的鎖而永遠(yuǎn)的休眠棚点,遲遲不歸還鎖等。 所以Thread.suspend, Thread.stop等方法都被Deprecated了湾蔓。不能直接把一個(gè)線程搞掛掉, 但有時(shí)候又有必要讓一個(gè)線程死掉, 或者讓它結(jié)束某種等待的狀態(tài) 該怎么辦呢?一個(gè)比較優(yōu)雅而安全的做法是:使用等待/通知機(jī)制或者給那個(gè)線程一個(gè)中斷信號(hào), 讓它自己決定該怎么辦瘫析。⑧中的三個(gè)方法就是和其相關(guān)的三個(gè)核心方法。中斷通過調(diào)用Thread.interrupt()方法來實(shí)現(xiàn). 這個(gè)方法通過修改了被調(diào)用線程的中斷狀態(tài)來告知那個(gè)線程, 說它被中斷了. 對(duì)于非阻塞中的線程, 只是改變了中斷狀態(tài), 即Thread.isInterrupted()將返回true; 對(duì)于可取消的阻塞狀態(tài)中的線程, 比如等待在這些函數(shù)上的線程, Thread.sleep(), Object.wait(), Thread.join(), 這個(gè)線程收到中斷信號(hào)后, 會(huì)拋出InterruptedException, 同時(shí)會(huì)把中斷狀態(tài)置回為true.但調(diào)用Thread.interrupted()會(huì)對(duì)中斷狀態(tài)進(jìn)行復(fù)位默责。

非阻塞中線程例子:


public class MyThread extends Thread{

    public void run(){ 

        while(true){ 

            if(Thread.currentThread().isInterrupted()){ 

                System.out.println("Someone interrupted me."); 

            } 

            else{ 

                System.out.println("Thread is Going..."); 

            }

        } 

    } 

    public static void main(String[] args) throws InterruptedException { 

        MyThread t = new MyThread(); 

        t.start(); 

        Thread.sleep(3000); 

        t.interrupt(); 

    } 

} 

在main線程sleep的過程中由于t線程中isInterrupted()為false所以不斷的輸出”Thread is going”贬循。當(dāng)調(diào)用t線程的interrupt()后t線程中isInterrupted()為true。此時(shí)會(huì)輸出Someone interrupted me.而且線程并不會(huì)因?yàn)橹袛嘈盘?hào)而停止運(yùn)行桃序。因?yàn)樗皇潜恍薷囊粋€(gè)中斷信號(hào)而已杖虾。當(dāng)我們調(diào)用t.interrput()的時(shí)候,線程t的中斷狀態(tài)(interrupted status) 會(huì)被置位媒熊。我們可以通過Thread.currentThread().isInterrupted() 來檢查這個(gè)布爾型的中斷狀態(tài)奇适。interrupt中斷的是線程的某一部分業(yè)務(wù)邏輯,前提是線程需要檢查自己的中斷狀態(tài)(isInterrupted())芦鳍。

如果線程的當(dāng)前狀態(tài)處于阻塞狀態(tài)嚷往,那么在將中斷標(biāo)志設(shè)置為true后,還會(huì)有如下三種情況之一的操作:

① 如果是wait柠衅、sleep以及jion三個(gè)方法引起的阻塞皮仁,那么會(huì)將線程的中斷標(biāo)志重新設(shè)置為false,并拋出一個(gè)InterruptedException菲宴。

② 如果是java.nio.channels.InterruptibleChannel進(jìn)行的io操作引起的阻塞贷祈,則會(huì)對(duì)線程拋出一個(gè)ClosedByInterruptedException。

③ 如果是輪詢(java.nio.channels.Selectors)引起的線程阻塞喝峦,則立即返回势誊,不會(huì)拋出異常。

如果在中斷時(shí)愈犹,線程正處于非阻塞狀態(tài),則將中斷標(biāo)志修改為true,而在此基礎(chǔ)上闻丑,一旦進(jìn)入阻塞狀態(tài)漩怎,則按照阻塞狀態(tài)的情況來進(jìn)行處理;例如嗦嗡,一個(gè)線程在運(yùn)行狀態(tài)中勋锤,其中斷標(biāo)志被設(shè)置為true,則此后,一旦線程調(diào)用了wait侥祭、jion叁执、sleep方法中的一種茄厘,立馬拋出一個(gè)InterruptedException,且中斷標(biāo)志被清除谈宛,重新設(shè)置為false次哈。

通過上面的分析,調(diào)用線程類的interrupted方法吆录,其本質(zhì)只是設(shè)置該線程的中斷標(biāo)志窑滞,將中斷標(biāo)志設(shè)置為true,并根據(jù)線程狀態(tài)決定是否拋出異常恢筝。因此哀卫,通過interrupted方法真正實(shí)現(xiàn)線程的中斷原理是:開發(fā)人員根據(jù)中斷標(biāo)志的具體值,來決定如何退出線程撬槽。interrupte方法的調(diào)用此改,該方法可在需要中斷的線程本身中調(diào)用,也可在其他線程中調(diào)用需要中斷的線程對(duì)象的該方法侄柔。綜合考慮兩種情況的run例子如下


public void run() {

            try { 

                while (true){ 

                    Thread.sleep(1000l);//阻塞狀態(tài)共啃,線程被調(diào)用了interrupte()方法,清除中斷標(biāo)志勋拟,拋出InterruptedException 

                    //dosomething 

                    boolean isIn = this.isInterrupted(); 

                    //運(yùn)行狀態(tài)勋磕,線程被調(diào)用了interrupte()方法,中斷標(biāo)志被設(shè)置為true 

                    //非阻塞狀態(tài)中進(jìn)行中斷線程操作 

                    if(isInterrupted()) break;//退出循環(huán)敢靡,中斷進(jìn)程 

                } 

            }catch (InterruptedException e){//阻塞狀態(tài)中進(jìn)行中斷線程操作 

                boolean isIn = this.isInterrupted();//退出阻塞狀態(tài)挂滓,且中斷標(biāo)志被清除,重新設(shè)置為false啸胧,所以此處的isIn為false 

                return;//退出run方法赶站,中斷進(jìn)程 

            } 

        } 

4.9 public final boolean isAlive()

測(cè)試線程是否處于活動(dòng)狀態(tài)。

4.10 public static Thread currentThread()

返回對(duì)當(dāng)前正在執(zhí)行的線程對(duì)象的引用纺念。

5贝椿、Object 和線程相關(guān)方法

Object 和線程相關(guān)方法主要有三個(gè)方法:wait(), notify(), notifyAll()。這幾個(gè)方法經(jīng)常與synchronized搭配使用陷谱,即在synchronized修飾的同步代碼塊或方法里面調(diào)用wait() 與 notify/notifyAll()方法烙博。關(guān)于synchronized在下面會(huì)詳細(xì)介紹。

由于 wait() 與 notify/notifyAll() 是放在同步代碼塊中的烟逊,因此線程在執(zhí)行它們時(shí)渣窜,肯定是進(jìn)入了臨界區(qū)中的,即該線程肯定是獲得了鎖的宪躯,如果在執(zhí)行wait() 與 notify/notifyAll() 之前沒有獲得相應(yīng)的對(duì)象鎖乔宿,就會(huì)拋出:java.lang.IllegalMonitorStateException異常。访雪。當(dāng)線程執(zhí)行wait()時(shí)详瑞,會(huì)把當(dāng)前的鎖釋放掂林,然后讓出CPU,進(jìn)入等待狀態(tài)坝橡。當(dāng)執(zhí)行notify/notifyAll方法時(shí)泻帮,會(huì)喚醒一個(gè)處于等待該 對(duì)象鎖 的線程,然后繼續(xù)往下執(zhí)行驳庭,直到執(zhí)行完退出對(duì)象鎖鎖住的區(qū)域(synchronized修飾的代碼塊)后再釋放鎖刑顺。從這里可以看出,notify/notifyAll()執(zhí)行后饲常,并不立即釋放鎖蹲堂,而是要等到執(zhí)行完臨界區(qū)中代碼后,再釋放贝淤。在實(shí)際編程中柒竞,應(yīng)該盡量在線程調(diào)用notify/notifyAll()后,立即退出臨界區(qū)播聪。即不要在notify/notifyAll()后面再寫一些耗時(shí)的代碼朽基。假設(shè)在線程A中執(zhí)行wait(),在線程B中執(zhí)行notify()离陶。但如果線程B先執(zhí)行了notify()然后結(jié)束了稼虎,線程A才去執(zhí)行wait(),那此時(shí)招刨,線程A將無法被正常喚醒了霎俩。下面是一個(gè)經(jīng)典的例子,通過調(diào)用Object的wait()沉眶,notify()實(shí)現(xiàn)建立三個(gè)線程打却,A線程打印10次A,B線程打印10次B,C線程打印10次C谎倔,要求線程同時(shí)運(yùn)行柳击,交替打印10次ABC。


public class MyThreadPrinter implements Runnable {

    private String name;   

    private Object prev;   

    private Object self;   

    private MyThreadPrinter(String name, Object prev, Object self) {   

        this.name = name;   

        this.prev = prev;   

        this.self = self;   

    }   

    @Override   

    public void run() {   

        int count = 10;   

        while (count > 0) {   

            synchronized (prev) {   

                synchronized (self) {   

                    System.out.print(name);   

                    count--;   

                    self.notify();   

                }   

                try {   

                    prev.wait();   

                } catch (InterruptedException e) {   

                    e.printStackTrace();   

                }   

            }   

        }   

    }   

    public static void main(String[] args) throws Exception {   

        Object a = new Object();   

        Object b = new Object();   

        Object c = new Object();   

        MyThreadPrinter pa = new MyThreadPrinter("A", c, a);   

        MyThreadPrinter pb = new MyThreadPrinter("B", a, b);   

        MyThreadPrinter pc = new MyThreadPrinter("C", b, c);   

        new Thread(pa).start(); 

        Thread.sleep(100);  //確保按順序A片习、B捌肴、C執(zhí)行 

        new Thread(pb).start(); 

        Thread.sleep(100);   

        new Thread(pc).start();   

        Thread.sleep(100);   

        }   

}   

解釋一下其整體思路,從大的方向上來講藕咏,該問題為三線程間的同步喚醒操作状知,主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA循環(huán)執(zhí)行三個(gè)線程。為了控制線程執(zhí)行的順序侈离,那么就必須要確定喚醒试幽、等待的順序筝蚕,所以每一個(gè)線程必須同時(shí)持有兩個(gè)對(duì)象鎖卦碾,才能繼續(xù)執(zhí)行铺坞。一個(gè)對(duì)象鎖是prev,就是前一個(gè)線程所持有的對(duì)象鎖洲胖。還有一個(gè)就是自身對(duì)象鎖济榨。主要的思想就是,為了控制執(zhí)行的順序绿映,必須要先持有prev鎖擒滑,也就前一個(gè)線程要釋放自身對(duì)象鎖,再去申請(qǐng)自身對(duì)象鎖叉弦,兩者兼?zhèn)鋾r(shí)打印丐一,之后首先調(diào)用self.notify()釋放自身對(duì)象鎖,喚醒下一個(gè)等待線程淹冰,再調(diào)用prev.wait()釋放prev對(duì)象鎖库车,終止當(dāng)前線程,等待循環(huán)結(jié)束后再次被喚醒樱拴。運(yùn)行上述代碼柠衍,可以發(fā)現(xiàn)三個(gè)線程循環(huán)打印ABC,共10次晶乔。

6珍坊、synchronized

6.1 修飾實(shí)例方法

作用于當(dāng)前實(shí)例加鎖,進(jìn)入同步代碼前要獲得當(dāng)前實(shí)例的鎖正罢。所謂的實(shí)例對(duì)象鎖就是用synchronized修飾實(shí)例對(duì)象中的實(shí)例方法阵漏,注意是實(shí)例方法不包括靜態(tài)方法,例如:


public class AccountingSync implements Runnable{

    //共享資源(臨界資源)

    static int i=0;

    /**

    * synchronized 修飾實(shí)例方法

    */

    public synchronized void increase(){

        i++;

    }

    @Override

    public void run() {

        for(int j=0;j<1000000;j++){

            increase();

        }

    }

    public static void main(String[] args) throws InterruptedException {

        AccountingSync instance=new AccountingSync();

        Thread t1=new Thread(instance);

        Thread t2=new Thread(instance);

        t1.start();

        t2.start();

        t1.join();

        t2.join();

        System.out.println(i);

    }

    /**

    * 輸出結(jié)果:

    * 2000000

    */

}

上述代碼中腺怯,開啟兩個(gè)線程操作同一個(gè)共享資源即變量i袱饭,由于i++;操作并不具備原子性,該操作是先讀取值呛占,然后寫回一個(gè)新值虑乖,相當(dāng)于原來的值加上1,分兩步完成晾虑,如果第二個(gè)線程在第一個(gè)線程讀取舊值和寫回新值期間讀取i的域值疹味,那么第二個(gè)線程就會(huì)與第一個(gè)線程一起看到同一個(gè)值,并執(zhí)行相同值的加1操作帜篇,這也就造成了線程安全失敗糙捺,因此對(duì)于increase方法必須使用synchronized修飾,以便保證線程安全笙隙。此時(shí)我們應(yīng)該注意到synchronized修飾的是實(shí)例方法increase洪灯,在這樣的情況下,當(dāng)前線程的鎖便是實(shí)例對(duì)象instance竟痰,注意Java中的線程同步鎖可以是任意對(duì)象签钩。從代碼執(zhí)行結(jié)果來看確實(shí)是正確的掏呼,倘若我們沒有使用synchronized關(guān)鍵字,其最終輸出結(jié)果就很可能小于2000000铅檩,這便是synchronized關(guān)鍵字的作用憎夷。這里我們還需要意識(shí)到,當(dāng)一個(gè)線程正在訪問一個(gè)對(duì)象的 synchronized 實(shí)例方法昧旨,那么其他線程不能訪問該對(duì)象的其他 synchronized 方法拾给,畢竟一個(gè)對(duì)象只有一把鎖,當(dāng)一個(gè)線程獲取了該對(duì)象的鎖之后兔沃,其他線程無法獲取該對(duì)象的鎖蒋得,所以無法訪問該對(duì)象的其他synchronized實(shí)例方法,但是其他線程還是可以訪問該實(shí)例對(duì)象的其他非synchronized方法乒疏,當(dāng)然如果是一個(gè)線程 A 需要訪問實(shí)例對(duì)象 obj1 的 synchronized 方法 f1(當(dāng)前對(duì)象鎖是obj1)窄锅,另一個(gè)線程 B 需要訪問實(shí)例對(duì)象 obj2 的 synchronized 方法 f2(當(dāng)前對(duì)象鎖是obj2),這樣是允許的缰雇,因?yàn)閮蓚€(gè)實(shí)例對(duì)象鎖并不同相同入偷,此時(shí)如果兩個(gè)線程操作數(shù)據(jù)并非共享的,線程安全是有保障的械哟,如果兩個(gè)線程操作的是共享數(shù)據(jù)疏之,那么線程安全就有可能無法保證了,如下代碼將演示出該現(xiàn)象:


public class AccountingSyncBad implements Runnable{

    static int i=0;

    public synchronized void increase(){

        i++;

    }

    @Override

    public void run() {

        for(int j=0;j<1000000;j++){

            increase();

        }

    }

    public static void main(String[] args) throws InterruptedException {

        //new新實(shí)例

        Thread t1=new Thread(new AccountingSyncBad());

        //new新實(shí)例

        Thread t2=new Thread(new AccountingSyncBad());

        t1.start();

        t2.start();

        //join含義:當(dāng)前線程A等待thread線程終止之后才能從thread.join()返回

        t1.join();

        t2.join();

        System.out.println(i);

    }

}

上述代碼與前面不同的是我們同時(shí)創(chuàng)建了兩個(gè)新實(shí)例AccountingSyncBad暇咆,然后啟動(dòng)兩個(gè)不同的線程對(duì)共享變量i進(jìn)行操作锋爪,但很遺憾操作結(jié)果不是期望結(jié)果2000000,因?yàn)樯鲜龃a犯了嚴(yán)重的錯(cuò)誤爸业,雖然我們使用synchronized修飾了increase方法其骄,但卻new了兩個(gè)不同的實(shí)例對(duì)象,這也就意味著存在著兩個(gè)不同的實(shí)例對(duì)象鎖扯旷,因此t1和t2都會(huì)進(jìn)入各自的對(duì)象鎖拯爽,也就是說t1和t2線程使用的是不同的鎖,因此線程安全是無法保證的钧忽。解決這種困境的的方式是將synchronized作用于靜態(tài)的increase方法毯炮,這樣的話,對(duì)象鎖就當(dāng)前類對(duì)象耸黑,由于無論創(chuàng)建多少個(gè)實(shí)例對(duì)象桃煎,但對(duì)于的類對(duì)象擁有只有一個(gè),所有在這樣的情況下對(duì)象鎖就是唯一的大刊。下面我們看看如何使用將synchronized作用于靜態(tài)的increase方法为迈。

6.2 修飾靜態(tài)方法

當(dāng)synchronized作用于靜態(tài)方法時(shí),其鎖就是當(dāng)前類的class對(duì)象鎖。由于靜態(tài)成員不專屬于任何一個(gè)實(shí)例對(duì)象葫辐,是類成員赋续,因此通過class對(duì)象鎖可以控制靜態(tài) 成員的并發(fā)操作。需要注意的是如果一個(gè)線程A調(diào)用一個(gè)實(shí)例對(duì)象的非static synchronized方法另患,而線程B需要調(diào)用這個(gè)實(shí)例對(duì)象所屬類的靜態(tài) synchronized方法,是允許的蛾绎,不會(huì)發(fā)生互斥現(xiàn)象昆箕,因?yàn)樵L問靜態(tài) synchronized 方法占用的鎖是當(dāng)前類的class對(duì)象,而訪問非靜態(tài) synchronized 方法占用的鎖是當(dāng)前實(shí)例對(duì)象鎖租冠,看如下代碼:


public class AccountingSyncClass implements Runnable{

    static int i=0;

    /**

    * 作用于靜態(tài)方法,鎖是當(dāng)前class對(duì)象,也就是

    * AccountingSyncClass類對(duì)應(yīng)的class對(duì)象

    */

    public static synchronized void increase(){

        i++;

    }

    /**

    * 非靜態(tài),訪問時(shí)鎖不一樣不會(huì)發(fā)生互斥

    */

    public synchronized void increase4Obj(){

        i++;

    }

    @Override

    public void run() {

        for(int j=0;j<1000000;j++){

            increase();

        }

    }

    public static void main(String[] args) throws InterruptedException {

        //new新實(shí)例

        Thread t1=new Thread(new AccountingSyncClass());

        //new心事了

        Thread t2=new Thread(new AccountingSyncClass());

        //啟動(dòng)線程

        t1.start();t2.start();

        t1.join();t2.join();

        System.out.println(i);

    }

}

由于synchronized關(guān)鍵字修飾的是靜態(tài)increase方法鹏倘,與修飾實(shí)例方法不同的是,其鎖對(duì)象是當(dāng)前類的class對(duì)象顽爹。注意代碼中的increase4Obj方法是實(shí)例方法纤泵,其對(duì)象鎖是當(dāng)前實(shí)例對(duì)象,如果別的線程調(diào)用該方法镜粤,將不會(huì)產(chǎn)生互斥現(xiàn)象捏题,畢竟鎖對(duì)象不同,但我們應(yīng)該意識(shí)到這種情況下可能會(huì)發(fā)現(xiàn)線程安全問題(操作了共享靜態(tài)變量i)肉渴。

6.3 修飾代碼塊

除了使用關(guān)鍵字修飾實(shí)例方法和靜態(tài)方法外公荧,還可以使用同步代碼塊,在某些情況下同规,同時(shí)存在一些比較耗時(shí)的操作循狰,而需要同步的代碼又只有一小部分,如果直接對(duì)整個(gè)方法進(jìn)行同步操作券勺,可能會(huì)得不償失绪钥,此時(shí)我們可以使用同步代碼塊的方式對(duì)需要同步的代碼進(jìn)行包裹,這樣就無需對(duì)整個(gè)方法進(jìn)行同步操作了关炼,同步代碼塊的使用示例如下:


public class AccountingSync implements Runnable{

    static AccountingSync instance=new AccountingSync();

    static int i=0;

    @Override

    public void run() {

        //省略其他耗時(shí)操作....

        //使用同步代碼塊對(duì)變量i進(jìn)行同步操作,鎖對(duì)象為instance

        synchronized(instance){

            for(int j=0;j<1000000;j++){

                    i++;

              }

        }

    }

    public static void main(String[] args) throws InterruptedException {

        Thread t1=new Thread(instance);

        Thread t2=new Thread(instance);

        t1.start();t2.start();

        t1.join();t2.join();

        System.out.println(i);

    }

}

從代碼看出程腹,將synchronized作用于一個(gè)給定的實(shí)例對(duì)象instance,即當(dāng)前實(shí)例對(duì)象就是鎖對(duì)象儒拂,每次當(dāng)線程進(jìn)入synchronized包裹的代碼塊時(shí)就會(huì)要求當(dāng)前線程持有instance實(shí)例對(duì)象鎖跪楞,如果當(dāng)前有其他線程正持有該對(duì)象鎖,那么新到的線程就必須等待侣灶,這樣也就保證了每次只有一個(gè)線程執(zhí)行i++;操作甸祭。當(dāng)然除了instance作為對(duì)象外,我們還可以使用this對(duì)象(代表當(dāng)前實(shí)例)或者當(dāng)前類的class對(duì)象作為鎖褥影,如下代碼:


//this,當(dāng)前實(shí)例對(duì)象鎖

synchronized(this){

    for(int j=0;j<1000000;j++){

        i++;

    }

}

//class對(duì)象鎖

synchronized(AccountingSync.class){

    for(int j=0;j<1000000;j++){

        i++;

    }

}

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末池户,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌校焦,老刑警劉巖赊抖,帶你破解...
    沈念sama閱讀 217,084評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異寨典,居然都是意外死亡氛雪,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門耸成,熙熙樓的掌柜王于貴愁眉苦臉地迎上來报亩,“玉大人,你說我怎么就攤上這事井氢∠易罚” “怎么了?”我有些...
    開封第一講書人閱讀 163,450評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵花竞,是天一觀的道長劲件。 經(jīng)常有香客問我,道長约急,這世上最難降的妖魔是什么零远? 我笑而不...
    開封第一講書人閱讀 58,322評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮厌蔽,結(jié)果婚禮上遍烦,老公的妹妹穿的比我還像新娘。我一直安慰自己躺枕,他們只是感情好服猪,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,370評(píng)論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著拐云,像睡著了一般罢猪。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上叉瘩,一...
    開封第一講書人閱讀 51,274評(píng)論 1 300
  • 那天膳帕,我揣著相機(jī)與錄音,去河邊找鬼薇缅。 笑死危彩,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的泳桦。 我是一名探鬼主播汤徽,決...
    沈念sama閱讀 40,126評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼灸撰!你這毒婦竟也來了谒府?” 一聲冷哼從身側(cè)響起拼坎,我...
    開封第一講書人閱讀 38,980評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎完疫,沒想到半個(gè)月后泰鸡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,414評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡壳鹤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,599評(píng)論 3 334
  • 正文 我和宋清朗相戀三年盛龄,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片芳誓。...
    茶點(diǎn)故事閱讀 39,773評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡余舶,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出兆沙,到底是詐尸還是另有隱情,我是刑警寧澤莉掂,帶...
    沈念sama閱讀 35,470評(píng)論 5 344
  • 正文 年R本政府宣布葛圃,位于F島的核電站,受9級(jí)特大地震影響憎妙,放射性物質(zhì)發(fā)生泄漏库正。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,080評(píng)論 3 327
  • 文/蒙蒙 一厘唾、第九天 我趴在偏房一處隱蔽的房頂上張望褥符。 院中可真熱鬧,春花似錦抚垃、人聲如沸喷楣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,713評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽铣焊。三九已至,卻和暖如春罕伯,著一層夾襖步出監(jiān)牢的瞬間曲伊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,852評(píng)論 1 269
  • 我被黑心中介騙來泰國打工追他, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留坟募,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,865評(píng)論 2 370
  • 正文 我出身青樓邑狸,卻偏偏與公主長得像懈糯,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子单雾,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,689評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容