黑馬程序員_Java基礎_線程間通信,生產者消費者案例,jdk1.5鎖機制,守護線程

 一,線程間的通信

1,在線程中我們使用多線程進行操作的時候,都是對同一個資源進行相同的操作。如果我們想要通過多線程操作某一個資源但是操作的動作都不相同的時候,這時候就會有兩個run方法。那麼這兩個run方法就代表不同的動作,也就是有不同的線程,那麼怎樣讓着兩個線程之間的同步是安全的呢?方法當然是加鎖。

我們先來看這個程序的需求:一個人在往系統中輸入一個人的姓名和性別,另一個人在輸出該系統中的人的姓名和性別;

分析:他們同時在對該系統進行不同的操作,每個人的動作算是一個線程,兩個人在操作就是有兩個線程,而這兩個人的輸入和輸出又有兩個動作,輸入的人要輸入姓名,還要輸入性別,而輸出的人同樣要輸出姓名和性別;

我們加鎖後只是保證了在輸入的那一個人沒有輸入完之前,輸出的那個人不能輸出,當只輸入了一個人的姓名和性別,輸出的人開始輸出了,這時候他可能獲取的CPU時間較長,就會不斷輸出一大塊,出入的人同樣也是這樣。我們的要求還有一個就是,當系統沒有輸入人的信息的時候,輸出的人不能輸出,當輸出的人輸出數據後系統裏的信息就沒有在輸出的必要了,這時候需要告訴輸入的人重新輸入。這就要用到線程通信的等待喚醒機制。

 

2,等待喚醒機制:當兩個不同的線程進行對同一資源進行操作的時候,他們之間是線程同步的,一個線程對資源操作完之後,喚醒另一個線程,讓另一個線程進行資源操作,這時候這個線程就需要讓自己處於凍結狀態,讓另一個線程操作資源,另一個線程操作完之後,喚醒前一個線程,讓自己處於凍結狀態,同時鎖也釋放了。

等待喚醒機制中的等待是通過wait()方法完成的,喚醒是通過notifynotifyAll方法完成的。

wait;

notify();喚醒的是單個等待的線程。

notifyall();喚醒的是多個等待的線程;

 

這幾個都是用在同步中,到時必須對持有監視器(鎖)的線程進行操作,

所以要使用在同步中,只有線程中財存在鎖;

 

爲什麼在這些對線程的操作要定義在Object中呢?

因爲這些方法操作同步中的線程時,必須都要標示他們所操作線程持有的鎖,只有同一個鎖上的被等待線程,可以被同一個鎖上的notify喚醒,不可以對持有不同鎖的進程喚醒;

也就是說等待和喚醒必須是同一個鎖,而鎖可以是任意對象,所以可以被任意對象調用的方法是存在於Object中;


class Res
{
    String name;
    String sex;
    boolean flag = false;
}
class Input implements Runnable
{
    private Res r;
    //Object obj = new Object();
    Input(Res r) {
        this.r = r;
    }
    public void run() {
        int x = 0;
        while(true) {
            //synchronized(obj){    如果按照這種方式加鎖,我們會發現很明顯這是兩個不同的鎖
            synchronized(r) {     //這裏不能用超類的子類是因爲這樣做事兩個不同的對象,所以
                                //該對象必須是同一個對象,這兩個鎖才能實現同步,所以可以用字
                               //節碼文件代替,如Input.class,Output.class,但是必須一致;也可以是
                              //公共類r,i,o;
                if(r.flag)
                    try{r.wait();}catch(Exception e){}
                if(x==0) {
                    r.name = "Mike";
                    r.sex = "man";
                }
                else {
                    r.name = "李紅";
                    r.sex = "女";
                }
                x = (x+1)%2;
                r.flag = true;
                r.notify();             }
            //}
        }
    }
}
class Output implements Runnable
{
    private Res r;
    //Object obj = new Object();
    Output(Res r) {
        this.r = r;
    }
    public void run() {
        while(true) {
            //synchronized(obj) {
                synchronized(r) {
                    if(!r.flag)
                    try{r.wait();}catch(Exception e){}
                        System.out.println(r.name + "++++++++++++" + r.sex);
                        r.flag = false;
                        r.notify();
                }
            //}
        }
    }
}
class InputAndOutput
{
    public static void main(String[] args) {
        Res r = new Res();
        Input i = new Input(r);
        Output o = new Output(r);
        Thread t1 = new Thread(i);
        Thread t2 = new Thread(o);
        t1.start();
        t2.start();
    }
}

    縱觀上述程序發現,程序的封裝性並不是太好,類Res裏面的屬性應該私有化,性別和姓名也應該通過Res類來設置,所以該代碼還應該優化,優化代碼如下:

