8、多線程(2)

四、線程同步

4.1 基本概念

  • 1、由于同一進程的多個線程共享同一片存儲空間,在帶來方便的同時,也帶來了訪問沖突這個嚴(yán)重的問題。java語言提供了專門的機制來解決這種沖突,有效避免了同一個數(shù)據(jù)對象被多個線程同時訪問。
  • 2、由于我們可以通過private關(guān)鍵字來保證數(shù)據(jù)對象只能被方法訪問,所以我們只需針對方法提出一套機制,這套機制就是synchronized關(guān)鍵字,它包括方法:synchronized方法和synchronized塊。

4.2 相關(guān)例子

不使用同步時可能會出現(xiàn)沖突

package cn.itcast.day178.thread02;
public class SynDemo01 {
    public static void main(String[] args) {
        Web12306 web = new Web12306();// 真實角色
        // 代理對象
        Thread t1 = new Thread(web, "黃牛1");// 第二個參數(shù)是當(dāng)前線程的名字
        Thread t2 = new Thread(web, "黃牛2");
        Thread t3 = new Thread(web, "黃牛3");
        // 啟動線程
        t1.start();
        t2.start();
        t3.start();
    }
}

class Web12306 implements Runnable {
    private int num = 10;
    private boolean flag = true;

    public void run() {
        while (flag) {
            test1();
        }
    }

    // 線程不安全
    public void test1() {
        if (num <= 0) {
            this.flag = false;
            return;
        }
        try {
            Thread.sleep(500);// 500ms的延時
            // 加入延時之后可能會造成資源沖突的問題,這就是并發(fā)問題
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "搶到了第" + num--
                + "張票");
    }
}

說明:這個例子是模擬搶票的情況,如果不加入同步,則可能一張票同時被多個人搶到,顯示這是有問題的,下面我們看使用同步方法來解決這個問題:

    // 線程安全,同步方法
    public synchronized void test2() {
        if (num <= 0) {
            this.flag = false;
            return;
        }
        try {
            Thread.sleep(500);// 500ms的延時
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "搶到了第" + num--
                + "張票");
    }

說明:在方法中加上synchronized關(guān)鍵字就可以將此方法變成一個同步方法,當(dāng)一個運行一個線程的此方法時,如果此方法沒有運行完,則其他線程的此方法是不能執(zhí)行的。當(dāng)然我們還可以使用同步塊來達到這個目的:

    // 線程安全,同步塊
    public void test3() {
        synchronized (this) {// 鎖定this,即鎖定當(dāng)前線程
            if (num <= 0) {
                this.flag = false;
                return;
            }
            try {
                Thread.sleep(500);// 500ms的延時
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "搶到了第"
                    + num-- + "張票");
        }
    }

說明:這里我們將方法中所要執(zhí)行的代碼全部放在同步塊中,這樣在同步塊中的代碼沒有執(zhí)行完的時候資源是被此線程鎖定的,同時要注意:這里同步塊需要給定鎖定的線程對象,這里我們給出的是當(dāng)前線程。當(dāng)時有時候我們將要執(zhí)行的代碼全部放在同步塊中會造成效率的下降,一般我們將可能出現(xiàn)并發(fā)錯誤的代碼放在同步塊中,達到最佳的效果,下面我們看一個錯誤的例子:

    // 線程不安全,同步塊,鎖定一部分,鎖定范圍不正確
    public void test4() {
        synchronized (this) {// 鎖定this,即鎖定當(dāng)前線程
            if (num <= 0) {
                this.flag = false;
                return;
            }
        }
        try {
            Thread.sleep(500);// 500ms的延時
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "搶到了第" + num--
                + "張票");
    }

說明:這里可能發(fā)生并發(fā)錯誤的位置是票數(shù)量減少的代碼,這里顯然同步塊位置是有問題的,所以并不能解決并發(fā)問題。放在同步塊中的代碼不僅要正確,我們鎖定的資源對象也要正確,下面看鎖定資源對象錯誤的一個例子:

    // 線程不安全,同步塊,鎖定資源不正確
    public void test5() {
        synchronized ((Integer) num) {// 對于基本類型需要包裝
            if (num <= 0) {
                this.flag = false;
                return;
            }
            try {
                Thread.sleep(500);// 500ms的延時
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "搶到了第"
                    + num-- + "張票");
        }
    }

