java多線程學(xué)習(xí)筆記

進(jìn)程:正在執(zhí)行的程序,是一個(gè)動(dòng)態(tài)的過(guò)程

線程:是進(jìn)程中用于控制程序執(zhí)行的控制單元(執(zhí)行路徑,執(zhí)行情景)

進(jìn)程中至少有一個(gè)線程。

在Java VM(java虛擬機(jī))啟動(dòng)的時(shí)候會(huì)有一個(gè)進(jìn)程java.exe.

該進(jìn)程中至少一個(gè)線程負(fù)責(zé)java程序的執(zhí)行。
而且這個(gè)線程運(yùn)行的代碼存在于main方法中。
該線程稱之為主線程。

擴(kuò)展:其實(shí)更細(xì)節(jié)說(shuō)明jvm,jvm啟動(dòng)不止一個(gè)線程,還有負(fù)責(zé)垃圾回收機(jī)制的線程。

創(chuàng)建線程

創(chuàng)建線程的第一種方式:繼承Thread類(lèi)

步驟:

1,定義類(lèi)繼承Thread。
2,復(fù)寫(xiě)Thread類(lèi)中的run方法。
    目的:將自定義代碼存儲(chǔ)在run方法。讓線程運(yùn)行。

3,調(diào)用線程的start方法,
    該方法兩個(gè)作用:?jiǎn)?dòng)線程,調(diào)用run方法。

發(fā)現(xiàn)運(yùn)行結(jié)果每一次都不同。因?yàn)槎鄠€(gè)線程都獲取cpu的執(zhí)行權(quán)。cpu執(zhí)行到誰(shuí),誰(shuí)就運(yùn)行。
明確一點(diǎn),在某一個(gè)時(shí)刻,只能有一個(gè)程序在運(yùn)行(多核除外)。cpu在做著快速的切換,以達(dá)到看上去是同時(shí)運(yùn)行的效果。

我們可以形象把多線程的運(yùn)行形容為在互相搶奪cpu的執(zhí)行權(quán)。

這就是多線程的一個(gè)特性:隨機(jī)性。誰(shuí)搶到誰(shuí)執(zhí)行,至于執(zhí)行多長(zhǎng),cpu說(shuō)的算。

為什么要覆蓋run方法呢?

Thread類(lèi)用于描述線程。該類(lèi)就定義了一個(gè)功能,用于存儲(chǔ)其他線程(非主線程)要運(yùn)行的代碼。該存儲(chǔ)功能就是run方法。

也就是說(shuō)Thread類(lèi)中的run方法,用于存儲(chǔ)線程要運(yùn)行的代碼。

創(chuàng)建線程的第二種方式:實(shí)現(xiàn)Runable接口

步驟:

1,定義類(lèi)實(shí)現(xiàn)Runnable接口
2,覆蓋Runnable接口中的run方法。
    將線程要運(yùn)行的代碼存放在該run方法中。

3,通過(guò)Thread類(lèi)建立線程對(duì)象。
4,將Runnable接口的子類(lèi)對(duì)象作為實(shí)際參數(shù)傳遞給Thread類(lèi)的構(gòu)造函數(shù)。
    為什么要將Runnable接口的子類(lèi)對(duì)象傳遞給Thread的構(gòu)造函數(shù)。
    因?yàn)?,自定義的run方法所屬的對(duì)象是Runnable接口的子類(lèi)對(duì)象。
    所以要讓線程去指定指定對(duì)象的run方法。就必須明確該run方法所屬對(duì)象。


5,調(diào)用Thread類(lèi)的start方法開(kāi)啟線程并調(diào)用Runnable接口子類(lèi)的run方法。

兩種創(chuàng)建現(xiàn)場(chǎng)的區(qū)別

實(shí)現(xiàn)方式和繼承方式有什么區(qū)別呢?

實(shí)現(xiàn)方式好處:避免了單繼承的局限性。
在定義線程時(shí),建立使用實(shí)現(xiàn)方式。

兩種方式區(qū)別:

繼承Thread:線程代碼存放Thread子類(lèi)run方法中。

實(shí)現(xiàn)Runnable,線程代碼存在接口的子類(lèi)的run方法。

線程中的方法

線程都有自己默認(rèn)的名稱:Thread-編號(hào) 該編號(hào)從0開(kāi)始。

線程中的常用方法

static Thread currentThread():獲取當(dāng)前線程對(duì)象。
getName(): 獲取線程名稱。

設(shè)置線程名稱:setName或者構(gòu)造函數(shù)。

