線程同步、死鎖與生產者消費者

一、問題引出

多個線程訪問同一個資源時,如果操作不當就很容易產生意想不到的錯誤,比如常見的搶票程序:

public class Demo1 {
    public static void main(String[] args) {
        Ticket tt = new Ticket();
        new Thread(tt, "甲").start();
        new Thread(tt, "乙").start();
    }
}

class Ticket implements Runnable {
    private int ticketCount = 10;
    @Override
    public void run() {
        while (ticketCount > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "搶到了第【" + (10 - ticketCount + 1) + "】張火車票");
            ticketCount--;
        }
    }
}

結果:

乙搶到了第【1】張火車票
甲搶到了第【2】張火車票
乙搶到了第【3】張火車票
甲搶到了第【4】張火車票
乙搶到了第【5】張火車票
甲搶到了第【6】張火車票
乙搶到了第【7】張火車票
甲搶到了第【8】張火車票
乙搶到了第【9】張火車票
甲搶到了第【9】張火車票
乙搶到了第【11】張火車票

以上代碼可以看出,第9張票被兩個人搶,且出現了第11張票,這不符合實際。爲什麼會出現這種現象?原因是當乙搶到第9張票但還未執行ticketCount–;語句時,甲也進入了run()方法,此時票數仍然是第9張票,也打印出了第9票,因此第9張票被搶了兩遍。之後甲和乙都會執行ticketCount–;語句,當ticketCount =0時,無論哪個線程搶到資源都會打印搶到了第【11】張火車票,當ticketCount = -1時,停止。

綜上分析,問題出現的主要原因就是甲、乙兩個線程同時訪問同一資源,造成了資源污染。

二、線程同步

上面可知當多個線程同時訪問同一資源,就可能會造成了資源污染,解決這個問題的方法就是在某個線程訪問資源時,其他線程在資源或者方法外面等待,也就是線程同步,或者叫加鎖。
線程同步就是指多個操作在同一時間段內只能有一個線程進行,而其他線程要等待此線程完成之後纔可以繼續進行。
要實現線程同步,需要通過關鍵字synchronized關鍵字,利用這個關鍵字可以定義同步方法或者代碼塊。格式如下:

synchronized(同步對象){
	操作;
}

一般要進行同步對象處理時,採用當前對象this進行同步。上面的搶票代碼加上同步後如下:

public class Demo1 {
    public static void main(String[] args) {
        Ticket tt = new Ticket();
        new Thread(tt, "甲").start();
        new Thread(tt, "乙").start();
    }
}

class Ticket implements Runnable {
    private int ticketCount = 10;
    @Override
    public void run() {
        synchronized(this){
            while (ticketCount > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "搶到了第【" + (10 - ticketCount + 1) + "】張火車票");
                ticketCount--;
            }
        }
    }
}

結果:

甲搶到了第【1】張火車票
甲搶到了第【2】張火車票
甲搶到了第【3】張火車票
甲搶到了第【4】張火車票
甲搶到了第【5】張火車票
甲搶到了第【6】張火車票
甲搶到了第【7】張火車票
甲搶到了第【8】張火車票
甲搶到了第【9】張火車票
甲搶到了第【10】張火車票

加鎖或者同步處理以後,雖然多線程同時訪問同一資源的問題雖然解決了,但是線程同步會降低整體性能。

三、死鎖

死鎖就是多個線程間相互等待的狀態。

class MyThread implements Runnable{
    int flag = 1;
    // 必須是靜態資源
    static Object o1 = new Object();
    static Object o2 = new Object();

    @Override
    public void run() {
        System.out.println("flag= " + flag);
        if(flag == 1){
            synchronized (o1){
                System.out.println(Thread.currentThread().getName() + "我搶到了o1,還需要o2");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2){
                    System.out.println("111");
                }
            }
        }
        if(flag == 0){
            synchronized (o2){
                System.out.println(Thread.currentThread().getName() + "我搶到了o2,還需要o1");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1){
                    System.out.println("222");
                }
            }
        }
    }
}

