Java基礎—多線程(二)

多線程(二)

一、線程間通信

    1.定義
      線程間通信就是多個線程操作同一資源,但是操作的動作不同。

    2.等待喚醒機制
      等待喚醒機制,是由wait()notify()notifyAll()等方法組成。對於有些資源的操作,需要一個線程完成一步,進入等待狀態,將CPU執行權交由另一個線程,讓它完成下一步的操作,如此交替進行。這個過程中,一個線程需要在完成一步操作後,先通知(notify())另一個線程運行,再等待(wait()),進入凍結狀態,以此類推。等待中的線程,都儲存在系統線程池中,等待這被notify()喚醒。

以下代碼,通過等待喚醒機制,實現了生產一個披薩,消費一個披薩:

package com.heisejiuhuche;

public class ProductionConsumptionModel {
    public static void main(String[] args) {
        Pizza pizza = new Pizza();

        new Thread(new Producer(pizza)).start();
        new Thread(new Consumer(pizza)).start();
    }
}

class Pizza {
    private String pizza;
    private int count = 1;
    //包子存在與否的旗標,false代表沒有pizza,true代表有
    private boolean flag = false;

    public synchronized void producePizza(String pizza) {
        if (flag) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.pizza = pizza + "---" + count++;
        System.out.println(Thread.currentThread().getName() + "-生產-----"
                + this.pizza);
        flag = true;
        notify();
    }

    public synchronized void consumePizza() {
        //如果沒有pizza,則執行生產包子的代碼
        if (!flag) {
            try {
                //如果有pizza,則線程等待
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName()
                + "-消費--------------" + this.pizza);
        //如果沒有pizza,生產完之後,將flag設爲true
        flag = false;
        //線程進入凍結狀態之前,通知另一線程開始啓動,消費pizza
        notify();
    }
}

class Producer implements Runnable {
    private Pizza pizza;

    Producer(Pizza pizza) {
        this.pizza = pizza;
    }

    public void run() {
        while (true) {
            pizza.producePizza("pizza");
        }
    }
}

class Consumer implements Runnable {
    private Pizza pizza;

    Consumer(Pizza pizza) {
        this.pizza = pizza;
    }

    public void run() {
        while (true) {
            pizza.consumePizza();
        }
    }
}

程序運行部分結果如下:

Thread-1-消費--------------pizza---20609
Thread-0-生產-----pizza---20610
Thread-1-消費--------------pizza---20610
Thread-0-生產-----pizza---20611
Thread-1-消費--------------pizza---20611

    3.Object類中的wait等方法
      wait()等多線程同步等待喚醒機制中的方法,被定義在Object類中是因爲:
      首先,在等待喚醒機制中,無論是等待操作,還是喚醒操作,都必須標識出等待的這個線程和被喚醒的這個線程鎖持有的鎖;表現爲代碼是:鎖.wait();鎖.notify();而這個鎖,由synchronized關鍵字格式可知,可以是任意對象;那麼,可以被任意對象調用的方法,一定是定義在了Object類當中。wait()notify()notifyAll()這些方法都被定義在了Object類中,因爲這些方法是要使用在多線程同步的等待喚醒機制當中,必須具備能被任意對象調用的特性。所以,這些方法要被定義在Object類中。

    4、生產者消費者模型
      在實際生產時,會有多個線程負責生產,多個線程負責消費;那麼在上述代碼中啓動新線程,來模擬多線程生產消費的情況。

示例代碼:

package com.heisejiuhuche;

public class ProductionConsumptionModel {
    public static void main(String[] args) {
        Pizza pizza = new Pizza();

        //兩個線程負責生產,兩個線程負責消費
        new Thread(new Producer(pizza)).start();
        new Thread(new Producer(pizza)).start();
        new Thread(new Consumer(pizza)).start();
        new Thread(new Consumer(pizza)).start();
    }
}

用這樣的方式,運行會出現如下結果:

Thread-0-生產-----pizza---198
Thread-1-生產-----pizza---199
Thread-2-消費--------------pizza---199

生產了兩個披薩,但只消費了一個。現在0,1線程負責生產,2,3線程負責消費,原因推斷:
1)當0線程生產完一個披薩,進入凍結;
2)1線程判斷有披薩,進入凍結;
3)2線程消費一個披薩,喚醒0線程,進入凍結;
4)3線程判斷沒披薩,進入凍結;
5)現在出於運行狀態的只有0線程,0線程生產一個披薩,喚醒1線程(1線程是線程池中第一個線程),進入凍結;
6)1線程又生產了一個披薩

這導致了生產兩個,只消費一個的問題。這個問題的發生是因爲,第50線程喚醒1線程的時候,由於1線程的等待代碼在if語句中,1線程醒了之後,不需要再判斷flag的值所導致。如果1線程被喚醒,還要繼續判斷flag的值,就不會產生這個情況。因此,要將if判斷,改爲while循環,讓線程被喚醒之後,再次判斷flag的值。