多線程中出現(xiàn)的問(wèn)題

多線程的運(yùn)行出現(xiàn)了安全問(wèn)題。

問(wèn)題的原因:

當(dāng)多條語(yǔ)句在操作同一個(gè)線程共享數(shù)據(jù)時(shí),一個(gè)線程對(duì)多條語(yǔ)句只執(zhí)行了一部分,還沒(méi)有執(zhí)行完,
另一個(gè)線程參與進(jìn)來(lái)執(zhí)行。導(dǎo)致共享數(shù)據(jù)的錯(cuò)誤。

解決辦法:

對(duì)多條操作共享數(shù)據(jù)的語(yǔ)句,只能讓一個(gè)線程都執(zhí)行完。在執(zhí)行過(guò)程中,其他線程不可以參與執(zhí)行。

Java對(duì)于多線程的安全問(wèn)題提供了專(zhuān)業(yè)的解決方式。

就是同步代碼塊,方法如下

synchronized(對(duì)象)//任意對(duì)象都行
{
    需要被同步的代碼

}

對(duì)象如同鎖。持有鎖的線程可以在同步中執(zhí)行。
沒(méi)有持有鎖的線程即使獲取cpu的執(zhí)行權(quán),也進(jìn)不去,因?yàn)闆](méi)有獲取鎖。

同步的前提:

1,必須要有兩個(gè)或者兩個(gè)以上的線程。
2,必須是多個(gè)線程使用同一個(gè)鎖。

必須保證同步中只能有一個(gè)線程在運(yùn)行。

好處:解決了多線程的安全問(wèn)題。

弊端:多個(gè)線程都需要判斷鎖,較為消耗資源,

示例代碼如下:

public class TicketDemo2 {

    public static void main(String[] args) {
        Ticket t = new Ticket();

        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        Thread t3 = new Thread(t);
        Thread t4 = new Thread(t);
        t1.start();
        t2.start();
        t3.start();
        t4.start();

    }

}

class Ticket implements Runnable {
    private int tick = 100;
    Object obj = new Object();// 為synchronized提供對(duì)象

    public void run() {
        while (true) {
            synchronized (obj) {
                if (tick > 0) {
                     try{Thread.sleep(50);}catch(Exception e){}
                    System.out.println(Thread.currentThread().getName() + "....sale : " + tick--);
                }
            }
        }
    }
}

此段代碼的是模擬賣(mài)票,假設(shè)有100張票,然后有4個(gè)線程同時(shí)賣(mài)票。
可以在Ticket類(lèi)中有synchronized同步代碼塊,當(dāng)有同步代碼塊時(shí),持有synchronized同步鎖的線程可以在同步中執(zhí)行。沒(méi)有持有鎖的線程即使獲取cpu的執(zhí)行權(quán),也進(jìn)不去,因?yàn)闆](méi)有獲取synchronized同步鎖。線程將依序執(zhí)行,也就是有一個(gè)線程在進(jìn)入時(shí),另一個(gè)線程無(wú)法進(jìn)入。

如果沒(méi)有synchronized同步鎖,那么有可能會(huì)有以下問(wèn)題產(chǎn)生,當(dāng)tick剩下1張票的時(shí)候,t1現(xiàn)場(chǎng)進(jìn)入了,然后遇到Thread.sleep(50),失去執(zhí)行權(quán)。

接著t2線程執(zhí)行了了,此時(shí)tick仍然是1張票,因此t2也進(jìn)入if語(yǔ)句,也遇到sleep()方法,失去了執(zhí)行權(quán)。

然后t3線程也來(lái)了,遇到了與t1/t2一樣的狀況。

t4線程也既有可能遇到這種情況。

最后t1開(kāi)始執(zhí)行了,tick為0了。但是此時(shí)t2也在if語(yǔ)句中,也執(zhí)行語(yǔ)句,tick就為-1,然后是t3/t4,導(dǎo)致了代碼運(yùn)行的結(jié)果出現(xiàn)了負(fù)數(shù),與設(shè)想中的不同!

上面所描述的問(wèn)題就是多線程中所要解決的問(wèn)題,這也是synchronized同步鎖的使用情景

同步鎖

synchronized可以給代碼塊上鎖,也可以給方法上鎖,如下:

        // 同步代碼塊
        synchronized (對(duì)象) {
            ····· 代碼塊
        }
        
        // 同步方法
        public synchronized void show (){}