public class DeadLock {
    public static void main(String[] args) {
        MyThread myThread1 = new MyThread();
        MyThread myThread2 = new MyThread();
        // 設置線程1先搶佔o1
        myThread1.flag = 1;
        // 設置線程1先搶佔o2
        myThread2.flag = 0;
        Thread t1 = new Thread(myThread1);
        Thread t2 = new Thread(myThread2);
        t1.start();
        t2.start();
    }
}

在這裏插入圖片描述
有兩個線程,都需要鎖住同樣的2個對象(a、b)才能完成操作,其中線程1已經鎖住了a對象,線程2鎖住了b對象,兩個線程都不釋放鎖就會造成死鎖。程序無法停止。

四、生產者和消費者模式

生產者和消費者模式是用來解決死鎖問題的。
爲什麼可以解決呢?
生產者和消費者模式中,一種是生產者線程用於生產數據,另一種是消費者線程用於消費數據,爲了解耦生產者和消費者的關係,通常會採用共享的數據區域,就像是一個倉庫,生產者生產數據之後直接放置在共享數據區中,並不需要關心消費者的行爲;而消費者只需要從共享數據區中去獲取數據,就不再需要關心生產者的行爲。但是,這個共享數據區域中應該具備這樣的線程間併發協作的功能,需要根據信號來生產或獲取:當生產者線程生產物品時,如果沒有空緩衝區可用,那麼生產者線程必須等待消費者線程釋放出一個空緩衝區。當消費者線程消費物品時,如果沒有滿的緩衝區,那麼消費者線程將被阻塞,直到新的物品被生產出來。這裏實際體現了信號燈法:flag = true,生產者生產,消費者等待;反之,生產者等待,消費者生產。

下面以蒸包子和喫包子爲例,廚師作爲生產者輸出包子,喫包子的作爲消費者,如果沒有采用生產者消費者模式,那就可能出現喫包子的沒包子喫或者蒸包子的產能過剩的情況;而採用生產者消費者模式後,廚師蒸好包子後不是接着蒸,而是把蒸好的包子放在籃子裏然後停下來告訴消費者可以吃了,然後消費者接到信號開始從籃子裏拿包子喫,直到喫完後會給廚師一個信號,告訴他可以接着蒸包子了,這時消費者停下,廚師開始蒸包子,接着循環下去。

/**
 * 裝包子的籃子類
 */
class Container{
    private String baozi;
    // 籃子狀態信號標誌:flag = true,籃子空了,生產者生產;flag = false,籃子滿了,消費者消費
    private boolean flag = true;

    // 模擬生產過程,生產過程要加鎖,防止生產過程中消費者過來消費
    public synchronized void play(String baozi) throws InterruptedException {
        // 生產者等待
        if(!flag){
            this.wait();
        }
        // 生產者生產
        Thread.sleep(500); // 模擬生產耗時
        // 包子蒸好了
        this.baozi = baozi;
        System.out.println("生產" + baozi);
        // 通知消費者來喫
        this.notify();
        // 停止生產
        this.flag = false;
    }

    // 模擬消費過程,消費過程要加鎖,防止消費過程中生產者過來生產
    public synchronized void eat() throws InterruptedException {
        // 消費者等待
        if(flag){
            this.wait();
        }
        // 消費者消費
        Thread.sleep(100); // 模擬消費耗時
        System.out.println("籃子裏的包子已經喫完了");
        // 通知生產者需要蒸包子了
        this.notify();
        // 停止喫包子
        this.flag = true;
    }
}

/**
 * 定義生產者
 */
class Player implements Runnable{
    private Container container;
    public Player(Container container) {
        this.container = container;
    }