說明:資源對象一般是某個線程對象(基本類型數(shù)據(jù)需要包裝),但是這里卻不是,所以也不能解決并發(fā)問題。還有一種同步塊范圍不對的情況:

    // 線程不安全,同步塊,鎖定資源不正確
    public void test6() {
        if (num <= 0) {
            this.flag = false;
            return;
        }
        //a b c
        synchronized (this) {
            try {
                Thread.sleep(500);// 500ms的延時
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "搶到了第"
                    + num-- + "張票");
        }
    }

說明:這里我們可以看到,多個線程可能同時出現(xiàn)在同步塊之前進行等待,那哪個線程進入同步塊中執(zhí)行呢?這顯然是不確定的,這樣就會造成沖突。上面我們講解了同步塊的兩種形式:

synchronized(引用類型)
synchronized(this)

其實同步塊還有一種形式synchronized(類.class)。先看一種設(shè)計模式:單例設(shè)計模式

package cn.itcast.day178.thread02;
//單例設(shè)計模式:確保一個類只有一個對象
public class SynDemo02 {
    public static void main(String[] args) {
        test2();
    }

    public static void test2() {
        // 此時我們看到單例就沒有達到效果,我們在getnInstance方法中加入同步關(guān)鍵字
        JvmThread thread1 = new JvmThread(100);
        JvmThread thread2 = new JvmThread(500);
        thread1.start();
        thread2.start();
    }
    public static void test1() {
        Jvm jvm1 = Jvm.getInstance();
        Jvm jvm2 = Jvm.getInstance();
        // 單線程中下面兩個對象是一樣的,達到了單例的效果,但是在多線程中就不一定了
        System.out.println(jvm1);
        System.out.println(jvm2);
    }
}

class JvmThread extends Thread {
    private long time;

    public JvmThread() {
    }

    public JvmThread(long time) {
        this.time = time;
    }

    public void run() {
        System.out.println(Thread.currentThread().getName() + "-->"
                + Jvm.getInstance(time));
    }
}

// 確保一個類只有一個對象:
// 懶漢式
class Jvm {

    // 1、構(gòu)造器私有化,避免外部直接創(chuàng)建對象
    private Jvm() {}
    // 2、聲明一個私有靜態(tài)變量
    private static Jvm instance = null;
    // 3、創(chuàng)建一個靜態(tài)的公共方法訪問該變量,如果變量沒有對象,創(chuàng)建該對象
    public static Jvm getInstance() {
        if (instance == null) {
            instance = new Jvm();
        }
        return instance;
    }
}

說明:單例設(shè)計模式就是為了確保在程序運行過程中一個類只有一個實例對象。Jvm類我們使用了基本的單例設(shè)計模式,在單線程中可以確保只有一個對象實例,但是在多線程就不一定了(test1方法)。從這個類中我們可以知道單例設(shè)計模式的基本步驟。其中加入延時是為了放大出錯的概率。從測試結(jié)果中可以看到并沒有達到單例的效果(run方法打印出來的結(jié)果不一致)。當(dāng)然解決這個問題最簡單的方式就是在getInstance方法中加入synchronized關(guān)鍵字,但是這里我們主要看使用同步塊如何解決,下面我們改進Jvm類:

class Jvm1 {
    // 1、構(gòu)造器私有化,避免外部直接創(chuàng)建對象
    private Jvm1() {
    }

    // 2、聲明一個私有靜態(tài)變量
    private static Jvm1 instance = null;