那么問(wèn)題來(lái)了,在同步代碼塊中我們可以很清楚的看到synchronized給哪一個(gè)對(duì)象上鎖,那么同步方法又是給誰(shuí)上鎖呢?

同步函數(shù)用的是哪一個(gè)鎖呢?
函數(shù)需要被對(duì)象調(diào)用。那么函數(shù)都有一個(gè)所屬對(duì)象引用,就是this,所以同步函數(shù)使用的鎖是this。

通過(guò)該程序進(jìn)行驗(yàn)證。

使用兩個(gè)線程來(lái)買(mǎi)票。一個(gè)線程在同步代碼塊中。一個(gè)線程在同步函數(shù)中。都在執(zhí)行買(mǎi)票動(dòng)作。

代碼如下:

public class TicketDemo2 {

    public static void main(String[] args) {
        Ticket t = new Ticket();

        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        t1.start();
        // 讓主線程休眠,將執(zhí)行權(quán)移交給t1/t2線程
        try{Thread.sleep(10);}catch(Exception e){}
        t.flag = false;
        t2.start();

    }

}

class Ticket implements Runnable {
    private int tick = 100;
    Object obj = new Object();
    // 使用flag
    boolean flag = true;

    public void run() {
        if (flag) {
            while (true) {
                // 使用的鎖是obj
                synchronized (obj) {
                    if (tick > 0) {
                        try{Thread.sleep(10);}catch(Exception e){}
                        System.out.println(Thread.currentThread().getName() + "....code : " + tick--);
                    }
                }
            }
        } else
            while (true)
                show();
    }
    // 同步方法的鎖對(duì)象是this,即所屬對(duì)象的引用
    public synchronized void show()    {  
        if (tick > 0) {
            try{Thread.sleep(10);}catch(Exception e){}
            System.out.println(Thread.currentThread().getName() + "....show.... : " + tick--);
        }
    }
}

上述代碼中添加了同步方法,并且設(shè)置了flag布爾值,用以讓線程能在中途從同步代碼塊中切換到同步方法中。

然后再控制臺(tái)中看到結(jié)果:

Thread-1....show.... : 4
Thread-0....code : 3
Thread-1....show.... : 2
Thread-1....show.... : 1
Thread-0....code : 0

這里居然賣(mài)出了0張票,這明顯是一個(gè)錯(cuò)誤的結(jié)果,這是為什么呢?

答案在于我們雖然在多線程中使用了同步鎖,但是我們的鎖對(duì)象并不是同一個(gè)對(duì)象,因?yàn)樵谕酱a塊中使用的是obj對(duì)象,如下:

synchronized (obj) {
·····代碼省略
}

但是當(dāng)obj替換成this的時(shí)候,我們就能輸出正確的結(jié)果,不在輸出0張票。

這足以證明在同步方法中使用的鎖對(duì)象就是所屬對(duì)象引用。

靜態(tài)同步方法

static實(shí)際上也能使用同步鎖,我們將上述代碼修改,首先將同步方法修改為靜態(tài)同步方法,然后將tick也標(biāo)識(shí)為static,代碼如下:

private static  int tick = 100;
···

public static synchronized void show(){
····
}

調(diào)用方法跟前面一樣。

這時(shí)再次輸出了tick=0的錯(cuò)誤結(jié)果!

這又是為什么呢?

這是因?yàn)殪o態(tài)方法中的同步鎖對(duì)象使用的不是this,因?yàn)殪o態(tài)方法中也不可以定義this。

其原因在于靜態(tài)進(jìn)內(nèi)存時(shí),內(nèi)存中沒(méi)有本類(lèi)對(duì)象,但是一定有該類(lèi)對(duì)應(yīng)的字節(jié)碼文件對(duì)象。
即 類(lèi)名.class 該對(duì)象的類(lèi)型是Class

靜態(tài)的同步方法,使用的鎖是該方法所在類(lèi)的字節(jié)碼文件對(duì)象。 類(lèi)名.class。

所以我們將同步代碼塊中的所對(duì)象修改為T(mén)icket.class時(shí)就能輸出正確結(jié)果,此時(shí)Ticket完整代碼如下:

class Ticket implements Runnable {
    private static  int tick = 100;
    Object obj = new Object();
    boolean flag = true;