class Res
{
    private String name;
    private String sex;
    boolean flag = false;
    public synchronized void set(String name,String sex) {
        if(flag)
            try{this.wait();}catch(Exception e){}
        this.name = name;
        this.sex = sex;
        flag = true;
        this.notify();
    }
//兩個Synchronized的鎖都是Res對象的鎖,也就是this的鎖
    public synchronized void print() {
        if(!flag)
            try{this.wait();}catch(Exception e){}
        System.out.println(name + "++++++++++++" + sex);
        flag = false;
        this.notify();
    }
}
class Input implements Runnable
{
    private Res r;
    Input(Res r) {
        this.r = r;
    }
    public void run() {
        int x = 0;
        while(true) {
            if(x==0) {
                r.set("Mike","man");
            }
            else {
                r.set("李紅","女");
            }
            x = (x+1)%2;
        }
    }
}
class Output implements Runnable
{
    private Res r;
    Output(Res r) {
        this.r = r;
    }
    public void run() {
        while(true) {
            r.print();
        }
    }
}
class InputAndOutput
{
    public static void main(String[] args) {
        Res r = new Res();
        //創建匿名對象
        new Thread(new Input(r)).start();
        new Thread(new Output(r)).start();
    }
}


二,生產者消費者問題是一個線程應用的經典例子,需要嚴格掌握。

    下面是使用jdk1.4之前的線程同步與通信的方法,以及等待喚醒機制。

    具體代碼步驟分析見代碼註釋部分。 
//創建一個資源,也就是生產者和消費者操作的資源
class Resource
{
    //定義產品的名稱,記錄操作第幾個產品,以及一個判斷標記字段。
    //判斷標記用於判斷是進行生產操作還是消費操作
    private String name;
    private int count = 1;
    private boolean flag = false;
    //定義產品的被生產屬性
    public synchronized void set(String name) {
        while(flag) //判斷是否生產了商品,如果存在商品沒有被消費則生產者等待。還讓讓被喚醒的線程再一次判斷標記。            try{this.wait();}catch (Exception e){}
        //否則生產商品,並打印出結果,將flag置爲真,表示生產了商品,沒被消費,並且喚醒等待的線程讓消費者消費
        this.name = name + "---" + count++ ;
        System.out.println(Thread.currentThread().getName() + "生產者" + this.name);
        flag = true;
        this.notifyAll();
    }
    //定義善品的被消費屬性
    public  synchronized void get() {
        while(!flag)  //如果不存在商品,則消費者sleep等待生產者生產
            try{this.wait();}catch (Exception e){}
        //否則,如果有生產好的商品需要消費,則消費者消費掉該商品。並將標誌位置爲false,並喚醒生產者生產
        System.out.println(Thread.currentThread().getName() + "消費者" + this.name);
        flag = false;
        this.notifyAll();
    }
}
//定義生產者線程,覆蓋run方法,生產一個商品
class Producer implements Runnable
{
    private Resource res;
    Producer(Resource res) {
        this.res = res;
    }
    public void run() {
        while(true) {
            res.set("商品");
        }
    }
}
//定義一個消費者線程,覆蓋run方法,消費生產好的商品
class Consumer implements Runnable
{
    private Resource res;
    Consumer(Resource res) {
        this.res = res;
    }
    public void run() {
        while(true) {
            res.get();
        }
    }
}
public class ProducerAndConsumer
{
    public static void main(String[] args) {
        Resource res = new Resource();
        //創建生產者消費者線程,生產,消費產品
        new Thread(new Producer(res)).start();
        new Thread(new Consumer(res)).start();
    }
}