    // 單日生產素餡的包子,雙日生產肉餡的包子
    @Override
    public void run() {
        for (int i = 0; i < 6; i++) {
            if(i%2 == 0){
                try {
                    container.play("肉餡的包子");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else {
                try {
                    container.play("素餡的包子");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

/**
 * 定義消費者
 */
class Consumer implements Runnable{
    private Container container;
    public Consumer(Container container) {
        this.container = container;
    }

    // 不管啥餡的包子我都喫
    @Override
    public void run() {
        for (int i = 0; i < 6; i++) {
            try {
                container.eat();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class CpDemo {
    public static void main(String[] args) {
        // 同一個籃子
        Container container = new Container();

        Player player = new Player(container);
        Consumer consumer = new Consumer(container);
        new Thread(player, "生產者").start();
        new Thread(consumer, "消費者").start();
    }
}
生產肉餡的包子
籃子裏的包子已經喫完了
生產素餡的包子
籃子裏的包子已經喫完了
生產肉餡的包子
籃子裏的包子已經喫完了
生產素餡的包子
籃子裏的包子已經喫完了
生產肉餡的包子
籃子裏的包子已經喫完了
生產素餡的包子
籃子裏的包子已經喫完了

以上,生產者線程和消費者線程交替進行,就能很好的避免死鎖問題。

五、volatile和synchronized的區別

volatile關鍵字主要是在屬性定義上使用,表示此屬性爲直接數據操作,而不進行副本的拷貝處理。

volatile和synchronized的區別:

  • volatile主要是在屬性定義上使用,而synchronized是在代碼塊或方法上使用
  • volatile無法描述同步處理,是一種直接內存處理,避免了副本操作;synchronized是同步操作。

六、sleep和wait的區別

① 這兩個方法來自不同的類分別是,sleep來自Thread類,和wait來自Object類。

sleep是Thread的靜態類方法,誰調用的誰去睡覺,即使在a線程裏調用b的sleep方法,實際上還是a去睡覺,要讓b線程睡覺要在b的代碼中調用sleep。

② 鎖: sleep方法不會釋放lock,但是wait會釋放,而且會加入到等待隊列中,使得其他線程可以使用同步控制塊或者方法。

sleep不需要被喚醒(休眠之後推出阻塞),但是wait需要(不指定時間需要被別人中斷)。sleep不出讓系統資源;wait是進入線程等待池等待,出讓系統資源,其他線程可以佔用CPU。一般wait不會加時間限制,因爲如果wait線程的運行資源不夠,再出來也沒用,要等待其他線程調用notify/notifyAll喚醒等待池中的所有線程,纔會進入就緒隊列等待OS分配系統資源。sleep(milliseconds)可以用時間指定使它自動喚醒過來,如果時間不到只能調用interrupt()強行打斷。

③ 使用範圍:wait,notify和notifyAll只能在同步控制方法或者同步控制塊裏面使用,而sleep可以在任何地方使用。sleep方法不依賴於同步器synchronized,但是wait需要依賴synchronized關鍵字。

七、notify和notifyAll的區別?

先要理解鎖池和等待池

  • 鎖池:假設線程A已經擁有了某個對象(注意:不是類)的鎖,而其它的線程想要調用這個對象的某個synchronized方法(或者synchronized塊),由於這些線程在進入對象的synchronized方法之前必須先獲得該對象的鎖的擁有權,但是該對象的鎖目前正被線程A擁有,所以這些線程就進入了該對象的鎖池中。
  • 等待池:假設一個線程A調用了某個對象的wait()方法,線程A就會釋放該對象的鎖後,進入到了該對象的等待池中

notify和notifyAll的區別

  • 如果線程調用了對象的 wait()方法,那麼線程便會處於該對象的等待池中,等待池中的線程不會去競爭該對象的鎖。
    當有線程調用了對象的 notifyAll()方法(喚醒所有 wait 線程)或
  • notify()方法(只隨機喚醒一個 wait 線程),被喚醒的的線程便會進入該對象的鎖池中,鎖池中的線程會去競爭該對象鎖。也就是說,調用了notify後只要一個線程會由等待池進入鎖池,而notifyAll會將該對象等待池內的所有線程移動到鎖池中,等待鎖競爭
  • 優先級高的線程競爭到對象鎖的概率大,假若某線程沒有競爭到該對象鎖,它還會留在鎖池中,唯有線程再次調用 wait()方法,它纔會重新回到等待池中。而競爭到對象鎖的線程則繼續往下執行,直到執行完了 synchronized 代碼塊,它會釋放掉該對象鎖,這時鎖池中的線程會繼續競爭該對象鎖。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章