    // 3、創(chuàng)建一個靜態(tài)的公共方法訪問該變量,如果變量沒有對象,創(chuàng)建該對象
    public static Jvm1 getInstance1(long time) {
        if (instance == null) {
            try {
                Thread.sleep(time);// 加入延時
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            instance = new Jvm1();
        }
        return instance;
    }

    // 加入同步,我們可以直接在方法上加上synchronized關(guān)鍵字,這里我們使用同步塊,但是效率不高
    // 在下面我們進行改進
    public static Jvm1 getInstance2(long time) {
        synchronized (Jvm.class) {// 這里我們不能使用this了,因為this還沒有創(chuàng)建出來,于是使用字節(jié)碼
            if (instance == null) {
                try {
                    Thread.sleep(time);// 加入延時
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                instance = new Jvm1();
            }
            return instance;
        }
    }

    // 改進,這里比如有a,b,c三個線程,一開始對象為空,進入第一個if,然后a進入同步塊,其他線程等待
    // 當(dāng)a進去之后則對象就被創(chuàng)建了,于是當(dāng)其他線程進入同步塊的時候就不需要像上面那樣等待了,直接返回已有
    // 對象
    public static Jvm1 getInstance3(long time) {
        if (instance == null) {
            synchronized (Jvm.class) {
                if (instance == null) {
                    try {
                        Thread.sleep(time);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    instance = new Jvm1();
                }
            }
        }
        return instance;
    }
}

說明:首先我們是對方法getInstance改進成了getInstance2,進入了同步關(guān)鍵字,但是參數(shù)不能再是this了,因為此時對象還沒有創(chuàng)建出來。此時我們進行測試可以發(fā)現(xiàn)達到了同步的效果。但是這種實現(xiàn)的方式可能效率和之前的同步方法的效率一樣,不太高,因為此時不管對象存在不存在都需要在同步塊前面等待,我們改進為getInstance3方法,這樣如果對象存在則不需要進入同步塊中,直接拿到對象即可使用。而單例創(chuàng)建的方式有上面提到的懶漢式,還有其他方式:

package cn.itcast.day178.thread02;

/*單例創(chuàng)建的幾種方式:
 * 1、懶漢式
 *  a、構(gòu)造器私有化
 *  b、聲明私有的靜態(tài)屬性
 *  c、對外提供訪問屬性的靜態(tài)方法,確保該對象存在
 * */
public class MyJvm03 {
    private static MyJvm03 instance;
    private MyJvm03(){
        
    }
    public static MyJvm03 getInstance(){
        if(instance == null){//為了效率
            synchronized (MyJvm03.class) {
                if(instance == null){//為了安全
                    instance = new MyJvm03();
                }
            }
        }
        
        return instance;
    }
}

/*2、惡漢式
 *  a、構(gòu)造器私有化
 *  b、聲明私有的靜態(tài)屬性,同時創(chuàng)建該對象
 *  c、對外提供訪問屬性的靜態(tài)方法,確保該對象存在
 * */
class MyJvm04{
    private static MyJvm04 instance = new MyJvm04();
    private MyJvm04(){
        
    }
    public static MyJvm04 getInstance(){
        return instance;
    }
    
}
//惡漢式提高效率的改進:類在使用的時候才讓其加載,這樣只要不調(diào)用
//getInstance方法,那么就不會加載類,這樣延緩了類加載時機
class MyJvm05{
    private static class JVMholder{
        private static MyJvm05 instance = new MyJvm05();
    }
    
    private MyJvm05(){
        
    }
    public static MyJvm05 getInstance(){
        return JVMholder.instance;
    }
}

說明:相對來說,惡漢式的效率較高一點。

五、死鎖

過多的同步容易造成死鎖,就是一份資源同時被多個線程同時調(diào)用。

package cn.itcast.day178.thread02;
//兩個線程使用的是同一份資源,可能就會造成死鎖,但是這并不絕對
//過多的同步容易造成死鎖
public class SynDemo03 {
    public static void main(String[] args) {
        Object goods = new Object();
        Object money = new Object();
        Test t1 = new Test(goods, money);
        Test1 t2 = new Test1(goods, money);
        Thread proxy1 = new Thread(t1);
        Thread proxy2 = new Thread(t2);
        proxy1.start();
        proxy2.start();
    }
}

class Test implements Runnable{
    Object goods;
    Object money;
    
    public Test(Object goods, Object money) {
        this.goods = goods;
        this.money = money;
    }

    public void run() {
        while(true){
            test();
        }
    }
    public void test(){
        synchronized (goods) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (money) {
            }
        }
        System.out.println("一手給錢");
    }
}


class Test1 implements Runnable{
    Object goods;
    Object money;
    
    public Test1(Object goods, Object money) {
        super();
        this.goods = goods;
        this.money = money;
    }

    public void run() {
        while(true){
            test();
        }
    }
    public void test(){
        synchronized (money) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (goods) {
            }
        }
        System.out.println("一手交貨");
    }
}

說明:此時我們發(fā)現(xiàn)不會的打印出任何內(nèi)容,因為造成了死鎖。解決死鎖的思路就是使用生產(chǎn)者消費者設(shè)計模式。

生產(chǎn)者消費者模式

