Java中多執行緒的概述、實現方式、執行緒控制、生命週期、多執行緒程式練習、安全問題的解決...

weixin_34120274發表於2016-11-01

多執行緒的概述

  • 程式
    • 正在執行的程式,是系統進行資源分配和呼叫的獨立單位。
    • 每一個程式都有它自己的記憶體空間和系統資源。


說起執行緒,它又分為單執行緒和多執行緒

  • 執行緒
  • 是程式中的單個順序控制流,是一條執行路徑
  • 一個程式如果只有一條執行路徑,則稱為單執行緒程式
  • 一個程式如果有多條執行路徑,則稱為多執行緒程式

多執行緒的實現(1)

如何實現多執行緒的程式呢?

  • 由於執行緒是依賴程式而存在的,所以我們應該先建立一個程式出來。而程式是由系統建立的,所以我們應該去呼叫系統功能建立一個程式。Java是不能直接呼叫系統功能的,所以,我們沒有辦法直接實現多執行緒程式。但是呢?Java可以去呼叫C/C++寫好的程式來實現多執行緒程式。由C/C++去呼叫系統功能建立程式,然後由Java去呼叫這樣的東西,然後提供一些類供我們使用。我們就可以實現多執行緒程式了。

  • 方式1:繼承Thread類

    • 步驟
      A:自定義類MyThread繼承Thread類。
      B:MyThread類裡面重寫run()
      C:建立物件
      D:啟動執行緒

下面我們就自定義一個MyThread類繼承Thread類啟動執行緒

public class MyThread extends Thread {

    @Override
    public void run() {
        // 自己寫程式碼
        // 一般來說,被執行緒執行的程式碼肯定是比較耗時的。所以我們用迴圈改進
        for (int x = 0; x < 100; x++) {
            System.out.println(x);
        }
    }
}
public class MyThreadDemo {
    public static void main(String[] args) {


        // 建立兩個執行緒物件
        MyThread my1 = new MyThread();
        MyThread my2 = new MyThread();

        my1.start();
        my2.start();
    }
}

這樣我們就建立並啟動了兩個執行緒
start()方法:首先啟動了執行緒,然後再由jvm去呼叫該執行緒的run()方法。
那麼,我們在繼承Thread類之後,為什麼要重寫run()方法呢?

  • 因為不是類中的所有程式碼都需要被執行緒執行的。而這個時候,為了區分哪些程式碼能夠被執行緒執行,java提供了Thread類中的run()用來包含那些被執行緒執行的程式碼。

獲取和設定執行緒名稱

  • Thread類的基本獲取和設定方法
    • public final String getName():獲取執行緒的名稱。
    • public final void setName(String name):設定執行緒的名稱
public class MyThread extends Thread {

    public MyThread() {
    }

    public MyThread(String name){
        super(name);
    }

    @Override
    public void run() {
        for (int x = 0; x < 100; x++) {
            System.out.println(getName() + ":" + x);
        }
    }
}
public class MyThreadDemo {
    public static void main(String[] args) {
        // 建立執行緒物件
        //無參構造+setXxx()
         MyThread my1 = new MyThread();
         MyThread my2 = new MyThread();
         //呼叫方法設定名稱
         my1.setName("阿杜");
         my2.setName("杜鵬程");
         my1.start();
         my2.start();

        //帶參構造方法給執行緒起名字
        // MyThread my1 = new MyThread("阿杜");
        // MyThread my2 = new MyThread("杜鵬程");
        // my1.start();
        // my2.start();

        //我們可以使用無參構造的方法,也可以使用帶參構造的方法
    }
}

但是我們要獲取main方法所在的執行緒物件的名稱,該怎麼辦呢?
遇到這種情況,Thread類提供了一個很好玩的方法:
public static Thread currentThread():返回當前正在執行的執行緒物件
System.out.println(Thread.currentThread().getName());
這句話如果在main中執行,就會輸出main。會返回當前執行的執行緒物件

執行緒控制

  • public static void sleep(long millis):執行緒休眠
  • public final void join():執行緒加入
  • public static void yield():執行緒禮讓
  • public final void setDaemon(boolean on):後臺執行緒
  • public final void stop():中斷執行緒
  • public void interrupt():中斷執行緒

public static void sleep(long millis):執行緒休眠