    public void run() {
        if (flag) {
            while (true) {
                synchronized (Ticket.class) {
                    if (tick > 0) {
                        try{Thread.sleep(50);}catch(Exception e){}
                        System.out.println(Thread.currentThread().getName() + "....code : " + tick--);
                    }
                }
            }
        } else
            while (true)
                show();
    }
    // 靜態(tài)方法的所對(duì)象是    類(lèi)名.class
    public static synchronized void show()    {  
        if (tick > 0) {
            try{Thread.sleep(50);}catch(Exception e){}
            System.out.println(Thread.currentThread().getName() + "....show.... : " + tick--);
        }
    }
}

單例模式中的懶漢式

單例模式中的懶漢式寫(xiě)法其實(shí)頗為復(fù)雜,因?yàn)橐紤]到多線程的問(wèn)題。如果不添加synchronized進(jìn)行同步,在多線程的情況下,仍有可能導(dǎo)致創(chuàng)建了多個(gè)單例的實(shí)例,這就違背了單例模式的設(shè)計(jì)出初衷,因此必須添加synchronized進(jìn)行同步,正確寫(xiě)法如下:

class Single {
    private static Single s = null;

    private Single() {
    }

    public static Single getInstance() {
        if (s == null) {
            synchronized (Single.class) {
                if (s == null)
                    s = new Single();
            }
        }
        return s;
    }
}

懶漢式與餓漢式的區(qū)別在于懶漢式用于延時(shí)加載。

而懶漢式出現(xiàn)的問(wèn)題在于使用多線程的時(shí)候創(chuàng)建多個(gè)實(shí)例,這時(shí)可以使用同步解決。

使用synchronized同步也是有技巧的,如果使用同步方法也是可以的,代碼如下:

    public synchronized static Single getInstance() {
        if (s == null) {                
                if (s == null)
                    s = new Single();
            }            
        return s;
    }

但這種寫(xiě)法會(huì)導(dǎo)致效率稍微低下,因此一般都采用雙重判斷的同步代碼塊的寫(xiě)法。

同時(shí)如果是使用了static靜態(tài)符合,靜態(tài)代碼塊所使用的同步鎖為該類(lèi)所屬的字節(jié)碼對(duì)象!

死鎖

產(chǎn)生死鎖的四個(gè)必要條件:

(1) 互斥條件:一個(gè)資源每次只能被一個(gè)進(jìn)程使用。
(2) 請(qǐng)求與保持條件:一個(gè)進(jìn)程因請(qǐng)求資源而阻塞時(shí),對(duì)已獲得的資源保持不放。
(3) 不剝奪條件:進(jìn)程已獲得的資源,在末使用完之前,不能強(qiáng)行剝奪。
(4) 循環(huán)等待條件:若干進(jìn)程之間形成一種頭尾相接的循環(huán)等待資源關(guān)系。

同步中嵌套同步,但是它們之間的鎖卻不同,容易導(dǎo)致死鎖,下面是死鎖示例:

public DeadLockTest TicketDemo2 {

    public static void main(String[] args) {
        Thread t1 = new Thread(new Test(true));
        Thread t2 = new Thread(new Test(false));
        t1.start();
        t2.start();
    }

}

class Test implements Runnable {
    private boolean flag;

    Test(boolean flag) {
        this.flag = flag;
    }

    public void run() {
        if (flag) {
            while (true) {
                synchronized (MyLock.locka) {
                    System.out.println(Thread.currentThread().getName() + "...if locka ");
                    synchronized (MyLock.lockb) {
                        System.out.println(Thread.currentThread().getName() + "..if lockb");
                    }
                }
            }
        } else {
            while (true) {
                synchronized (MyLock.lockb) {
                    System.out.println(Thread.currentThread().getName() + "..else lockb");
                    synchronized (MyLock.locka) {
                        System.out.println(Thread.currentThread().getName() + ".....else locka");
                    }
                }
            }
        }
    }
}

class MyLock {
    static Object locka = new Object();
    static Object lockb = new Object();
}

在上面的例子中Test在t1線程中進(jìn)入if語(yǔ)句得到了MyLock.locka的鎖,然后進(jìn)入了第二個(gè)代碼塊中需要MyLock.lockb的鎖才能進(jìn)行下一步。

但是此時(shí)t2線程也開(kāi)始執(zhí)行了,它先進(jìn)入了Test中的else語(yǔ)句,獲得了MyLock.locka的鎖,需要MyLock.locka的所完成同步代碼。

此時(shí)陷入了死局,t1持有了MyLock.locka的鎖,但是無(wú)法獲得MyLock.lockb的鎖執(zhí)行完線程,也無(wú)法釋放MyLock.locka的鎖。

