1. 程式人生 > >JAVA進階(06)多執行緒

JAVA進階(06)多執行緒

一、三個概念

1、程式

  • 程式(Program)是一個靜態的概念,一般對應於作業系統中的一個可執行檔案

2、程序

(1)執行中的程式叫做程序(Process),是一個動態的概念

(2)特點:

  • 程序是程式的一次動態執行過程, 佔用特定的地址空間
  • 每個程序由3部分組成:cpu、data、code,每個程序都是獨立的,保有自己的cpu時間、程式碼、資料,程序一多會加大記憶體和 cpu 的負擔
  • 多工作業系統將CPU時間動態地劃分給每個程序,作業系統同時執行多個程序,每個程序獨立執行

3、執行緒

(1)概念:一個程序可產生多個執行緒,同一程序的多個執行緒也可共享此程序的某些資源(程式碼、資料),執行緒又被稱為輕量級程序

(2)特點:

  • 一個程序內部的一個執行單元,是程式中的一個單一的順序控制流程
  • 一個程序可擁有多個並行的執行緒
  •  一個程序中的多個執行緒共享相同的記憶體單元,可以訪問相同的變數和物件,從同一堆中分配物件並進行通訊、資料交換和同步操作
  • 由於執行緒間的通訊是在同一地址空間上進行的,使得通訊更簡便,資訊傳遞速度更快
  • 執行緒消耗的資源非常少

å¾11-2 线ç¨å±äº«èµæºç¤ºæå¾.png

4、程序和執行緒的區別

  • 執行緒和程序最根本的區別在於:程序是資源分配的單位,執行緒是排程和執行的單位;
  • 每個程序都有獨立的資源,程序間的切換會有較大的開銷, 執行緒是輕量級的程序,多執行緒共享資源,執行緒切換的開銷小;
  • 多程序是在作業系統中同時執行多個任務(程式), 多執行緒是在同一應用程式中有多個順序流同時執行;

5、程序和程式的區別

  • 程序是程式的一部分,程式執行的時候會產生程序;
  • 一個程式可以沒有程序,可以有一個程序,也可以有多個程序;

 

二、JAVA中的多執行緒實現

1、繼承Thread類實現多執行緒

(1)實現步驟:   

  • 寫一個類繼承Thread,並寫一個執行緒體(run 方法);
  • new 一個該類的物件;
  • 通過物件呼叫start()方法來啟動一個執行緒,執行 run 方法;

(2)缺陷:由於 java 中的單繼承的特性,如果該類已經繼承了一個類,則無法再繼承 Thread 類了

2、Runnable介面實現多執行緒

  • 克服了方法一的缺點
public class TestThread2 implements Runnable {//自定義類實現Runnable介面;
    //run()方法裡是執行緒體;
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
    public static void main(String[] args) {
        //建立執行緒物件,把實現了Runnable介面的物件作為引數傳入;
        Thread thread1 = new Thread(new TestThread2());
        thread1.start();//啟動執行緒;
        Thread thread2 = new Thread(new TestThread2());
        thread2.start();
    }
}

 

三、執行緒狀態

1、五種狀態

    å¾11-4 线ç¨çå½å¨æå¾.png

(1) 新生狀態(New)

  • 用new關鍵字建立一個執行緒物件後,該執行緒物件就處於新生狀態

(2)就緒狀態(Runnable)

   1、新生態執行緒呼叫start方法進入就緒狀態,等待系統為其分配CPU

   2、以下3種狀態也可進入就緒狀態:

  • 阻塞執行緒:阻塞解除,進入就緒狀態
  • 執行執行緒:呼叫yield()方法,直接進入就緒狀態
  • 執行執行緒:JVM將CPU資源從本執行緒切換到其他執行緒

(3)執行狀態(Running)

  • 獲得 cpu 執行run方法,直到呼叫其他方法而終止或等待某資源而阻塞或完成任務而死亡

