Java工程师面试1000题221-Java的指令重排与volatile关键字

221、Java的指令重排与volatile关键字

如果要用一句话来概括volatile关键字的作用,我会这么说:使用volatile关键字,它能够使被修饰的变量在值发生改变时尽快地让其他线程知道。下面我们再来详细解释一下volatile关键字和指令重排。

首先,我们需要知道的是,编译器为了加快程序的运行速度,对一些变量的写操作会首先在速度更快的寄存器和CPU高速缓存中进行,然后才再将变量刷新到内存中,在这个过程中,变量的新值对其他线程是不可可见的。而volatile关键字的作用就是使被它修饰的变量的读写操作都必须要在内存中进行。

volatile关键字和synchronized关键字是有很大区别的:volatile的本质是告诉JVM当前的变量在内存中的值有可能已经在寄存器或者CPU缓存中发生过改变了,synchronized关键字则是锁定当前变量,不允许其他线程对该变量进行访问,只有当前线程可以访问该变量,其他线程被阻塞住。volatile仅仅能修饰变量,synchronized则可以用来修饰变量和方法。

volatile仅能实现变量的修改可见性,并不具备原子性,而synchronized则可以保证变量的修改可见性和原子性。volatile并不会造成线程的阻塞,而synchronized则有可能造成线程的阻塞,volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化。下面举例来验证上面所描述的内容:

看下面的程序:

public class ThreadOld implements Runnable {

    private boolean flag = false;
    
    @Override
    public void run() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        flag = true;
    }
    
    public boolean isFlag(){
        return flag;
    }
    
    public void setFlag(boolean flag){
        this.flag = flag;
    }

    public static void main(String[] args) {
        ThreadOld old = new ThreadOld();
        new Thread(old).start();

        //无限循环
        for(;;){
            if (old.isFlag()){
                System.out.println("flag: " + old.flag);
            }
        }
    }
}

在上面的程序里我们定义了一个线程以及一个死循环,其中flag初始值为false,执行线程(这里让线程休眠200毫秒为了便于演示),我们知道flag肯定会在执行线程后变为true,理论上会进入循环输出flag,可是结果并没有如我们所愿,我们执行程序,并没有任何输出啊。这是怎么回事?

我们需要先了解一下Java的内存模型:在Java中每一个线程都会有一个工作内存和主内存独立开来的,工作内存存放的是主内存中变量的值得拷贝,当数据从主内存复制到工作内存时,必须会出现两个动作:第一,由主内存执行读(read)操作,第二,由工作内存执行相应的load操作;当数据从工作内存复制到主内存时也会出现两个操作:第一个,由工作内存执行存储(store)操作,第二,由主内存执行相应的写操作(write)操作,每一个操作都是原子的,即执行期间不会被中断,对于普通变量,一个线程中更新的值不会立刻反应在其他线程中。

由于工作内存与主内存并非实时同步,就会出现上面的情况,如果需要在其他线程中立即可见,就需要使用volatile关键字。

那么什么又是指令重排?指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。也就是说,JVM为了执行效率会将指令进行重新排序,但是这种重新排序不会对单线程程序产生影响。

指令重排的基本原则

  • 程序顺序原则:一个线程内保证语义的串行性
  • volatile规则:volatile变量的写,先发生于读
  • 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
  • 传递性:A先于B,B先于C 那么A必然先于C
  • 线程的start方法先于它的每一个动作
  • 线程的所有操作先于线程的终结(Thread.join())
  • 线程的中断(interrupt())先于被中断线程的代码
  • 对象的构造函数执行结束先于finalize()方法

以下这几种情况,不可重排

  • 写后读 a = 1;b = a; 写一个变量之后,再读这个位置。
  • 写后写 a = 1;a = 2; 写一个变量之后,再写这个变量。
  • 读后写 a = b;b = 1; 读一个变量之后,再写这个变量。

也就是说,a,b前后存在依赖关系,不可重排。

volatile同样可防止指令重排。

猜你喜欢

转载自blog.csdn.net/qq_21583077/article/details/89185235
今日推荐