多線程之間的通訊(仿真生產與消費)

源於螞蟻課堂的學習,點擊這裏查看(老餘很給力)

java內存模型(jmm)

java內存模型分爲兩大類型即,主內存和本地內存
1.主內存
    也就是主進程所佔用的內存
2.本地內存
    線程中開闢的屬於線程自己的內存,其中存放着全局變量的副本數據
    也就是全局共享的數據在主內存中的,線程中複製一份放入自己的本地內存,線程執行結束後將其變動刷新至主內存。
    這也就是爲什麼多線程會有線程安全的問題所在。
    多個線程同時做了修改,都去刷新主內存,會造成結果和實際不一致。

 volatile關鍵字

其功能爲多線程可見。
怎麼說呢?實際上就是當線程在本地內存中修改了被volatile修改的變量後,會立刻將其值刷新至主內存中,其他線程
也會馬上得到這個修改的結果
實際看上去就像線程在直接修改主線程中的這個變量一樣。

還有就是防止指令重新排序
代碼自上而下執行,這是常識,但是,計算機執行時,可能會將,沒有依賴關係的代碼指令的執行順序打亂,但一般不會
影響結果。
如:int a=1;int b=1;這時無論怎麼變化這兩條指令,都沒有影響。
如果再來一條int c=a+b;就會相互之間有了依賴關係,計算機就不會將其重新排序
指令並不是代碼行,指令是原子的,通過javap命令可以看到一行代碼編譯出來的指令,當然,像int i=1;這樣的代碼行也
是原子操作。

而對於那些比較隱晦的,指令重拍可能會引發問題得數據,volatile關鍵字可以指定其對應的代碼不會參與重新排序

 案例

package live.yanxiaohui;

/**
 * @Description 仿真消息的生產和消費(多線程之間的通訊),即來一個消息,消費一條消息
 * @CSDN https://blog.csdn.net/yxh13521338301
 * @Author: yanxh<br>
 * @Date 2020-05-15 11:08<br>
 * @Version 1.0<br>
 */
public class Test2 {
    public static void main(String[] args) {
        Money money = new Money();
        new Task1(money).start();
        new Task2(money).start();
    }
}

class Task1 extends Thread{
    private Money money;

    private int count;
    public Task1(Money money) {
        this.money = money;
    }

    @Override
    public void run() {
        while (true){
            if(count % 2 ==0){
                money.name = "馬雲";
                money.componet = "阿里";
            }else {
                money.name = "馬化騰";
                money.componet = "騰訊";
            }
            count++;
        }
    }
}

class Task2 extends Thread{
    private Money money;

    public Task2(Money money) {
        this.money = money;
    }

    @Override
    public void run() {
        while (true){
            System.out.println(money.name + "," + money.componet);
        }
    }
}

class Money{
    public String name;
    public String componet;
}

首先我們運行程序,會發現數據錯亂的現象

這是由於多個線程訪問全局共享數據造成的線程安全問題。
說人話:
寫入的線程做了修改,將本地內存改動的數據要同步至主內存,剛好同步一半,這時被掛起,
讀線程從主內存讀取數據就是一個錯誤的數據。

 那麼加鎖如何?

對共享的money對象加鎖。關鍵代碼如下
寫入操作:
           synchronized (money){
               if(count % 2 ==0){
                   money.name = "馬雲";
                   money.componet = "阿里";
               }else {
                   money.name = "馬化騰";
                   money.componet = "騰訊";
               }
           }

讀操作:
            synchronized (money){
                System.out.println(money.name + "," + money.componet);
            }

 一定會有讀者有疑問:讀爲什麼還要加鎖?

此處對數據的修改和讀取是兩個變量,這兩個變量需要保證其原子性,否則依舊是可能出現線程安全的
比如:如果讀不加鎖,那麼其讀到第一個屬性值後,被掛起,寫入的線程做了修改,讀線程繼續執行後
就會出現第二個屬性和第一個屬性不同步的現象

 運行程序

數據是保證一致性了,但依然不滿足我們消費的機制,所以需要再優化下 

我們可以使用wait和notify配合進行多個線程之間對同一共享的全局數據進行通訊


package live.yanxiaohui;

/**
 * @Description 仿真消息的生產和消費(多線程之間的通訊),即來一個消息,消費一條消息
 * @CSDN https://blog.csdn.net/yxh13521338301
 * @Author: yanxh<br>
 * @Date 2020-05-15 11:08<br>
 * @Version 1.0<br>
 */