(4)阻塞狀態(Blocked)

   1、阻塞指的是暫停一個執行緒的執行以等待某個條件發生(如某資源就緒),有4種原因會導致阻塞:

  • 執行sleep(int millsecond)方法,使當前執行緒休眠,進入阻塞狀態,當指定的時間到了後,執行緒進入就緒狀態。
  • 執行wait()方法,使當前執行緒進入阻塞狀態,當使用nofity()方法喚醒這個執行緒後,它進入就緒狀態。
  • 執行緒執行時,某個操作進入阻塞狀態,比如執行IO流操作,只有當引起該操作阻塞的原因消失後,執行緒進入就緒狀態。
  • join()執行緒聯合: 當某個執行緒等待另一個執行緒執行結束後,才能繼續執行時,使用join()方法。

(5)死亡狀態(Terminated)

   1、執行緒死亡的原因有兩個:

  • 一個是正常執行的執行緒完成了它run()方法內的全部工作
  • 另一個是執行緒被強制終止,如通過執行stop()或destroy()方法來終止一個執行緒(不推薦使用)

2、終止執行緒的典型方法(重要)

public class TestThreadCiycle implements Runnable {
    String name;
    boolean live = true;// 標記變數,表示執行緒是否可中止;
    public TestThreadCiycle(String name) {
        super();
        this.name = name;
    }
    public void run() {
        int i = 0;
        //當live的值是true時,繼續執行緒體;false則結束迴圈,繼而終止執行緒體;
        while (live) {
            System.out.println(name + (i++));
        }
    }
    public void terminate() {
        live = false;
    }
 
    public static void main(String[] args) {
        TestThreadCiycle ttc = new TestThreadCiycle("執行緒A:");
        Thread t1 = new Thread(ttc);// 新生狀態
        t1.start();// 就緒狀態
        for (int i = 0; i < 100; i++) {
            System.out.println("主執行緒" + i);
        }
        ttc.terminate();
        System.out.println("ttc stop!");
    }
}

3、暫停執行緒執行sleep/yield

(1)暫停執行緒執行常用的方法有sleep()和yield()方法,這兩個方法的區別是:

  • sleep()方法:讓執行緒進入阻塞狀態,直到休眠時間滿了,進入就緒狀態,執行時有延遲
  • yield()方法:讓執行緒直接進入就緒狀態,讓出CPU的使用權,執行時無明顯延遲
Thread.sleep(2000);//呼叫執行緒的sleep()方法;
Thread.yield();//呼叫執行緒的yield()方法;

4、執行緒聯合join()

(1)執行緒A在執行期間,呼叫執行緒B的join()方法,讓B和A聯合,這樣執行緒A就必須等待執行緒B執行完畢後,才能繼續執行

(2)示例程式碼:

public class TestThreadState {
    public static void main(String[] args) {
        System.out.println("爸爸和兒子買菸故事");
        Thread father = new Thread(new FatherThread());
        father.start();
    }
}
 
class FatherThread implements Runnable {
    public void run() {
        System.out.println("爸爸想抽菸,發現煙抽完了");
        System.out.println("爸爸讓兒子去買包紅塔山");
        Thread son = new Thread(new SonThread());
        son.start();
        System.out.println("爸爸等兒子買菸回來");
        try {
            son.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("爸爸出門去找兒子跑哪去了");
            // 結束JVM。如果是0則表示正常結束;如果是非0則表示非正常結束
            System.exit(1);
        }
        System.out.println("爸爸高興的接過煙開始抽,並把零錢給了兒子");
    }
}
 