public class ThreadSleep extends Thread {
    @Override
    public void run() {
        for (int x = 0; x < 100; x++) {
            System.out.println(getName() + ":" + x + ",日期:" + new Date());
            // 睡眠1秒鐘
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class ThreadSleepDemo {
    public static void main(String[] args) {
        ThreadSleep ts1 = new ThreadSleep();
        ThreadSleep ts2 = new ThreadSleep();

        ts1.setName("阿杜");
        ts2.setName("杜鵬程");

        ts1.start();
        ts2.start();
    }
}

public final void join():執行緒加入,等待該執行緒終止

public class ThreadJoin extends Thread {
    @Override
    public void run() {
        for (int x = 0; x < 100; x++) {
            System.out.println(getName() + ":" + x);
        }
    }
}
public class ThreadJoinDemo {
    public static void main(String[] args) {
        ThreadJoin tj1 = new ThreadJoin();
        ThreadJoin tj2 = new ThreadJoin();
        ThreadJoin tj3 = new ThreadJoin();

        tj1.setName("中秋節");
        tj2.setName("國慶節");
        tj3.setName("聖誕節");

        tj1.start();
        try {
            tj1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        tj2.start();
        tj3.start();
    }
}

執行程式,我們發現名字為中秋節的執行緒走完了之後才開始走下面的兩個執行緒。
給那個執行緒用這個方法就是等待該執行緒終止後,再繼續執行接下來的執行緒。

public static void yield():執行緒禮讓

public class ThreadYield extends Thread {
    @Override
    public void run() {
        for (int x = 0; x < 100; x++) {
            System.out.println(getName() + ":" + x);
            Thread.yield();
        }
    }
}
public class ThreadYieldDemo {
    public static void main(String[] args) {
        ThreadYield ty1 = new ThreadYield();
        ThreadYield ty2 = new ThreadYield();

        ty1.setName("阿杜");
        ty2.setName("杜鵬程");

        ty1.start();
        ty2.start();
    }
}

這個方法暫停當前正在執行的執行緒物件,並執行其他執行緒。
讓多個執行緒的執行更和諧,但是不能靠它保證一人一次。

public final void setDaemon(boolean on):守護執行緒

public class ThreadDaemon extends Thread {
    @Override
    public void run() {
        for (int x = 0; x < 100; x++) {
            System.out.println(getName() + ":" + x);
        }
    }
}
public class ThreadDaemonDemo {
    public static void main(String[] args) {
        ThreadDaemon td1 = new ThreadDaemon();
        ThreadDaemon td2 = new ThreadDaemon();

        td1.setName("關羽");
        td2.setName("張飛");

        // 設定守護執行緒
        td1.setDaemon(true);
        td2.setDaemon(true);

        td1.start();
        td2.start();

        Thread.currentThread().setName("劉備");
        for (int x = 0; x < 5; x++) {
            System.out.println(Thread.currentThread().getName() + ":" + x);
        }
    }
}

執行程式可以看到,當劉備執行完5次後,張飛和關於也會執行完,並不會執行100次。
將該執行緒標記為守護執行緒或使用者執行緒。
當正在執行的執行緒都是守護執行緒時,Java 虛擬機器退出。
該方法必須在啟動執行緒前呼叫。

**public final void stop():中斷執行緒 **
public void interrupt():中斷執行緒

這兩個方法都是中斷執行緒的意思,但是他們還是有區別的,我們來一起研究一下

public class ThreadStop extends Thread {
    @Override
    public void run() {
        System.out.println("開始執行:" + new Date());

        // 我要休息10秒鐘,親,不要打擾我哦
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            // e.printStackTrace();
            System.out.println("執行緒被終止了");
        }

        System.out.println("結束執行:" + new Date());
    }
}

public class ThreadStopDemo {
    public static void main(String[] args) {
        ThreadStop ts = new ThreadStop();
        ts.start();

        // 你超過三秒不醒過來,我就乾死你
        try {
            Thread.sleep(3000);
//           ts.stop();
            ts.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

我們分別執行stop()方法和interrupt()方法。
我們可以發現stop()方法執行後,該執行緒就停止了,不再繼續執行了
但是interrupt()方法執行後,它會終止執行緒的狀態,還會繼續執行run方法裡面的程式碼。

執行緒的生命週期圖

1835466-287b0601c4d7a591.png
執行緒的生命週期圖.png

多執行緒的實現(2)

  • 方式2:實現Runnable介面
    • 步驟:
      • A:自定義類MyRunnable實現Runnable介面
      • B:重寫run()方法
      • C:建立MyRunnable類的物件
      • D:建立Thread類的物件,並把C步驟的物件作為構造引數傳遞
public class MyRunnable implements Runnable {

    @Override
    public void run() {
        for (int x = 0; x < 100; x++) {
            // 由於實現介面的方式就不能直接使用Thread類的方法了,但是可以間接的使用
            System.out.println(Thread.currentThread().getName() + ":" + x);
        }
    }
}
public class MyRunnableDemo {
    public static void main(String[] args) {
        // 建立MyRunnable類的物件
        MyRunnable my = new MyRunnable();

        // 建立Thread類的物件,並把C步驟的物件作為構造引數傳遞

        // Thread(Runnable target, String name)
        Thread t1 = new Thread(my, "阿杜");
        Thread t2 = new Thread(my, "杜鵬程");

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

這樣我們就實現了多執行緒的第二種啟動方式

多執行緒程式練習

某電影院目前正在上映賀歲大片,共有100張票,而它有3個售票視窗售票,請設計一個程式模擬該電影院售票。

我們分別用兩種實現多執行緒的方法來完成這個需求
1.繼承Thread類來實現

public class SellTicket extends Thread {

    // 定義100張票
    private static int tickets = 100;

    @Override
    public void run() {
        // 定義100張票
        // 每個執行緒進來都會走這裡,這樣的話,每個執行緒物件相當於買的是自己的那100張票,這不合理,所以應該定義到外面
        // int tickets = 100;

        // 是為了模擬一直有票
        while (true) {
            if (tickets > 0) {
                System.out.println(getName() + "正在出售第" +(tickets--) + "張票");
            }
        }
    }
}
public class SellTicketDemo {
    public static void main(String[] args) {
        // 建立三個執行緒物件
        SellTicket st1 = new SellTicket();
        SellTicket st2 = new SellTicket();
        SellTicket st3 = new SellTicket();

        // 給執行緒物件起名字
        st1.setName("視窗1");
        st2.setName("視窗2");
        st3.setName("視窗3");

        // 啟動執行緒
        st1.start();
        st2.start();
        st3.start();
    }
}

這樣我們就實現了三個視窗同時在出售這100張票的多執行緒程式

2.實現Runnable介面的方式實現

public class SellTicket implements Runnable {
    // 定義100張票
    private int tickets = 100;

    @Override
    public void run() {
        while (true) {
            if (tickets > 0) {
                System.out.println(Thread.currentThread().getName() + "正在出售第"+ (tickets--) + "張票");
            }
        }
    }
}

public class SellTicketDemo {
    public static void main(String[] args) {
        // 建立資源物件
        SellTicket st = new SellTicket();

        // 建立三個執行緒物件
        Thread t1 = new Thread(st, "視窗1");
        Thread t2 = new Thread(st, "視窗2");
        Thread t3 = new Thread(st, "視窗3");

        // 啟動執行緒
        t1.start();
        t2.start();
        t3.start();
    }
}

我們這個電影院售票程式,從表面上看不出什麼問題,但是在真實生活中,售票時網路是不能實時傳輸的,總是存在延遲的情況,所以,在出售一張票以後,需要一點時間的延遲,所以我們每次賣票延遲100毫秒

while (true) {
            if (tickets > 0) {
                // 為了模擬更真實的場景,我們稍作休息
                try {
                    Thread.sleep(100); 
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在出售第"+ (tickets--) + "張票");
            }
        }
    }

這樣我們模擬真是場景,稍作休息了,可是執行程式後,還是會出現下面兩個問題。

  • 相同的票出現多次
    - CPU的一次操作必須是原子性的
  • 還出現了負數的票
    - 隨機性和延遲導致的

這裡就牽扯到了執行緒的安全問題,執行緒安全問題在理想狀態下,不容易出現,但一旦出現對軟體的影響是非常大的。

多執行緒安全問題

如何解決多執行緒安全問題呢?

  • 把多個語句操作共享資料的程式碼給鎖起來,讓任意時刻只能有一個執行緒執行即可。

解決執行緒安全問題實現(1)

  • 同步程式碼塊
    • 格式:
      • synchronized(物件){ 需要同步的程式碼; }
    • 同步可以解決安全問題的根本原因就在那個物件上。該物件如同鎖的功能。

我們多上面售票的程式碼進行改進

public class SellTicket implements Runnable {
    // 定義100張票
    private int tickets = 100;
    //建立鎖物件
    private Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (tickets > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()
                            + "正在出售第" + (tickets--) + "張票");
                }
            }
        }
    }
}

我們只要運用同步程式碼塊的格式來解決執行緒的問題就可以,主要就是這裡的物件,必須使用的是同一個鎖物件。
所以我們可以來總結一下同步的特點

同步的特點

  • 同步的前提
    • 多個執行緒
    • 多個執行緒使用的是同一個鎖物件
  • 同步的好處
    • 同步的出現解決了多執行緒的安全問題。
  • 同步的弊端
    • 當執行緒相當多時,因為每個執行緒都會去判斷同步上的鎖,這是很耗費資源的,無形中會降低程式的執行效率。

解決執行緒安全問題實現(2)

我們 還有一種方法可以解決多執行緒的安全問題
同步方法:就是把同步的關鍵字加到方法上

private synchronized void sellTicket() {
            if (tickets > 0) {
            try {
                    Thread.sleep(100);
            } catch (InterruptedException e) {
                    e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()
                        + "正在出售第" + (tickets--) + "張票 ");
            }
    }

我們只要呼叫這個方法就可以了
我們也可以讓此方法為靜態的方法

private static synchronized void sellTicket() {
        if (tickets > 0) {
        try {
                Thread.sleep(100);
        } catch (InterruptedException e) {
                e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()
                    + "正在出售第" + (tickets--) + "張票 ");
        }
    }

我們要來總結一下,同步程式碼塊的鎖物件可以時任意物件。
但是,當把同步關鍵字加在方法上,它的物件是this
當此方法為精態方法時,它的物件是類的位元組碼檔案物件,也就是 類名.class

相關文章