欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

JavaEE中volatile、wait和notify詳解

 更新時間:2023年02月03日 11:24:13   作者:Node_Hao  
這篇文章主要給大家介紹了關于JavaEE中volatile、wait和notify的相關資料,文中通過實例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下

一.volatile 關鍵字. 

1.volatile 能保證內存可見性問題

什么是內存可見性?

可見性指 , 一個線程對內存的修改 , 能夠及時的被其他線程看到.

Java內存模型(JMM):Java虛擬機規(guī)范中定義了Java內存模型 , 目的是屏蔽一切硬件和操作系統(tǒng)的內存訪問差異 , 以實現(xiàn)Java程序在各種平臺下都能達到一致的并發(fā)效果.

  • 線程之間的共享變量存在主內存(Main Memory)
  • 每一個線程都有自己的"工作內存"(寄存器)
  • 當線程要讀取一個共享變量時 , 會把共享變量從主內存拷貝到工作內存, 再從工作內存中讀取數(shù)據(jù).
  • 當線程要修改共享變量時 , 也先修改工作內存中的副本 , 最后同步到主內存中.

由于每個線程都有自己的工作內存, 這些工作內存中的內容相當于同一個共享變量的副本 , 此時修改線程 t1 的工作內存中的值 , 線程 t2 的工作內存不一定及時發(fā)生變化.這時代碼就容易發(fā)生問題.

此時引出兩個問題:

  • 為什么要這么多內存
  • 為什么要拷貝多次

1) 為什么要這么多內存?

實際并沒有這么多的內存 , 這只是Java規(guī)范中的一個術語 , 是術語抽象的叫法.

所謂主內存才是真正硬件角度的內存 , 而所謂工作內存 , 則是指CPU的寄存器和高速緩存(cache).至于為什么起名工作內存 , 一方面是為了表述簡單 , 另一方面也是避免涉及到硬件的細節(jié)和差異 , 例如有的CPU可能沒有cache , 有的還存在很多個 , 因此Java就使用工作內存一言蔽之了.

2) 為什么要多次拷貝?

因為CPU訪問寄存器和高速緩存的速度 , 比訪問寄存器快了3-4個數(shù)量級.

如果要連續(xù)10次讀取同一個數(shù)據(jù) , 不斷從內存中訪問就很慢 , 那么如果第一次從內存中讀取到寄存器 , 后面9次從寄存器中讀取就會快很多.

  • volatile 修飾的變量 , 能夠保證內存可見性.

代碼示例:

創(chuàng)建兩個線程 t1 和 t2 , t1 線程循環(huán)重復快速讀取flag , t2 線程對 flag 進行修改.按照預期結構 , 如果我們修改 t2 線程中的 flag  變?yōu)榉? , t1 線程就會循環(huán)結束.