  • 1)生產(chǎn)者消費者模式也稱有限資源緩沖問題,是一個多線程同步問題的經(jīng)典案例。該問題描述了兩個共享固定大小的緩沖區(qū)的線程-即所謂的生產(chǎn)者合格消費者-在實際運行時會發(fā)生的問題。生產(chǎn)者的主要作用是生成一定量的數(shù)據(jù)放到緩沖區(qū)中,然后重復(fù)此過程。與此同時,消費者也在緩沖區(qū)消耗這些數(shù)據(jù)。該問題的關(guān)鍵就是要保證生產(chǎn)者不會在緩沖區(qū)滿時加入數(shù)據(jù),消費者也不會在緩沖區(qū)空時消耗數(shù)據(jù)。

  • 2)要解決該問題,就必須讓生產(chǎn)者在緩沖區(qū)滿時休眠(要么干脆就放棄數(shù)據(jù)),等到下次消費者消耗緩沖區(qū)中的數(shù)據(jù)的時候,生產(chǎn)者才能被喚醒,開始往緩沖區(qū)中添加數(shù)據(jù)。同樣,也可以讓消費者在緩沖區(qū)空的時候進入休眠,等到生產(chǎn)者往緩沖區(qū)中添加數(shù)據(jù)之后,再喚醒消費者。通常常用的方法有信號燈法,管程等。如果解決方法不不夠完善,則容易出現(xiàn)死鎖的情況,出現(xiàn)死鎖時,兩個線程都會陷入休眠,等待對方喚醒自己。

這里我們介紹信號燈法,首先給出資源:

package cn.itcast.day178.thread02;
/*一個場景,一份共同的資源
 * 生產(chǎn)者消費者模式,采用信號燈法
 * wait會釋放鎖,而sleep則不釋放鎖
 * notify和notifyAll表示喚醒
 * 注意:上面說的方法必須和同步在一起使用,不然就使用不了
 * */