三,JDK1.5 中提供了多線程升級解決方案。

將同步synchronized替換成實現Lock操作;將Object中的wait,notify,notifyAll替換成Condition,該對象可以Lock鎖進行獲取。這樣做僅僅是把synchronized換成了Lock,把wait和notify換成了condition,但是會喚醒自己這一方的問題還是沒有解決,和之前用synchronized,wait,notify沒什麼區別。

 

而Lock存在的意義就是避免notifyAll的時候喚醒自己這一方等待的線程,

只喚醒對方等待的線程;

該例中實現了本方只喚醒對方的操作;


import java.util.concurrent.locks.*;
class Resource
{
    private String name;
    private int count = 1;
    private boolean flag = false;
    private Lock lock = new ReentrantLock();
    private Condition condition_pro = lock.newCondition();//一個鎖上可以有多個condition
    private Condition condition_cons = lock.newCondition();//一個鎖上可以有多個condition
    public void set(String name) throws InterruptedException {
        lock.lock();//顯示的給代碼塊上鎖,讓程序員知道具體在哪裏上的鎖
        try
        {
            while(flag)
                condition_pro.await();//
            this.name = name + "---" + count++ ;
            System.out.println(Thread.currentThread().getName() + "生產者" + this.name);
            flag = true;
            condition_cons.signalAll();
        }
        finally {
            lock.unlock();
        }
    }
    public  void get() throws InterruptedException {
        lock.lock();
            try
            {
                while(!flag)
                    condition_cons.await();
                System.out.println(Thread.currentThread().getName() + "消費者" + this.name);
                flag = false;
                condition_pro.signalAll();
            }
            finally {
                lock.unlock();
            }
    }
}
class Producer implements Runnable
{
    private Resource res;
    Producer(Resource res) {
        this.res = res;
    }
    public void run() {
        while(true) {
            try
            {
                res.set("商品");
            }catch(InterruptedException e) {}
        }
    }
}
class Consumer implements Runnable
{
    private Resource res;
    Consumer(Resource res) {
        this.res = res;
    }
    public void run() {
        while(true) {
            try{
                res.get();
            }catch (InterruptedException e){}
        }
    }
}
public class ProducerAndConsumer
{
    public static void main(String[] args) {
        Resource res = new Resource();
        new Thread(new Producer(res)).start();
        new Thread(new Consumer(res)).start();
    }
}


面試:生產者消費者有什麼替代方案?

1.5版本以後提供了顯式的鎖機制,以及顯式的所對象上的等待喚醒操作機制,同時將等待喚醒進行了封裝,一個鎖可以對應多個condition,沒有之前一個鎖對應一個wait和notify;如果想再有一個wait和notify,必須要在建一個鎖同步,而鎖的增加可能會引起死鎖,而1.5以後想當於 一個鎖可以有多個wait和notify。

 

四,線程的停止方法。

線程的停止方法不能直接使用stop方法,因爲stop方法已經過時了。

停止線程的原理是終止run方法,run方法中運行的是線程的運行代碼,只要讓run方法終止就能停止線程了。

開啓多線程的運行通常代碼是循環結構的,只要控制循環就可以讓run方法結束。

 

特殊情況:當線程處於凍結狀態的時候,就不會讀取到標記,那麼run方法不會結束,線程也不會結束當沒有指定的方法讓凍結的線程恢復到運行狀態時,這時候需要對凍結進行清除。就是使用interrupt()方法。當線程調用該方法的時候,會拋出InterruptedException異常,對該異常進行try。。。catch處理,處理代碼塊中將標誌置爲false就可以結束while循環,從而也就結束了run方法。