而t2線程同樣如此,持有MyLock.lockb的鎖,但是無(wú)法獲得MyLock.locka的鎖執(zhí)行完線程,也無(wú)法釋放MyLock.locka的鎖。

在寫(xiě)程序的時(shí)候應(yīng)該避免死鎖的產(chǎn)生。

線程間通信

線程間通訊: 其實(shí)就是多個(gè)線程在操作同一個(gè)資源,但是操作的動(dòng)作不同。

wait:
notify();
notifyAll();

notify()方法用于喚醒等待中的線程,一般線程使用了wait()方法,會(huì)進(jìn)入線程池中等待執(zhí)行,此時(shí)使用notify()方法一般喚醒線程池中的第一個(gè)等待線程。

都使用在同步中,因?yàn)橐獙?duì)持有監(jiān)視器(鎖)的線程操作。
所以要使用在同步中,因?yàn)橹挥型讲啪哂墟i。

為什么這些操作線程的方法要定義Object類(lèi)中呢?
因?yàn)檫@些方法在操作同步中線程時(shí),都必須要標(biāo)識(shí)它們所操作線程所持有的鎖,
只有同一個(gè)鎖上的被等待線程,可以被同一個(gè)鎖上notify喚醒。
不可以對(duì)不同鎖中的線程進(jìn)行喚醒。

也就是說(shuō),等待和喚醒必須是同一個(gè)鎖。

而鎖可以是任意對(duì)象,所以可以被任意對(duì)象調(diào)用的方法定義Object類(lèi)中。

多線程生產(chǎn)消費(fèi)者示例

下列代碼為正確示例:

class ProducerConsumerDemo {
    public static void main(String[] args) {
        Resource r = new Resource();

        Producer pro = new Producer(r);
        Consumer con = new Consumer(r);

        Thread t1 = new Thread(pro);
        Thread t2 = new Thread(pro);
        Thread t3 = new Thread(con);
        Thread t4 = new Thread(con);

        t1.start();
        t2.start();
        t3.start();
        t4.start();

    }
}

/*
 * 對(duì)于多個(gè)生產(chǎn)者和消費(fèi)者。 為什么要定義while判斷標(biāo)記。 原因:讓被喚醒的線程再一次判斷標(biāo)記。
 * 為什么定義notifyAll, 因?yàn)樾枰獑拘褜?duì)方線程。 因?yàn)橹挥胣otify,容易出現(xiàn)只喚醒本方線程的情況。導(dǎo)致程序中的所有線程都等待。
 */

class Resource {
    private String name;
    private int count = 1;
    private boolean flag = false;

    // t1 t2
    public synchronized void set(String name) {
        while (flag)
            try {this.wait();} catch (Exception e) {} // t1(放棄資格) t2(獲取資格)
        this.name = name + "--" + count++;

        System.out.println(Thread.currentThread().getName() + "...生產(chǎn)者.." + this.name);
        flag = true;
        this.notifyAll();
    }

    // t3 t4
    public synchronized void out() {
        while (!flag)
            try { wait();} catch (Exception e) {} // t3(放棄資格) t4(放棄資格)
        System.out.println(Thread.currentThread().getName() + "...消費(fèi)者........." + this.name);
        flag = false;
        this.notifyAll();
    }
}

class Producer implements Runnable {
    private Resource res;

    Producer(Resource res) {
        this.res = res;
    }

    public void run() {
        while (true) {
            res.set("+商品+");
        }
    }
}

class Consumer implements Runnable {
    private Resource res;

    Consumer(Resource res) {
        this.res = res;
    }

    public void run() {
        while (true) {
            res.out();
        }
    }
}

在多線程的情況下,通用的做法是使用while循環(huán) + notifyAll()喚醒方法來(lái)判斷執(zhí)行代碼。

而在單生產(chǎn)者和單消費(fèi)者的情況下使用if循環(huán) + notify()來(lái)判斷執(zhí)行代碼。

首先分析一下使用if循環(huán) + notify()在多生產(chǎn)者和多消費(fèi)者中產(chǎn)生的問(wèn)題,使用這種方法會(huì)導(dǎo)致程序執(zhí)行兩次生產(chǎn),一次消費(fèi)的情況,又或者是兩次消費(fèi)一次生產(chǎn)的情況。

關(guān)鍵代碼如下:

public synchronized void set(String name)
{
    if(flag)
        try{this.wait();}catch(Exception e){}//t1(放棄資格)  t2(獲取資格)
    this.name = name+"--"+count++;

    System.out.println(Thread.currentThread().getName()+"...生產(chǎn)者.."+this.name);
    flag = true;
    this.notify();
}