public class Movie {
    private String pic;
    //信號燈,當(dāng)為true時表示生產(chǎn)者生產(chǎn),消費者等待,生產(chǎn)完成之后通知消費者消費
    //當(dāng)為false的時候,生產(chǎn)者等待,消費者消費,當(dāng)消費完成之后通知生產(chǎn)者生產(chǎn)
    private boolean flag = true;
    public synchronized void play(String pic){
        if(!flag){//生產(chǎn)者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //開始生產(chǎn)
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("生產(chǎn)了: " + pic);
        //生產(chǎn)完畢
        this.pic = pic;
        //通知消費
        this.notify();
        //生產(chǎn)者停止
        this.flag = false;
    }
    
    public synchronized void watch(){
        if(flag){
            //消費者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //開始消費
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("消費了: " + pic);
        //消費完畢,通知生產(chǎn)
        this.notify();
        
        //消費停止
        this.flag = true;
    }
}

說明:資源是一個電影,那么生產(chǎn)者就是演員:

package cn.itcast.day178.thread02;
/*表演者,這里就相當(dāng)于生產(chǎn)者 */
public class Player implements Runnable{
    private Movie movie;
    
    public Player(Movie movie) {
        super();
        this.movie = movie;
    }

    public void run() {
        for(int i = 0; i < 20; i++){
            if(i % 2 == 0){
                movie.play("左青龍");
            }else{
                movie.play("右白虎");
            }
        }
    }
}

說明:再給出消費者:

package cn.itcast.day178.thread02;
public class Watcher implements Runnable{
    private Movie movie;

    public Watcher(Movie movie) {
        super();
        this.movie = movie;
    }

    public void run() {
        for(int i = 0; i < 20; i++){
            movie.watch();
        }
    }
}

說明:下面我們使用:

package cn.itcast.day178.thread02;
public class App {
    public static void main(String[] args) {
        //共同的資源
        Movie m = new Movie();
        
        //多線程
        Player p = new Player(m);
        Watcher w = new Watcher(m);
        
        new Thread(p).start(); 
        new Thread(w).start(); 
    }
}

說明:我們在使用的時候同時開啟了生產(chǎn)者和消費者線程,在運行過程中如果資源沒有生產(chǎn)出來則消費者線程等待,資源生產(chǎn)出來之后消費者線程執(zhí)行。

六、任務(wù)調(diào)度

  • 1)Timer定時器類
  • 2)TimerTask任務(wù)類
  • 3)通過timertimertask:(spring的任務(wù)調(diào)度就是通過它們來實現(xiàn)的)
  • 4)在這種實現(xiàn)方式中,Timer類實現(xiàn)的是類似鬧鐘的功能,也就是定時或者每個一定時間觸發(fā)一次線程。其實,Timer類本身實現(xiàn)的就是一個線程,只是這個線程是用來實現(xiàn)調(diào)用其他線程的。而TimerTask類是一個抽象類,該類實現(xiàn)了Runnable接口,所以按照前面的介紹,該類具備多線程的能力。
  • 5)在這種實現(xiàn)方式中,通過繼承TimerTask使該類獲得多線程的能力,將需要多線程執(zhí)行的代碼書寫在run方法內(nèi)部,然后通過Timer類啟動線程的執(zhí)行。
  • 6)在實際使用時,一個Timer可以啟動任意多個TimerTask實現(xiàn)的線程,但是多個線程之間會存在阻塞。所以如果多個線程之間如果需要完全獨立運行的話,最好還是一個Timer啟動一個TimerTask實現(xiàn)。

下面看一個例子:

package cn.itcast.day178.thread02;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
/*
 * 使用方法schedule指定任務(wù)
 * */
public class TimerDemo01 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        //這里第一個參數(shù)表示指定一個任務(wù),第二個參數(shù)表示什么時候開始執(zhí)行,
        //第三個參數(shù)表示每隔多少秒執(zhí)行一次,如果沒有第三個參數(shù)則只運行一次
        timer.schedule(new TimerTask() {
            //線程體
            public void run() {
                System.out.println("線程體....");
            }
        }, new Date(System.currentTimeMillis() + 1000), 200);
    }
}

最后我們看一下notifynotifyAll的區(qū)別:
這里notify()notifyAll()都是Object對象用于通知處在等待該對象的線程的方法。

  • void notify():喚醒一個正在等待該對象的線程。
  • void notifyAll():喚醒所有正在等待該對象的線程。

兩者的最大區(qū)別在于:
notifyAll使所有原來在該對象上等待被notify的線程統(tǒng)統(tǒng)退出wait的狀態(tài),變成等待該對象上的鎖,一旦該對象被解鎖,他們就會去競爭。notify他只是選擇一個wait狀態(tài)線程進行通知,并使它獲得該對象上的鎖,但不驚動其他同樣在等待被該對象notify的線程們,當(dāng)?shù)谝粋€線程運行完畢以后釋放對象上的鎖,此時如果該對象沒有再次使用notify語句,即便該對象已經(jīng)空閑,其他wait狀態(tài)等待的線程由于沒有得到該對象的通知,繼續(xù)處在wait狀態(tài),直到這個對象發(fā)出一個notifynotifyAll,它們等待的是被notifynotifyAll,而不是鎖。

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

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

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