public class Test2 {
    public static void main(String[] args) {
        Money money = new Money();
        new Task1(money).start();
        new Task2(money).start();
    }
}

class Task1 extends Thread{
    private Money money;

    private int count;



    public Task1(Money money) {
        this.money = money;
    }

    @Override
    public void run() {
        try{
            while (true){
                synchronized (money){
                    // 方便查看效果,設置阻塞時間
                    Thread.sleep(100);
                    if(!money.success){
                        // 交出對象的使用權,等待消息消費
                        money.wait();
                    }
                    if(count % 2 ==0){
                        money.name = "馬雲";
                        money.componet = "阿里";
                    }else {
                        money.name = "馬化騰";
                        money.componet = "騰訊";
                    }
                    money.success = false;
                    // 喚醒此對象所有等待的線程
                    money.notify();
                }
                count++;
            }
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

class Task2 extends Thread{
    private Money money;

    public Task2(Money money) {
        this.money = money;
    }

    @Override
    public void run() {
        try {
            while (true){
                synchronized (money){
                    if(money.success){
                        // 交出對象的使用權,等待消息生產
                        money.wait();
                    }
                    System.out.println(money.name + "," + money.componet);
                    money.success = true;
                    // 喚醒此對象所有等待的線程
                    money.notify();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class Money{
    public String name;
    public String componet;
    // true表示可以寫,false表示可以讀
    public boolean success;
}

 

 由此可以看出,線程執行是搶奪CPU分配權的,和主線程代碼的執行順序沒太大關係

我們可以使用lock鎖,去替代sync 

 

package live.yanxiaohui;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Description 仿真消息的生產和消費(多線程之間的通訊),即來一個消息,消費一條消息
 * @CSDN https://blog.csdn.net/yxh13521338301
 * @Author: yanxh<br>
 * @Date 2020-05-15 11:08<br>
 * @Version 1.0<br>
 */
public class Test3 {
    public static void main(String[] args) {
        Money money = new Money();
        new Task1(money).start();
        new Task2(money).start();
    }


}

class Task1 extends Thread {
    private Money money;

    private int count;


    public Task1(Money money) {
        this.money = money;
    }

    @Override
    public void run() {
        while (true) {
            try {
                // 對象上鎖
                money.lock.lock();
                // 方便查看效果,設置阻塞時間
                Thread.sleep(100);
                if (!money.success) {
                    // 交出對象的使用權,等待消息消費
                    money.condition.await();
                }
                if (count % 2 == 0) {
                    money.name = "馬雲";
                    money.componet = "阿里";
                } else {
                    money.name = "馬化騰";
                    money.componet = "騰訊";
                }
                money.success = false;
                // 喚醒此對象所有等待的線程
                money.condition.signal();
                count++;
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // 釋放鎖
                money.lock.unlock();
            }
        }
    }
}

class Task2 extends Thread {
    private Money money;

    public Task2(Money money) {
        this.money = money;
    }

    @Override
    public void run() {
        try {
            while (true) {
                // 對象上鎖
                money.lock.lock();
                if (money.success) {
                    // 交出對象的使用權,等待消息生產
                    money.condition.await();
                }
                System.out.println(money.name + "," + money.componet);
                money.success = true;
                // 喚醒此對象所有等待的線程
                money.condition.signal();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 釋放鎖
            money.lock.unlock();
        }
    }
}

class Money {
    public String name;
    public String componet;
    // true表示可以寫,false表示可以讀
    public boolean success;

    public Lock lock = new ReentrantLock();

    public Condition condition = lock.newCondition();
}

運行結果也是相同

總結

1.多線程之間實現通訊基於對全局共享數據的鎖定,即多個線程都必須去爭奪鎖,獲取鎖之後依照業務進行對應的等待或運行。
2.wait和notify必須要配合synchronized使用, 且必須要在同步代碼塊中執行
3.lock鎖因爲是手動添加的,所以它可控,實際開發中可用到的最多
4.wait底層是將對象的鎖釋放掉,自身線程進入阻塞等待狀態,直到被對象的notify喚醒
5.sleep只是線程的定時阻塞,不會釋放鎖
6.volatile指定全局共享變量在內存模型中線程間數據的可見性
7.volatile還可以防止指令重新排序

 

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