示例代碼:

while (flag) {
    try {
        wait();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

每次被喚醒,都要判斷flag的值。代碼運行結果如下:

Thread-0-生產-----pizza---1
Thread-2-消費--------------pizza---1
Thread-0-生產-----pizza---2
Thread-3-消費--------------pizza---2

程序出現了無響應,因爲使用while循環,可能會出現所有線程全部進入凍結狀態的情況。要解決這個問題,必須用到另一個方法notifyAll();喚醒所有線程。由於用了while循環,所有線程被喚醒之後第一件事是判斷flag的值,所以不會再出現多生產或多消費問題。至此,程序運行正常。

示例代碼:

public synchronized void consumePizza() {
    while(!flag) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName()
                + "-消費--------------" + this.pizza);
        //如果沒有pizza,生產完之後,將flag設爲true
        flag = false;
        //線程進入凍結狀態之前,喚醒所有其他線程
        notifyAll();
    }
}

程序運行部分結果:

Thread-2-消費--------------pizza---198
Thread-0-生產-----pizza---199
Thread-3-消費--------------pizza---199
Thread-1-生產-----pizza---200
Thread-3-消費--------------pizza---200

二、jdk5新特性

    1.概述
      jdk5開始,提供了多線程同步的升級解決方案。將synchronized關鍵字,替換成Lock接口;將Object對象,替換爲Condition對象;將wai()notify()notifyAll()方法,替換爲await()signal()signalAll()方法。一個鎖,可以對應多個Condition對象。這個特性的出現,可以讓多線程在喚醒其他線程時,不必喚醒本方的線程,只喚醒對方線程。例如在生產者消費者模型中,使用LockCondition類,可以實現只喚醒消費者線程,或只喚醒生產者線程。

    2.Lock接口和Condition接口
      1)Lock接口已知實現類中,有ReentrantLock類。這個子類可以用來實例化,創建ReentrantLock對象

ReentrantLock lock = new ReentrantLock();

      2)Condition接口的實例可以通過newCondition()方法獲得

Condition conditon = Lock.newCondition();

      3)一個Lock對象可以對應多個Condition對象

Condition condition1 = Lock.newCondition();
Condition condition2 = Lock.newCondition();

    3.新特性應用
      將此新特性應用在消費者生產者模型中,實現只喚醒對方線程。

修改之後的Pizza類代碼如下:

class Pizza {
    private String pizza;
    private int count = 1;
    private boolean flag = false;

    //獲取Lock和Condition對象
    private final ReentrantLock lock = new ReentrantLock();
    //分別指定生產者和消費者的Condition對象
    private final Condition conditionPro = lock.newCondition();
    private final Condition conditionCon = lock.newCondition();

    public void producePizza(String pizza) {
        //上鎖
        lock.lock();
        try {
            while (flag) {
                //如果有披薩,線程凍結
                conditionPro.await();
            }
            this.pizza = pizza + "---" + count++;
            System.out.println(Thread.currentThread().getName() + "-生產-----"
                    + this.pizza);
            flag = true;
            //只喚醒消費者線程中的一個
            conditionCon.signal();
        } catch(InterruptedException e) {
            e.printStackTrace();
        } finally {
            //這是一定要執行的代碼,解鎖
            lock.unlock();
        }

    }

