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禁止指令重排的效果。

比简单的文字描述来的更直接客观。印象深刻。

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