class SonThread implements Runnable {
    public void run() {
        System.out.println("兒子出門去買菸");
        System.out.println("兒子買菸需要10分鐘");
        try {
            for (int i = 1; i <= 10; i++) {
                System.out.println("第" + i + "分鐘");
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("兒子買菸回來了");
    }
}

 

四、執行緒基本資訊和優先順序

1、獲取執行緒基本資訊的方法

表11-1线ç¨ç常ç¨æ¹æ³.png

2、執行緒的優先順序

  • 處於就緒狀態的執行緒,等待JVM來挑選
  • 執行緒的優先順序用數字表示,範圍從1到10,一個執行緒的預設優先順序是5
  • getPriority() 和 setPriority(10) 獲得或設定執行緒物件的優先順序
  • 優先順序低只是意味著獲得排程的概率低,並不是絕對先呼叫優先順序高的執行緒後呼叫優先順序低的執行緒

 

五、執行緒同步和併發

1、執行緒同步的概念

  • 多執行緒訪問修改同一個物件,可能存在安全問題, 這時需要用到執行緒同步
  • 執行緒同步其實就是一種等待機制,多執行緒排隊一個一個地訪問這個物件

2、執行緒同步的實現

(1)synchronized 方法

  • 語法
public  synchronized  void accessVal(int newVal);
  • 原理: synchronized 方法控制對物件的類成員變數的訪問,每個物件對應一把鎖,每個 synchronized 方法都必須獲得呼叫該方法的物件的鎖方能執行,否則所屬執行緒阻塞,方法一旦執行,就獨佔該鎖,直到從該方法返回時才將鎖釋放,此後被阻塞的執行緒方能獲得該鎖,重新進入可執行狀態

(2)synchronized塊

  • 說明:將一個大的方法宣告為synchronized 將會大大影響效率,塊可以實現精確地控制到具體的成員變數,縮小同步的範圍,提高效率
  • 語法:括號中的是物件鎖
synchronized(syncObject)
   { 
   //允許訪問控制的程式碼 
   }

3、死鎖及解決方案

(1)死鎖:一個同步塊需要同時擁有兩個以上物件的鎖時,可能會發生死鎖(synchronized 塊的巢狀造成的)

           synchronized (lipstick) {//需要得到口紅的“鎖”;
                System.out.println(girl + "拿著口紅!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
 
                synchronized (mirror) {//需要得到鏡子的“鎖”;
                    System.out.println(girl + "拿著鏡子!");
                }
 
            }

(2)解決方案:同步塊的巢狀改為並列即可

           synchronized (lipstick) {
                System.out.println(girl + "拿著口紅!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
 
            }
            synchronized (mirror) {
                System.out.println(girl + "拿著鏡子!");
            }

4、執行緒併發協作(生產者|消費者模式)

(1)3個概念

  • 生產者:指的是負責生產資料的模組(這裡模組可能是:方法、物件、執行緒、程序)。
  • 消費者:指的是負責處理資料的模組(這裡模組可能是:方法、物件、執行緒、程序)。
  • 緩衝區:生產者將生產好的資料放入“緩衝區”,消費者從緩衝區拿要處理的資料。

(2)緩衝區的好處

  • 實現執行緒的併發協作:有了緩衝區一個管放,一個管拿,互不影響;
  • 解耦了生產者和消費者: 生產者不需要和消費者直接打交道;
  • 解決忙閒不均,提高效率:生產慢依然可消費,消費慢依然可生產;

(3)示例程式碼

package com.wc.pro02;

public class TestProduce {
	public static void main(String[] args) {
        SyncStack sStack = new SyncStack();// 定義緩衝區物件;
        Shengchan sc = new Shengchan(sStack);// 定義生產執行緒;
        Xiaofei xf = new Xiaofei(sStack);// 定義消費執行緒;
        sc.start();
        xf.start();
    }
}

class Mantou {// 饅頭
    int id;
 
    Mantou(int id) {
        this.id = id;
    }
}
 
class SyncStack {// 緩衝區(相當於:饅頭筐)
    int index = 0;
    Mantou[] ms = new Mantou[10];
 
    public synchronized void push(Mantou m) {
        while (index == ms.length) {//說明饅頭筐滿了
            try {
               //wait後,執行緒會將持有的鎖釋放,進入阻塞狀態;
               //這樣其它需要鎖的執行緒就可以獲得鎖;
                this.wait();
                //這裡的含義是執行此方法的執行緒暫停,進入阻塞狀態,
                //等消費者消費了饅頭後再生產。
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 喚醒在當前物件等待池中等待的第一個執行緒。
        //notifyAll叫醒所有在當前物件等待池中等待的所有執行緒。
        this.notify();
        // 如果不喚醒的話。以後這兩個執行緒都會進入等待執行緒,沒有人喚醒。
        ms[index] = m;
        index++;
    }
 
    public synchronized Mantou pop() {
        while (index == 0) {//如果饅頭筐是空的;
            try {
                //如果饅頭筐是空的,就暫停此消費執行緒(因為沒什麼可消費的嘛)。
                this.wait();                //等生產執行緒生產完再來消費;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.notify();
        index--;
        return ms[index];
    }
}
 
class Shengchan extends Thread {// 生產者執行緒
    SyncStack ss = null;
 
    public Shengchan(SyncStack ss) {
        this.ss = ss;
    }
 
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("生產饅頭:" + i);
            Mantou m = new Mantou(i);
            ss.push(m);
        }
    }
}
 
class Xiaofei extends Thread {// 消費者執行緒;
    SyncStack ss = null;
 
    public Xiaofei(SyncStack ss) {
        this.ss = ss;
    }
 
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            @SuppressWarnings("unused")
			Mantou m = ss.pop();
            System.out.println("消費饅頭:" + i);
        }
    }
}

(4)生消模式應用情景:

  • 生產者和消費者共享同一個資源,並且生產者和消費者之間相互依賴,互為條件。
  • 對於生產者,沒有生產產品之前,消費者要進入等待狀態,而生產了產品之後,又需要馬上通知消費者消費。
  • 對於消費者,在消費之後,要通知生產者已經消費結束,需要繼續生產新產品以供消費。

(5)執行緒通訊: 在生產者消費者問題中,僅有synchronized是不夠,還需要不同執行緒之間的訊息傳遞(通訊)

  • 注意:以下方法均是Object類的方法,都只能在同步方法或者同步程式碼塊中使用,否則會丟擲異常

     è¡¨11-2 线ç¨é信常ç¨æ¹æ³.png

5、任務定時排程(Timer和Timetask

(1)程式碼

public class TestTimer {
    public static void main(String[] args) {
        Timer t1 = new Timer();//定義計時器;
        MyTask task1 = new MyTask();//定義任務;
        t1.schedule(task1,3000);  //3秒後執行;
        //t1.schedule(task1,5000,1000);//5秒以後每隔1秒執行一次!
        //GregorianCalendar calendar1 = new GregorianCalendar(2010,0,5,14,36,57); 
        //t1.schedule(task1,calendar1.getTime()); //指定時間定時執行; 
    }
}
 
class MyTask extends TimerTask {//自定義執行緒類繼承TimerTask類;
    public void run() {
        for(int i=0;i<10;i++){
            System.out.println("任務1:"+i);
        }
    }
}

(2)Timer

  • Timer類作用是定時或者每隔一定時間觸發一次執行緒。
  • Timer類本身實現的就是一個執行緒,只是這個執行緒是用來實現呼叫其它執行緒的。

(3)TimerTask

  • TimerTask類是一個抽象類,該類實現了Runnable介面,所以該類具備多執行緒的能力。
  • 繼承TimerTask使該類獲得多執行緒的能力,將需要多執行緒執行的程式碼書寫在run方法內部,然後通過Timer類啟動執行緒的執行

(3)說明:

  • 一個Timer可以啟動任意多個TimerTask實現的執行緒,但是多個執行緒之間會存在阻塞
  • 如果多個執行緒之間需要完全獨立的話,最好還是一個Timer啟動一個TimerTask實現