併發一 java內存模型和線程安全

事先聲明 看zejian博客:併發專題 受益良多
https://blog.csdn.net/javazejian/article/category/6940462

1.線程不安全實例

涉及到JVM的運行時內存區域,這裏不做討論

通過一個代碼引發的問題去展開探討(不要糾結業務邏輯是否可優化)

public class MyThread extends Thread {

    private boolean isRestFlag = false;//休息的指令
    private boolean isWorkFlag = true;//幹活的指令

    public MyThread(String name) {
        super(name);
    }

    public MyThread(String name, boolean isRestFlag) {
        super(name);
        this.isRestFlag = isRestFlag;
    }

    private static int anInt = 0;//乾的事情大家都知道 多線程共享
    private static List<Integer> anIntList = new ArrayList<>();

    @Override
    public void run() {
        //老公就是一直幹活
        while (isWorkFlag) {
            // anInt++ 分解成本來的兩個動作;
            int temp = anInt;
            anIntList.add(anInt);
            anInt = temp + 1;
//            System.out.println(anInt++);
            if (isRestFlag) {
                //老婆下令結束 老公就可以休息了
                isWorkFlag = false;
                System.out.println(Thread.currentThread().getName() + "老公休息吧");
            }
        }

        System.out.println(Thread.currentThread().getName() + "繁忙的一天結束了");
    }


    public static void main(String[] args) throws InterruptedException {
        new MyThread("老公線程~~").start();//老公在幹活
        TimeUnit.SECONDS.sleep(1);
        new MyThread("老婆線程~~", true).start();//老婆說休息

        TimeUnit.SECONDS.sleep(3);
        System.out.println("主線程結束 anInt:" + anInt);

        System.out.println("anInt++ 中多線程情況下是否會取到重複的值" + anIntList.stream().collect(Collectors.groupingBy(Function
                .identity(), Collectors.counting())).entrySet().stream().filter(x -> x.getValue() > 1).findFirst());
    }
}
/* 打印信息  (備註 老公線程一直沒有結束)
老婆線程~~老公休息吧
老婆線程~~繁忙的一天結束了
主線程結束 anInt:9346170
Exception in thread "main" java.lang.NullPointerException: element cannot be mapped to a null key
*/
  • 線程間數據共享 場景1

這段程序很簡單,就是兩個線程 都操作兩個變量 然後發現老公線程一直沒有結束,空指針異常也是因爲老公線程一直在處理list,而造成list foreach過程中拋異常, 爲什麼,老公線程一直沒有結束–>isWorkFlag是實例變量,存儲在棧區,屬於線程獨有的,無法共享,這個問題是入門級問題,不懂的可以看下java運行時內存
所以對應的放入棧區,簡單的就是static修飾成類變量

    private static boolean isWorkFlag = true;//幹活的指令

    /* 打印信息  (備註老公線程一直沒有結束,但anInt++多線程下少被重複取到了)
     老婆線程~~老公休息吧
    老婆線程~~繁忙的一天結束了
    老公線程~~繁忙的一天結束了
    主線程結束 anInt:5880983
    anInt++ 中多線程情況下是否會取到重複的值Optional[5880982=2]
    */
  • 線程安全問題 場景2

線程間數據的傳遞解決了,來了個新問題,就是多線程下anInt++被重複取到,換個說法就是雖然線程間數據共享,but執行兩次+1操作得到還是1,這可以理解爲線程不安全,是由於anInt++兩步動作不是原子操作引起的

  • 又一種線程安全問題 場景3

我們再調整一下代碼,去掉anIntList相關的 在效果方面system.out和list.add一樣

//            anIntList.add(anInt);

/* 老公線程又沒有結束... 也就是static修飾的isWorkFlag 沒有共享!
老婆線程~~老公休息吧
老婆線程~~繁忙的一天結束了
主線程結束 anInt:127657172
anInt++ 中多線程情況下是否會取到重複的值Optional.empty
*/

現在是isWorkFlag沒有被看見,這也是線程安全問題,是由於數據可見性引起的

要解決線程安全問題得先了解java內存模型

2.java內存模型

JMM:java memory model

這裏寫圖片描述

JMM是一種抽象的規定,並沒有具體實現,規定了工作內存與主內存之間的交互方式,在多個線程同時操作主內存的情況下,如下圖,在A線程修改數據爲2的情況下,線程B是讀到的是1呢還是2呢,這是不確定的,而這種不確定性 就是線程不安全的根因(線程操作主內存數據時存在不確定性就是線程不安全),而JMM的規定可以解決這個問題,同時JMM也爲jvm與硬件內存的跨平臺提供的解決方式
這裏寫圖片描述

JMM規定了是圍繞原子性,可見性,有序性三個特性展開的
原子性:和數據庫原子性類似
可見性:讀取時屏蔽工作內存的值,直接讀取主內存的值,而寫時立即會寫主內存,同時通過這個內存屏障禁止指令重排序引起的多線程可見性問題(指令重排序知道cpu有這個優化手段就行)
有序性:使用內存屏障確保不會出現指令重排序,保證程序的有序性

對應的解決方案:

原子性:除了JVM自身提供的對基本數據類型讀寫操作的原子性外,對於方法級別或者代碼塊級別的原子性操作,可以使用synchronized關鍵字或者重入鎖(ReentrantLock)保證程序執行的原子性
可見性和有序性:volatile關鍵字

場景3之所以static變量仍未能讀取到主內存最新數據,是因爲一直運行anInt++,根本不需要去主內存讀取數據,所以isWorkFlag一直未刷新,而日常開發中較少碰到多線程情況下如此簡單的業務場景,而略複雜的業務場景都需要讀主內存數據,比如system.out/list.add(動態拓展時native方法)
所以上面代碼的解決方案就是保證可見性或加synchronized也行

private volatile static boolean isWorkFlag = true;

JMM與jvm運行期內存區域的關係

沒有關係,這不是一個層次的區分,java內存模型是一種抽象,jvm運行期內存區域是具體的數據存放劃分,兩者之間沒有直接關係,僅有一些相似之處就是jvm棧可以比擬工作內存,堆可以比擬主內存

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