class MyCounter{
    public int flag = 0;
}
public class ThreadDemo2 {
    public static void main(String[] args) {
        MyCounter myCounter = new MyCounter();
        Thread t1 = new Thread(()->{
            while (myCounter.flag == 0){
                //循環(huán)重復快速讀取
            }
            System.out.println("循環(huán)結束");
        });
        Thread t2 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("請輸入一個數(shù)");
            myCounter.flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

結果與我們預期并不相符 , 對 flag 作出修改后 , t1 線程并沒有循環(huán)結束. 

通過 jconsole 查看 t1 線程還在執(zhí)行 , 而 t2 線程已執(zhí)行完畢. 

結合內存可見性問題 , 答案顯而易見. 一個線程讀 , 一個線程改 , 會產生線程不安全問題.從匯編的角度來理解 , 執(zhí)行下面這段代碼分為兩個步驟:

  • load 把內存中的值讀到寄存器中.
  • cmp 把寄存器的值和0進行比較 , 根據(jù)比較結果決定下一步往哪執(zhí)行(條件循環(huán)指令)

上述循環(huán)操作在寄存器中 , 執(zhí)行速度極快(1秒鐘執(zhí)行百萬次以上) , 循環(huán)這么多次 , 在 t2 真正修改前 , load 得到的執(zhí)行結果都一樣.另一方面 load 相比于 cmp 操作速度慢非常多 , 再加上反復 load 的結果都一樣 , JVM 就會認為沒有人改 flag 的值 , 從此不再從內存中 load flag 的值 , 直接讀取寄存器中保存的 flag , 這時JVM/編譯器的一種優(yōu)化方式 , 但由于多線程的復雜性 , 判定可能存在誤差.

解決方式:

此時為了避免上述情況 , 就需要程序員手動干預 , 可以給 flag 這個變量加上 volatile 關鍵字.意思是告訴編譯器這個變量是"易變" , 一定要每次都從內存中重新 load 這個變量 , 不能再進行激進的優(yōu)化了.

class MyCounter{
    public volatile int flag = 0;
}

2.volatile 不能保證原子性 

volatile 與 synchronized 有本質的區(qū)別 , synchronized 保證原子性 , volatile 保證的是內存可見性.

代碼示例:

這是最初演示線程安全的代碼 , 兩個線程分別對 count 自增5萬次.

  • 去掉修飾 add 方法的 synchronized 關鍵字.
  • 給 count 變量加上 volatile 關鍵字. 

最終代碼執(zhí)行結果并不是預期的10w次. 

class Counter{
    public volatile int count;
    public void add(){
            count++;
    }
}
public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("count = "+counter.count);
    }

二.wait和notify

由于線程的特性是搶占式執(zhí)行隨機調度 , 因此線程之間的先后執(zhí)行順序難易預知 , 但實際開發(fā)中我們希望合理的協(xié)調多個線程之間的先后執(zhí)行順序.

完成這個協(xié)調工作主要涉及三個方法:

  • wait()/wait(long timeout).讓當前線程進入等待狀態(tài).
  • notify()/notifyAll().喚醒當前在對象上等待的方法.

Tips:wait(),notify(),notifyAll()都是Object類的方法.

通過上述介紹可以發(fā)現(xiàn) , wait 和 notify 與 join 和 sleep 在功能上有極大的重合之處 , 那么為什么還要開發(fā) wait 和 notify 呢?

因為 , 使用 join 就必須等待一個線程徹底執(zhí)行完才能換另一個線程. 如果我們想讓線程1執(zhí)行50% , 然后立即執(zhí)行線程2 , 顯然 join 達不到這個效果. 而且使用 sleep 必須指定休眠多長時間 , 但線程1執(zhí)行完畢需要花費多少時間并不好估計.所以使用 wait 和 notify 可以更好的解決上述問題.

1.wait方法

wait做的事情:

  • 先釋放鎖
  • 進行阻塞等待
  • 收到通知后 , 重新嘗試獲取獲取這個鎖 , 并且在獲取這個鎖后 , 繼續(xù)往下執(zhí)行.

代碼示例:

 public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        object.wait();
    }

運行該代碼出現(xiàn)異常 , 這是因為執(zhí)行 wait 操作 , 需先獲取當前線程的鎖 , 而當前線程并沒加鎖 , 所以會出現(xiàn)非法鎖狀態(tài)異常.這就好比 , 我的一個朋友還沒收到offer就已經開始挑選公司.

修改后代碼:

public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        synchronized (object) {
            System.out.println("wait 之前");
            object.wait();
            System.out.println("wait 之后");
        }
    }

通過運行結果可以得知 , 代碼執(zhí)行到object.wait()就進入阻塞.實際上在阻塞狀態(tài)之前 , wait 已經釋放了鎖 , 此時其他線程可以獲取到object對象的鎖 , 等到 wait 被喚醒后再嘗試獲取這個鎖.

舉個例子就是滑稽老鐵去ATM機取錢 , 當他進入銀行網點后鎖上門開始操作ATM機 , 結果發(fā)現(xiàn)ATM機沒錢 , 由于銀行外還有排隊等待辦理其他業(yè)務的人 , 他只能打開鎖后出去(相當于 wait 釋放鎖的操作) , 等待運鈔車來存錢(相當于 wait 的阻塞等待) , 當運鈔車把錢存進銀行 , 站在外面排隊等待的滑稽老鐵 , 又要和其他競爭進入銀行的機會.(重新嘗試獲取這個鎖) , 進入銀行后執(zhí)行取錢操作(重新加鎖后繼續(xù)執(zhí)行其他操作).