//  t3   t4  
public synchronized void out()
{
    if(!flag)
        try{wait();}catch(Exception e){}//t3(放棄資格) t4(放棄資格)
    System.out.println(Thread.currentThread().getName()+"...消費(fèi)者........."+this.name);
    flag = false;
    this.notify();
}

產(chǎn)生這種情況的原因在于如果生產(chǎn)者(t1/t2)先獲得了執(zhí)行權(quán),此時(shí)flag為false,因此t1直接往下執(zhí)行,生產(chǎn)出一個(gè)產(chǎn)品(執(zhí)行println),然后將flag設(shè)為true,但是此時(shí)t1仍然能繼續(xù)執(zhí)行(本案例中線程沒(méi)有退出機(jī)制,因此一旦執(zhí)行將持續(xù)執(zhí)行不停止),于是t1再次在if語(yǔ)句中判斷,然而flag已經(jīng)為true,所以t1執(zhí)行了wait()方法,放棄執(zhí)行權(quán)。

重點(diǎn)來(lái)了,在這個(gè)時(shí)候t2生產(chǎn)者,t3/t4消費(fèi)者都有執(zhí)行權(quán),假設(shè)這時(shí)t2取得執(zhí)行權(quán),執(zhí)行下去了,此時(shí)flag仍為true,因此t2也進(jìn)如wait()階段。

然后就輪到t3/t4執(zhí)行了,他們消費(fèi)一次之后,將flag設(shè)為false,然后喚醒了在線程池中的線程,而notify()會(huì)喚醒處于線程池中的第一個(gè)等待的線程,也就是t1。然而此時(shí)t3仍舊執(zhí)行,但是遇到flag為false,于是t3執(zhí)行了wait()方法。

這時(shí)線程中擁有執(zhí)行權(quán)的就剩下生產(chǎn)者t1和消費(fèi)者t4了,然而計(jì)算機(jī)執(zhí)行了t4線程,因?yàn)閒lag為false的緣故,t4也等待了。

此時(shí)在線程池中等待的順序依次為 t2,t3,t4。

t1開(kāi)始執(zhí)行了,然后把flag設(shè)置為true,并且執(zhí)行了notify()方法,喚醒了t2線程。

但是之前t2已經(jīng)通過(guò)了if的判斷,處于wait()狀態(tài),因此被喚醒的時(shí)候不在執(zhí)行if判斷,直接往下執(zhí)行,因此在t1之后也執(zhí)行了生產(chǎn)命令。

最后喚醒了t3消費(fèi)者,然后繼續(xù)執(zhí)行。

這就是聯(lián)系兩次執(zhí)行了生產(chǎn)命令的原因。

上面說(shuō)的有點(diǎn)啰嗦,但這非常重要,需要細(xì)細(xì)體會(huì)。

了解了為什么在多生產(chǎn)/消費(fèi)者執(zhí)行的情況下,使用if判斷和notify()方法喚醒會(huì)持續(xù)執(zhí)行兩次生產(chǎn)或者消費(fèi)的命令后,這是因?yàn)閕f只判斷一次flag的情況,當(dāng)線程被喚醒之后會(huì)直接往下執(zhí)行的緣故。

那么我們?nèi)绻褂脀hile加上notify()方法,讓線程每次被喚醒的時(shí)候都進(jìn)行判斷呢?

這時(shí)產(chǎn)生了死鎖。

這個(gè)結(jié)束比較簡(jiǎn)單,首先t1運(yùn)行,flag為true,然后t1繼續(xù)執(zhí)行的時(shí)候就進(jìn)入wait()方法,如果此時(shí)t2執(zhí)行的話,也進(jìn)入wait()方法中。

然后t3/t4執(zhí)行了,t3將flag設(shè)為false,并喚醒了t1,然后t3繼續(xù)執(zhí)行便執(zhí)行了wait()方法,如果此時(shí)t4執(zhí)行了,也會(huì)進(jìn)入wait()方法中,這時(shí)消費(fèi)者線程全滅

最后剩下t1在執(zhí)行了,t1將flag設(shè)置true之后,喚醒了t2,但是此時(shí)flag為true,而每次喚醒都會(huì)在while進(jìn)行一次循環(huán),此時(shí)悲催的t2再次進(jìn)入wait()方法,生產(chǎn)者線程也全滅了!

因?yàn)橹挥胣otify,容易出現(xiàn)只喚醒本方線程的情況。導(dǎo)致程序中的所有線程都等待。