示例代碼:
public class ThreadStopDemo {
    public static void main(String[] args) {
        ThreadDem td = new ThreadDem();
        Thread t1 = new Thread(td);
        Thread t2 = new Thread(td);
        t1.start();
        t2.start();
        
        int num = 0;
        //主線程中的運行代碼
        while(true) {
            if(num++ == 60) {
                //td.changFlag();
                t1.interrupt();//強制中斷處於凍結狀態的線程
                t2.interrupt();
                break;
            }
            System.out.println(Thread.currentThread().getName() + "...main" + num);
        }
    }
}
class ThreadDem implements Runnable {
    private boolean flag = true;
    public synchronized void run() {
        while(flag) {
            try {
                wait();
            } catch (InterruptedException e) {
                //System.out.println("InterruptedException異常");
                flag = false;
            }
            System.out.println(Thread.currentThread().getName() + ".....run");
        }
    }
    /*public void changFlag() {
        flag = false;
    }*/
}


五,守護線程

守護線程(用戶線程)可以理解爲後臺線程,我們所看到的線程都是前臺線程。當把某些線程標記爲後臺線程後,啓動這些線程後和前臺線程一樣啓動,執行。但是後臺線程的不同之處在於,當所有的前臺線程都結束後,後臺線程會自動結束。也就是當一個程序中所有的線程都結束,但是隻剩下守護進程的時候,虛擬機會自動停止。

標記爲守護線程的方法是Thread類中的setDaemon()方法,該方法要在啓動線程之前調用,也就是在調用start方法之前,調用setDaemon();


六,Thread類的join方法:

這個方法不是特別重要,但是有點技術含量。它的作用是,當A線程執行到了B線程的join方法,此時A線程會凍結,B線程獲取執行權,當B線程運行結束後,A線程才能獲取執行權。Join方法可以臨時加入線程執行。
class Demon implements Runnable
{
    public void run() {
        for(int i=0;i<60;i++) {
            System.out.println("Thread......." + i);
        }
    }
}
class JoinDemon
{
    public static void main(String[] args) throws Exception {
        Demon d = new Demon();
        Thread t1 = new Thread(d); 
        Thread t2 = new Thread(d);
        t1.start();
        //t1.join(); //join加在這裏,是說t1要cup的執行權,此時執行權在主線程上,主線程將執行權交給t1,
        //主線程會停止,等待t1線程運行結束後才獲取執行權,然後主線程和t2爭奪資源,交替執行。
        t2.start();
        t1.join();//join在這個位置時,主線程停止,沒有執行權,主線程會等到t1執行完後
                //才能擁有執行權,但是t1在主線程等待之前還是有執行權的會和t2交替執行
                //但是如果一旦t1運行完,不管t2是否結束,主線程都會搶到執行權。
        for (int i=0;i<100 ;i++ )
        {
            System.out.println("main...." + i);
        }
    }
}

七,ThreadGroup類中,關於線程有限制的設置方法,setMaxPriority()方法設置優先級,如果想通過控制態輸出線程的優先級,可以通過Thread.currentThread().toString()方法打印出線程的完整信息,線程的優先級只有1到10。


    Thread中的yield方法:暫停正在執行的線程,執行其他線程。其實作用就是讓線程之間循環交替,均勻的循環交替執行。


八,企業中的線程使用:

/*
例如這樣一個程序:
class Demon {
    public static void main(String[] ars) {
        for(int i=0;i<100;i++) {
            System.out.println("result" + i);
        }
        for(int i=0;i<100;i++) {
            System.out.println("result" + i);
        }
        for(int i=0;i<100;i++) {
            System.out.println("result" + i);
        }
    }
}
類中的這三個計算是按順序計算的,只要第一個不運算結束,後兩個基本上沒有
計算機會,如果第一個計算足夠大,後兩個有可能永遠不能計算。
*/
class Demon {
    public static void main(String[] ars) {
        new Thread() {
            public void run() {
                for(int i=0;i<100;i++) {
                    System.out.println(Thread.currentThread().getName() + i);
                }
            }
        }.start();
        for(int i=0;i<100;i++) {
            System.out.println(Thread.currentThread().getName() + i);
        }
        Runnable r = new Runnable() {
            public void run() {
                for(int i=0;i<100;i++) {
                    System.out.println(Thread.currentThread().getName() + i);
                }
            }
        };
        new Thread(r).start();
    }
}
//這樣以後就相當於三個計算在同時進行計算,程序裏面有三個線程,他們在交替計算 



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