Java 併發之內存模型的重排序的Java代碼實例分析

一般在看JMM(Java內存模型)的時候,裏面有代碼會因爲種種原因優化,導致指令重排。也沒實際見過。也沒法驗證這個說法。

說是volatile這個關鍵詞可以1,禁止指令重排,2,內存可見。這都是理論,回頭就忘記了。

下面用實際例子,切身體會一下他這個重排序。

package com.lxk.jdk.jvm.resort;

import com.google.common.collect.Sets;

import java.util.Set;
import java.util.concurrent.CountDownLatch;

/**
 * 重排序導致問題
 *
 * @author LiXuekai on 2020/6/11
 */
public class Main {

    private static int x = 0, y = 0;
    private static int a = 0, b = 0;
    private static Set<String> sets = Sets.newHashSet();

    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        for (; ; ) {
            i++;
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            CountDownLatch latch = new CountDownLatch(1);

            Thread one = new Thread(() -> {
                try {
                    latch.await();
                } catch (InterruptedException ignore) {
                }
                a = 1;
                x = b;
            });

            Thread other = new Thread(() -> {
                try {
                    latch.await();
                } catch (InterruptedException ignore) {
                }
                b = 1;
                y = a;
            });
            one.start();
            other.start();
            latch.countDown();
            one.join();
            other.join();

            String result = "第" + i + "次 (" + x + "," + y + ")";
            sets.add("" + x + y);
            if (x == 0 && y == 0) {
                System.err.println(result + "   sets is " + sets.toString());
                break;
            } else {
                System.out.println(result + "   sets is " + sets.toString());
            }
        }
    }
}

運行結果截圖:

多線程之所以牛逼,就因爲看着代碼,很難猜到結局!當然,你技術牛b,也許就不存在這個問題了。

看這個結果是不是也有的捉摸不透?

稍微強行解釋一波:

i 用來記錄執行到哪一次了,在for循環裏面每次+1,這個簡單。

在for循環裏面,一次次的重複執行n次。不出重排序的結果,就一直循環。

弄個set就是想收集一下執行的幾種結果,直觀的看下全部情況。

CountDownLatch 一個多線程併發工具,latch翻譯過來就是門閂shuan的意思。

在代碼裏面啓動了2個線程,2個線程都new完之後,先後start()啓動,都啓動了之後,2個線程內部實現都有一個latch.await(),意思是2個線程啓動運行之後,到這個地方都得阻塞,只有在latch的值達到0之後,才能再次被執行,這個時候,這2個線程才能允許被執行,即有了執行權限,具體誰執行,怎麼執行就得靠cpu的隨機了。

之後再latch.countDown(),只有這個方法被調用之後,因爲在每次循環中countdownLatch的初始值都是設置的1,經過一次countdown(倒計時),就歸零了,線程1,2因await()阻塞的狀態就不再是阻塞狀態,而是改爲就緒狀態,等待機會,獲取到cpu時間片就能執行了,

線程1,2的join()方法,就是線程1,2不執行完,當前線程main線程需要掛起等待,直到1,2線程執行完,才能繼續。1,2兩個線程在同一起跑線上,開始執行,他們2個現在都是可執行的狀態的,但是誰先拿到執行權,即cpu時間片,誰就執行,完全不可控。

預測一下執行的結果:

情況1:1線程先執行完,之後2線程執行,則輸出結果是:x=0 y=1

情況2:2線程先執行完,之後1線程再執行,則輸出結果是:x=1 y=0

情況3:1線程在執行完a=1之後,cpu時間片沒了,改2執行了,2執行完之後,1再繼續,則輸出結果:x=1 y=1

情況4:2線程執行完b=1之後,cpu時間片沒了,改1執行了,1執行完之後,2再繼續,則輸出結果:x=1 y=1

情況5:12線程分別運行到a=1,b=1之後,都停了一下,然後再都啓動,則輸出結果是:x=1 y=1

怎麼分析,各種cpu時間片在2個線程來回切換,好像都不能出現結果 x=0 y=0的情況!

唯一能解釋的過去的就是,代碼被優化了,即指令重排了。

2個線程中,都有2個簡單的賦值操作。就單個線程來看,裏面的2個賦值操作,沒有什麼直接影響關係。誰先誰後都OK的感覺。

看下面的重排序的幾個優化。

如果2個線程裏面吧x=b 和 y=a 都優化一下,分別放在a=1 和 b=1前面,這樣就能解釋 00的由來了。

因爲根據上面的理論,在單獨的一個線程裏面,Java看來2個簡單賦值操作,沒有前後依賴關係的,交換一下順序是不會有問題的。所以,就有可能對這2個簡單賦值指令進行重排序。進而就導致了多線程的問題。

不是說volatile可以禁止指令重排嗎?

既然如此,那就對上面的代碼稍微改動,把四個int類型數據,在聲明的時候,加上volatile。看看還會不會出現,我們預測之後外的情況,也就是00

好的,00不見了,同時看見了Java代碼中的指令重排和volatile禁止指令重排的效果。

比簡單的文字描述來的更直接客觀。印象深刻。

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