這就是產(chǎn)生死鎖的原因。

所以在多生產(chǎn)多消費(fèi)者的情況下使用while讓每個(gè)線程在喚醒的時(shí)候再次進(jìn)行判斷,然后使用notifyAll()喚醒所有的線程,讓每個(gè)線程再次做判斷是否繼續(xù)往下執(zhí)行,這樣才能確保每次都有線程能夠執(zhí)行,也能確保每個(gè)線程都能被喚醒,不會(huì)導(dǎo)致死鎖。

使用jdk1.5之后的Lock接口進(jìn)行同步

上面生產(chǎn)者和消費(fèi)者的使用方法太過(guò)繁瑣了,JDK1.5 中提供了多線程升級(jí)解決方案。
將同步Synchronized替換成現(xiàn)實(shí)Lock操作。
將Object中的wait,notify notifyAll,替換了Condition對(duì)象。該對(duì)象可以Lock鎖 進(jìn)行獲取。
在下列示例中,實(shí)現(xiàn)了本方只喚醒對(duì)方操作。

Lock:替代了Synchronized
    lock 
    unlock
    newCondition()

Condition:替代了Object wait notify notifyAll
    await();
    signal();
    signalAll();

我們修改上面生產(chǎn)者和消費(fèi)者案例中的Resource的代碼,將Synchronized替換為L(zhǎng)ock,將notifyAll替換為Condition,代碼如下:

class Resource {
    private String name;
    private int count = 1;
    private boolean flag = false;
    // t1 t2
    private Lock lock = new ReentrantLock();

    private Condition condition_pro = lock.newCondition();
    private Condition condition_con = lock.newCondition();

    public void set(String name) throws InterruptedException {
        lock.lock();
        try {
            while (flag)
                condition_pro.await();// t1,t2等待
            this.name = name + "--" + count++;

            System.out.println(Thread.currentThread().getName() + "...生產(chǎn)者.." + this.name);
            flag = true;
            condition_con.signal(); // 喚醒t3,t4中的一個(gè)
        } finally {
            lock.unlock();// 釋放鎖的動(dòng)作一定要執(zhí)行。
        }
    }

    // t3 t4
    public void out() throws InterruptedException {
        lock.lock();
        try {
            while (!flag)
                condition_con.await();
            System.out.println(Thread.currentThread().getName() + "...消費(fèi)者........." + this.name);
            flag = false;
            condition_pro.signal();
        } 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.out();
            } catch (InterruptedException e) {
            }
        }
    }
}

使用Condition的好處在于一個(gè)Lock鎖可以擁有多個(gè)Condition對(duì)象。

而在上面的代碼中定義了兩個(gè)Condition對(duì)象,一個(gè)為生產(chǎn)者condition_pro的條件,另一個(gè)為消費(fèi)者condition_con條件。

這使condition_pro可以使用自己的await()方法和condition_pro.signal()喚醒方法,這樣就能讓在生產(chǎn)者中喚醒消費(fèi)者,而在消費(fèi)者中喚醒生產(chǎn)者,不會(huì)發(fā)生像Synchornized中的那種喚醒了本方線程的失誤事件。

但是要注意,使用Lock的時(shí)候,一定要在finally中釋放鎖,即調(diào)用lock.unlock()方法。

停止線程的方式

stop方法已經(jīng)過(guò)時(shí)。

如何停止線程?
只有一種,run方法結(jié)束。
開(kāi)啟多線程運(yùn)行,運(yùn)行代碼通常是循環(huán)結(jié)構(gòu)。

只要控制住循環(huán),就可以讓run方法結(jié)束,也就是線程結(jié)束。

示例代碼中給Runnable對(duì)象添加了flag標(biāo)記,控制了while循環(huán),代碼如下:

class StopThreadDemo {
    public static void main(String[] args) {
        StopThread st = new StopThread();
        
        Thread t1 = new Thread(st);
        Thread t2 = new Thread(st);

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

        int num = 0;

        while (true) {
            if (num++ == 60) {    
                st.changeFlag();
                t1.interrupt();
                t2.interrupt();
                break;
            }
            System.out.println(Thread.currentThread().getName() + "......." + num);
        }
        System.out.println("over");

    }
}

class StopThread implements Runnable {
    private boolean flag = true;
    
