java筑基--final关键字理解再进阶,重排序探究

final关键字理解再进阶,实现原理探究

final的java基本用法移步下面的链接:

https://blog.csdn.net/weixin_46039528/article/details/130519663?spm=1001.2014.3001.5501

1. final进阶

1.1 final的重排序规则
1.1.1 重排序的概念理解

​ 重排序指的是编译器或处理器为了优化程序性能而对指令的顺序进行重新排列的行为。重排序可能会改变程序的执行结果,从而导致程序出现错误。在java中,final域是一种特殊类型的变量,当一个final域被初始化后,其值就不能被修改了,但是,在多线程的环境下,final域的写入操作,读取操作可能会发生重排序,导致其值在初始化完成之前被读取到,从而引发逻辑错误。

举个例子来说明:假设有一个对象类型的final域,它的构造函数如下:

class FinalFieldExample {
    
    
    final int x;
    int y;

    public FinalFieldExample() {
    
    
        x = 3;
        y = 4;
    }
}

假设现在有两个线程T1和T2同时访问FinalFieldExample的实例对象,T1执行构造函数中的x=3操作,T2读取x的值,并且看到了x=0的结果。这是因为T2读取x域的操作在T1执行x = 3操作之前发生了重排序,从而导致T2读取到了未初始化的x域的值。为了避免这种情况的发生,可以使用volatile修饰final域,或者通过synchronized和锁来保证可见性和有序性。

1.2 写final重排序
  • 根据Java内存模型规范,JMM禁止编译器把final域的写重排序到构造函数之外。

这是因为final域的写入操作具有特殊的语义,即在对象构造完成之前,final域必须被正确地初始化。如果编译器将final域的写入操作重排序到构造函数之外,可能会导致final域的值在构造函数执行完成之前被其他线程所见,从而破坏final域的语义。

以下是一个简单的Java代码示例,用于验证final域写入操作不会被重排序到构造函数之外:

public class FinalFieldExample {
    
    
    private final int finalField;

    public FinalFieldExample() {
    
    
        finalField = 42;
    }

    public int getFinalField() {
    
    
        return finalField;
    }
}

在上面的示例中,finalField是一个final域,它在构造函数中被初始化为42。如果编译器将finalField的写入操作重排序到构造函数之外,那么在调用getFinalField方法时,可能会返回一个未被正确初始化的finalField值。但是,根据JMM的规定,编译器不得将finalField的写入操作重排序到构造函数之外,因此getFinalField方法总是返回42。

为了证明JVM的重排序行为,我们需要在多线程环境下运行该代码。为了简化问题,我们使用单个线程访问FinalFieldExample对象。以下是这个示例的Java代码:

扫描二维码关注公众号,回复: 16770296 查看本文章
public class Main {
    
    
    public static void main(String[] args) {
    
    
        FinalFieldExample example = new FinalFieldExample();

        int beforeConstruction = example.getFinalField();

        for (int i = 0; i < 1000000; i++) {
    
    
            Math.sin(i);
        }

        int afterConstruction = example.getFinalField();

        if (beforeConstruction != afterConstruction) {
    
    
            System.out.println("重排序了");
        } else {
    
    
            System.out.println("未重排序");
        }
    }
}

在这个示例中,我们首先创建FinalFieldExample对象,然后立即访问其final字段。然后,我们模拟一些CPU工作,然后再次访问final字段。如果JVM在创建FinalFieldExample对象时不遵循初始化顺序规则,则可能出现不同的字段值。因此,如果在第二次访问final字段时字段值发生了变化,就可以证明JVM进行了重排序。

  • 编译器会在final域写之后,构造函数return之前,插入一个storestore屏障。这个屏障可以禁止处理器把final域的写重排序到构造函数之外。

我们可以通过编写一个包含final域的类,并在构造函数中对其进行初始化和赋值操作,在另一个线程中读取该final域的值来验证上述行为。

具体地,我们可以在主线程中创建FinalFieldExample对象,并启动一个新线程,在该线程中读取FinalFieldExample对象的final域x的值。由于在主线程中,编译器会在final域写操作之后、构造函数return之前,插入storestore屏障,因此我们可以保证在新线程中读取到的final域x的值一定是已经完成初始化和赋值操作后的值,而不会是默认值或未初始化的值。

以下是示例代码:

class FinalFieldExample {
    
    
    final int x;
    int y;

    public FinalFieldExample() {
    
    
        x = 3;
        y = 4;
    }

    public int getX() {
    
    
        return x;
    }
}

public class Main {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        FinalFieldExample obj = new FinalFieldExample();
        Thread t = new Thread(() -> {
    
    
            System.out.println(obj.getX());
        });
        t.start();
    }
}

输出:3 表示final域x已经成功初始化并赋值为3,没有发生重排序的情况。

1.3 读final域重排序

​ 读final域重排序规则是指,在一个线程中,初次读对象引用和初次读该对象包含的final域,Java内存模型(JMM)会禁止这两个操作的重排序。处理器会在读final域操作的前面插入一个LoadLoad屏障。这条规则存在的主要原因是,虽然读对象的引用和读该对象的final域存在间接依赖性,但有一些处理器会重排序这两个操作,因此需要这种屏障来确保线程安全。

具体地,我们可以在主线程中创建FinalFieldExample对象,并启动两个新线程,一个线程在构造函数执行完成后,输出final域x的值,另一个线程在构造函数执行完成前,读取final域x的值。由于final域的写操作与构造函数执行完成前后有序关系,因此,我们可以保证在新线程中读取到的final域x的值是已经完成初始化和赋值操作后的值,而不会是默认值或未初始化的值。

class FinalFieldExample {
    
    
    final int x;
    int y;

    public FinalFieldExample() {
    
    
        x = 3;
        y = 4;
    }

    public int getX() {
    
    
        return x;
    }
}

public class Main {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        FinalFieldExample obj = new FinalFieldExample();
        Thread t1 = new Thread(() -> {
    
    
            System.out.println(obj.getX());
        });
        Thread t2 = new Thread(() -> {
    
    
            try {
    
    
                Thread.sleep(2000);
                System.out.println(obj.getX());
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

在该示例代码中,我们首先定义了一个FinalFieldExample类,该类包含final域x和非final域y。接着在主线程中创建FinalFieldExample对象obj,并启动了两个新线程t1和t2。线程t1会在构造函数执行完成后,输出final域x的值。线程t2会等待2秒钟,然后读取final域x的值。在主线程中,我们通过调用join()方法,使得主线程等待t1和t2线程的执行完毕。

运行上述代码后,我们会发现,在t1线程中输出的final域x的值是3,而在t2线程中读取到的final域x的值也是3。由于final域的初始化顺序与构造函数执行前后关系有序,而且JMM规范会禁止读对象引用和读包含final域的操作之间的重排序,因此我们可以得出结论:读final域重排序规则,确实有效地避免了final域在多线程环境下出现逻辑错误。

猜你喜欢

转载自blog.csdn.net/weixin_46039528/article/details/130545402