wait結束等待的條件

  • 其他線程調用該對象的 notify 方法.
  • wait 等待時間超時.(wait 有一個帶參方法 , 可以指定等待時間)
  • 其他線程調用該等待線程的 Interrupted 方法 , 導致 wait 拋出InterruptException異常.

2.notify方法

notify 方法是喚醒等待的線程.

  • notifty 方法同樣需要在加鎖的方法和加鎖的代碼塊中調用 , 該方法是用來喚醒那些因調用 wait方法而阻塞等待的線程 , 通知它們重新獲取對象鎖.
  • 如果有多個線程調用同一對象處于等待 , 則由線程調度器 , 隨機挑選一個呈 wait 狀態(tài)的線程喚醒.
  • 在 notify 方法執(zhí)行完畢后 , 當前線程不會立即釋放該對象鎖 , 要等待執(zhí)行 notify 方法的線程徹底退出加鎖代碼塊后才會釋放鎖對象.

代碼示例:

public class ThreadDemo3 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        Thread t1 = new Thread(() -> {
            //這個線程負責進行等待
            System.out.println("t1: wait 之前");
            try {
                synchronized (object) {
                    object.wait();
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("t1: wait 之后");
        });
        Thread t2 = new Thread(() -> {
            System.out.println("t2: notify 之前");
            //notify務必獲取鎖才能通知
            synchronized (object) {
                object.notify();
            }
            System.out.println("t2: notify 之后");
        });
        t1.start();
//此時讓 wait 先執(zhí)行,防止 notify 空打一炮.
        Thread.sleep(100);
        t2.start();
    }
}

觀察代碼執(zhí)行結果明顯符合預期. 

為什么 notify 方法也要在同步方法或同步代碼塊中?

同步方法或同步代碼塊指的是 , 加鎖的方法或加鎖的代碼塊.

代碼示例:

假設我們要實現(xiàn)一個阻塞隊列 , 如果不加同步代碼塊實現(xiàn)方法如下:

class BlockingQueen{
    Queue<String> queue = new LinkedList<>();
    Object lock = new Object();
    public void add(String data){
        queue.add(data);
        lock.notify();
    }
    public String take() throws InterruptedException {
        while (queue.isEmpty()){
            lock.wait();
        }
        //返回隊列的頭結點
        return queue.remove();
    }
}

這段代碼的核心思想是 , 當隊列為空時使用lock.wait()阻塞 , 如果調用add()方法添加元素時再采用lock.notify()喚醒.這段代碼可能產生以下問題:

  • 一個消費者調用 take() 方法獲取數(shù)據(jù) , 但queue.isEmpty() , 于是反饋給生產者.
  • 在消費者調用 wait 之前 , 由于CPU的調度 , 消費者線程被掛起 , 生產者調用add() , 然后notify().
  • 之后消費者調用wait().由于錯誤的條件判斷導致 wait 調用在 notify 之后.
  • 在這種情況下 , 消費者就會一直被掛起 , 生產者也不再生產 , 這個阻塞隊列就有問題.

由此看來 , 在調用 wait 和 notify 這種會掛起的操作時 , 需要一種同步機制保證

3.wait和sleep的對比

理論上 wait 和 sleep 沒有可比性 , 因為 wait 常用于線程間通信 , sleep 則是讓線程阻塞一段時間 , 唯一的相同點是都可以讓線程放棄執(zhí)行一段時間.

  • 1.wait 需要搭配 synchronized 關鍵字使用 , 而sleep則不需要.
  • 2.wait 是object 方法 , sleep則是Thread類的靜態(tài)方法.
  • 3.wait 被notify 喚醒屬于正常的業(yè)務范疇 , sleep 被Interrupt 喚醒需要報異常.

總結 

到此這篇關于JavaEE中volatile、wait和notify的文章就介紹到這了,更多相關JavaEE volatile、wait和notify內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

最新評論