    public void run() {
        while (flag) {    
            System.out.println(Thread.currentThread().getName() + "....run");
        }
    }

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

特殊情況:
當(dāng)線程處于了凍結(jié)狀態(tài)。
就不會(huì)讀取到標(biāo)記。那么線程就不會(huì)結(jié)束。

當(dāng)沒(méi)有指定的方式讓凍結(jié)的線程恢復(fù)到運(yùn)行狀態(tài)是,這時(shí)需要對(duì)凍結(jié)進(jìn)行清除。
強(qiáng)制讓線程恢復(fù)到運(yùn)行狀態(tài)中來(lái),這樣就可以操作標(biāo)記讓線程結(jié)束。Thread類(lèi)提供該方法 interrupt();

示例如下:

class StopThread implements Runnable {
    private boolean flag = true;
    
    public synchronized void run() {
        while (flag) {
            try {
                wait();
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + "....Exception");
                flag = false;
            }
            System.out.println(Thread.currentThread().getName() + "....run");
        }
    }

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

class StopThreadDemo {
    public static void main(String[] args) {
        StopThread st = new StopThread();
        
        Thread t1 = new Thread(st);
        Thread t2 = new Thread(st);

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

        int num = 0;

        while (true) {
            if (num++ == 60) {                    
                t1.interrupt();
                t2.interrupt();
                break;
            }
            System.out.println(Thread.currentThread().getName() + "......." + num);
        }
        System.out.println("over");

    }
}

要注意的是使用 interrupt();方法并不是正確的停止線程的方式,而是以拋出異常的方式停止線程,要慎用。

Thread中的其他方法

setDaemon(boolean on) 將該線程標(biāo)記為守護(hù)線程或用戶線程。簡(jiǎn)單的說(shuō)就是將線程設(shè)置為后臺(tái)線程,此時(shí)將會(huì)與主線程搶奪CPU資源,而且主線程結(jié)束時(shí),后臺(tái)線程會(huì)自動(dòng)結(jié)束。

修改上面的StopThreadDemo代碼,將t1/t2線程設(shè)置為后臺(tái)線程,如下

class StopThreadDemo {
    public static void main(String[] args) {
        StopThread st = new StopThread();
        
        Thread t1 = new Thread(st);
        Thread t2 = new Thread(st);

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

        int num = 0;

        while (true) {
            if (num++ == 60) {                    
                
                break;
            }
            System.out.println(Thread.currentThread().getName() + "......." + num);
        }
        System.out.println("over");

    }
}

class StopThread implements Runnable {
    private boolean flag = true;
    
    public void run() {
        while (flag) {    
            System.out.println(Thread.currentThread().getName() + "....run");
        }
    }

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

此時(shí)并沒(méi)有使用任何的方式結(jié)束線程,卻發(fā)現(xiàn)當(dāng)主線程結(jié)束時(shí),t1/t2線程也結(jié)束了。

join()方法與yield()方法

join:
當(dāng)A線程執(zhí)行到了B線程的.join()方法時(shí),A就會(huì)等待。等B線程都執(zhí)行完,A才會(huì)執(zhí)行。

join可以用來(lái)臨時(shí)加入線程執(zhí)行,代碼示例:

class JoinDemo {
    public static void main(String[] args) {
        Demo d = new Demo();
        Thread t1 = new Thread(d);
        Thread t2 = new Thread(d);
        t1.start();

         try {
            t1.join();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
        t2.start();

        for (int x = 0; x < 80; x++) {
             System.out.println("main....."+x);
        }
        System.out.println("over");
    }
}

class Demo implements Runnable {
    public void run() {
        for (int x = 0; x < 70; x++) {
            System.out.println(Thread.currentThread().toString() + "....." + x);
        }
    }
}

toString():返回該線程的字符串你表示形式,包括線程名稱、優(yōu)先級(jí)和線程組

yield():暫停當(dāng)前正在執(zhí)行的線程對(duì)象,并執(zhí)行其他線程,代碼示例:

class YieldDemo {
    public static void main(String[] args) {
        Demo d = new Demo();
        Thread t1 = new Thread(d);
        Thread t2 = new Thread(d);
        t1.start();


        t2.start();

        System.out.println("over");
    }
}

class Demo implements Runnable {
    public void run() {
        for (int x = 0; x < 70; x++) {
            System.out.println(Thread.currentThread().toString() + "....." + x);
            Thread.yield();
        }
    }
}

線程間通訊

Java線程間通訊其實(shí)也就是使用Synchronized和Object類(lèi)方法wait(),notify(),notifyAll()的共同使用,保證運(yùn)行結(jié)果正確,這些都在上述代碼有所涉及。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容