    public void consumePizza() {
        lock.lock();
        try {
            while(!flag) {
                conditionCon.await();
            }
            System.out.println(Thread.currentThread().getName()
                    + "-消費--------------" + this.pizza);
            flag = false;
            conditionPro.signal();
        } catch(InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

分別創建的conditionProconditionCon對象,用於實現只喚醒對方線程,代碼更優。

三、停止線程

    1.線程停止原理
      stop()方法已經過時,停止的唯一標準就是run()方法結束。開啓多線程運行,運行代碼通常都是循環結構,只要控制住循環,就可以讓run()方法結束,就可以讓線程結束。

注意:
當線程處於凍結狀態,無法讀取控制循環的標記,線程就不會結束。

    2.interrupt()方法
      將處於凍結狀態的線程,強制恢復到運行狀態。interrupt()方法是在清除線程的凍結狀態。

示例代碼:

package com.heisejiuhuche;

public class InterruptTest {
    public static void main(String[] args) {
        int x = 0;

        Interrupt inter = new Interrupt();

        Thread t1 = new Thread(inter);
        Thread t2 = new Thread(inter);

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

        while(true) {
            System.out.println(Thread.currentThread().getName() + "run....");
            if(x++ == 60) {
                //強制t1 t2恢復運行狀態,拋出異常
                t1.interrupt();
                t2.interrupt();
                break;
            }
        }
        System.out.println("over");
    }
}

class Interrupt implements Runnable {
    //循環控制變量
    private boolean flag = true;

    public synchronized void run() {
        while(flag) {
            try {
                //讓t1 t2進入凍結狀態
                this.wait();
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + "Interrupt Exception.....");
                //處理完異常,改變flag的值,下次判斷時,結束循環
                changeFlag();
            }
            System.out.println(Thread.currentThread().getName() + " Interrupt run.....");
        }
    }

    public void changeFlag() {
        flag = false;
    }
}

如果不調用t1t2線程的interrupt()方法,程序會無響應,因爲兩個線程都處於凍結狀態,無法繼續運行。

上述程序運行結果:

mainrun....
over
Thread-1Interrupt Exception.....
Thread-1 Interrupt run.....
Thread-0Interrupt Exception.....
Thread-0 Interrupt run.....

四、Thread類其他方法

    1.setDaemon()方法
      將該線程標記爲守護線程或用戶線程。當正在運行的線程都是守護線程時,Java虛擬機退出。該方法必須在啓動線程前調用。守護線程可以理解爲後臺線程。後臺線程開啓後,會和前臺線程(一般線程)一起搶奪CPU資源;當所有前臺線程結束運行後,後臺線程自動結束。可以理解爲,後臺線程依賴前臺線程的運行。

示例代碼:

package com.heisejiuhuche;

public class InterruptTest {
    public static void main(String[] args) {
        int x = 0;

        Interrupt inter = new Interrupt();

        Thread t1 = new Thread(inter);
        Thread t2 = new Thread(inter);

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

在啓動兩個線程前,將兩個線程設置爲守護線程,其他代碼不變;那麼這兩個線程依賴主線程運行;雖然這兩個線程都處於凍結狀態,但是當主線程運行完畢,這兩個守護進程隨之結束。

    2.join()方法
      調用join()方法的線程,在申請CPU執行權。之前擁有CPU執行權的線程,將轉入凍結狀態,等調用join()方法的線程執行完畢,再轉回運行狀態。

示例代碼:

package com.heisejiuhuche;

public class JoinTest {
    public static void main(String[] args) {
        Join j = new Join();

        Thread t1 = new Thread(j);
        Thread t2 = new Thread(j);

        t1.start();
        try {
            //主線程將CPU執行權交給t1線程,自己轉入凍結
            //等待t1線程執行完畢,主線程再運行
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();

        for(int x = 0; x < 100; x++) {
            System.out.println(Thread.currentThread().getName() + "***" + x);
        }
    }
}

class Join implements Runnable {
    public void run() {
        for(int x = 0; x < 100; x++) {
            System.out.println(Thread.currentThread().getName() + "---" + x);
        }
    }
}

程序在啓動t1線程之後,主線程先等待t1線程打印完100個數;主線程再繼續和t2線程交替打印100個數。

    3.yield()方法
      調用yield()方法的線程,會臨時釋放執行權,可以達到線程均衡運行的效果。

示例代碼:

package com.heisejiuhuche;

public class YieldTest {
    public static void main(String[] args) {
        Yield j = new Yield();

        Thread t1 = new Thread(j);
        Thread t2 = new Thread(j);

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

        for(int x = 0; x < 100; x++) {
            System.out.println(Thread.currentThread().getName() + "***" + x);
        }
    }
}

class Yield implements Runnable {
    public void run() {
        for(int x = 0; x < 100; x++) {
            System.out.println(Thread.currentThread().getName() + "---" + x);
            Thread.yield();
        }
    }
}

程序運行部分結果:

Thread-1---51
main***79
Thread-0---46
main***80
Thread-1---52
main***81
Thread-0---47

三個線程均衡執行。

五、多線程開發應用

    多線程應用在程序中的運算需要同時進行的時候,可以提高程序運行的效率。例如,main()方法中有三個循環需要執行,如果是單線程,第二個循環要等待第一個循環執行完才能執行,第三個循環要等第二個循環執行完,如此一來,程序運行效率低下。此時,就可以運用多線程,讓三個循環同時運行。

示例代碼:

package com.heisejiuhuche;

public class ThreadApplycation {
    public static void main(String[] args) {
        //主線程執行
        for(int x = 0; x < 100; x ++) {
            System.out.println("Main thread running ...");
        }

        //匿名線程執行
        new Thread() {
            public void run() {
                for(int x = 0; x < 100; x++) {
                    System.out.println("Anonymous thread running ...");
                }
            }
        }.start();

        //線程r執行
        Runnable r = new Runnable() {
            public void run() {
                for(int x = 0; x < 100; x ++) {
                    System.out.println("r thread running ...");
                }
            }
        };
        new Thread(r).start();
    }
}

讓主線程,匿名線程和r線程,同時開始運算。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章