Java中Volatile关键字怎么使用

这篇文章将为大家详细讲解有关 Java中Volatile关键字怎么使用,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。

创新互联建站长期为上1000家客户提供的网站建设服务,团队从业经验10年,关注不同地域、不同群体,并针对不同对象提供差异化的产品和服务;打造开放共赢平台,与合作伙伴共同营造健康的互联网生态环境。为鸡西梨树企业提供专业的成都做网站、成都网站制作,鸡西梨树网站改版等技术服务。拥有十年丰富建站经验和众多成功案例,为您定制开发。

Volatile 可见性承诺

Java volatile关键字保证了跨线程更改线程间共享变量的可见性。这可能听起来有点抽象,让我们详细说明一下。

在多线程应用程序中,线程对 non-volatile 变量进行操作,出于性能原因,每个线程在处理变量时,可以将它们从主内存复制到CPU缓存中。如果你的计算机包含一个以上的CPU,每个线程可以在不同的CPU上运行。这意味着,每个线程可以将同一个变量复制到不同CPU的CPU缓存中。这就和计算机的组成和工作原理息息相关了,之所以在每一个 CPU 中都含有缓存模块是因为出于性能考虑。因为 CPU 的执行速度要比内存(这里的内存指的是 Main Memory)快很多,因为 CPU 要对数据进行读、写的操作,如果每次都和内存进行交互那么 CPU 在等待 I/O 这个过程中就消耗了大量时间,大部分时间都是在停滞等待而没有真正投入工作当中。所以为了解决这个问题就引入了CPU缓存。如下图所示:

Java中Volatile关键字怎么使用

这样就导致了一个问题同一个变量会被不同的 CPU 放在自己的缓存中,对该变量的读、写操作在缓存中进行。当然对于非共享数据来说这一点问题也没有,就比如函数内部的变量,但是对于共享数据来说就会造成多个 CPU 之间对该数据进行了操作但是别的 CPU 不知道这个数据发生了改变 ,依然使用旧的数据,最终导致程序不符合我们的预期。因为 CPU 是不知道你的程序内哪些数据是多线程共享数据,而那些数据不是,如果你不告诉 CPU 那么它默认都会认为这些数据都是不共享的,而各自在自己的缓存中随意操作。比如这个代码:

public class VolatileCase0 {
    public int counter = 0;
}

这个代码在多线程执行的环境下是不安全的,counter 是共享变量。假设两个 CPU 共同操作同一个 VolatileCase0 对象,如下图所示:

Java中Volatile关键字怎么使用Java中Volatile关键字怎么使用

目前这个情况下 counter 在两个 CPU 缓存中都存在,但是每个 CPU 对 counter 的操作对其他 CPU 来说是不可见的。因为此时我们并没有告知 CPU 和 CPU 缓存这个 counter 是一个共享内存变量。要解决多个 CPU 缓存之间变量写操作可见性的问题,就需要用 volatile 关键字来修饰这个 counter 。代码如下:

public class VolatileCase0 {
    public volatile int counter = 0;
}

接下来看一个例子程序:

public class VolatileCase1 {

    volatile boolean running = true;

    public void run() {
        while (running) {

        }
        System.out.println(Thread.currentThread().getName() + " end of execution ");
    }


    public void stop() {
        running = false;
        System.out.println(Thread.currentThread().getName() + " thread Modified running to false");
    }

    public static void main(String[] args) throws Exception {

        VolatileCase1 vc = new VolatileCase1();

        Thread t1 = new Thread(vc::run , "Running-Thread");

        Thread t2 = new Thread(vc::stop , "Stop-Thread");

        t1.start();
        TimeUnit.SECONDS.sleep(1);
        t2.start();

    }

}

如果对 running 变量不加 volatile 关键字,程序就会陷在 “Running-Thread”中一直执行而无法结束。加上了 volatile 关键字之后 “Running-Thread”会读取到被修改后的 running 值,这时就可以执行结束了。

Volatile 禁止指令重排序

首先需要解释一下什么是“指令重排序”。所谓指令重排序也就是 CPU 对程序指令进行执行的时候,会按照自己制定的顺序,并不是完全严格按照程序代码编写的顺序执行。这样做的原因也是出于性能因素考虑,CPU对一些可以执行的指令先执行可以提供总体的运行效率,而不是让CPU把时间都浪费在停滞等待上面。感兴趣的读者可以参考这篇文章:

感兴趣的读者也可以阅读 64-ia-32-architectures-software-developer-vol-3a-part-1-manual 这个开发手册。以下是该手册中对于指令重排序的一些描述:

Java中Volatile关键字怎么使用

译文:术语Memory Ordering 是指处理器通过系统总线向系统内存发出读(装入)和写(存储)的顺序。Intel 64和IA-32体系结构支持多种内存排序模型,具体取决于体系结构的实现。例如,Intel386处理器强制执行程序排序(通常称为强排序),在任何情况下,读写都是按指令流中发生的顺序在系统总线上发出的。

为了优化指令执行的性能,IA-32体系结构允许在Pentium 4、Intel Xeon和P6系列处理器中偏离称为处理器排序的强排序模型。这些处理器排序变体(在这里称为内存排序模型)允许性能增强操作,比如允许读优先于缓冲写。这些变化的目的是提高指令执行速度,同时保持内存一致性,即使在多处理器系统中也是如此。我们通过一个代码来证实CPU对指令的重排序:

public class MemoryOrderingCase1 {

    static int x = 0 , y = 0 , a = 0 , b = 0;


    public static void main(String[] args) throws Exception {

        while (true) {

            CountDownLatch latch = new CountDownLatch(2);
            x = 0;
            y = 0;
            a = 0;
            b = 0;

            Thread t1 = new Thread(() -> {
                a = 1;
                x = b;
                latch.countDown();
            });


            Thread t2 = new Thread(() -> {
                b = 1;
                y = a;
                latch.countDown();
            });

            t1.start();
            t2.start();

            latch.await();

            if (x == 0 && y == 0) {
                System.out.println("x = " + x + " , y = " + y + " , a = " + a + " , b = " + b);
                break;
            }
        }
    }

}

当 x = 0 同时 y = 0 的时候说明CPU在写指令完成之前执行了读指令。

另一个例子 Java Double checking locking 单例模式,代码如下:

public class MemoryOrderingCase2 {

    private static volatile MemoryOrderingCase2 INSTANCE;

    int a;
    int b;

    private MemoryOrderingCase2() {
        a = 1;
        b = 2;
    }

    public static MemoryOrderingCase2 getInstance() {
        if (MemoryOrderingCase2.INSTANCE == null) {
            synchronized (MemoryOrderingCase2.class) {
                if (MemoryOrderingCase2.INSTANCE == null) {
                    MemoryOrderingCase2.INSTANCE = new MemoryOrderingCase2();
                }
            }
        }
        return MemoryOrderingCase2.INSTANCE;
    }
}

在这个例子中如果 INSTANCE 取除掉 volidate 关键字就会导致问题的发生。假设有两个线程在访问 getInstance() 函数,执行序列如下:

1. 线程 1 进入 getInstance 函数 , INSTANCE 为 null ,并切当前没有线程持有锁定。

2. 线程 1 再次判断 INSTANCE 是否为 null ,结果为 true 。

3. 线程 1 执行 INSTANCE = new MemoryOrderingCase2() 。

4. 线程 1 执行 new MemoryOrderingCase2() 。

5. 线程 1 在堆内存中为对象分配了空间。

6. 线程 1 INSTANCE 指向了该对象,此时 INSTANCE 已经不为 null。

7. 线程 1 new MemoryOrderingCase2() 对象开始执行初始化过程,调用父类构造函数,给一些属性赋值等。

8. 线程 2 进入 getInstance 函数 ,判断 INSTANCE 不为 null ,将 INSTANCE 返回。

这里的问题在于 MemoryOrderingCase2 对象还没有完成全部的初始化过程,就被线程2暴漏给了外界。也就是说读操作在写操作还没有完成之前就发生了。

查看 getInstance() 函数的部分汇编代码:

0x0000000003a663f4: movabs $0x7c0060828,%rdx  ;   {metadata('org/blackhat/concurrent/date20200312/MemoryOrderingCase2')}
  0x0000000003a663fe: mov    0x60(%r15),%rax
  0x0000000003a66402: lea    0x18(%rax),%rdi
  0x0000000003a66406: cmp    0x70(%r15),%rdi
  0x0000000003a6640a: ja     0x0000000003a66557
  0x0000000003a66410: mov    %rdi,0x60(%r15)
  0x0000000003a66414: mov    0xa8(%rdx),%rcx
  0x0000000003a6641b: mov    %rcx,(%rax)
  0x0000000003a6641e: mov    %rdx,%rcx
  0x0000000003a66421: shr    $0x3,%rcx
  0x0000000003a66425: mov    %ecx,0x8(%rax)
  0x0000000003a66428: xor    %rcx,%rcx
  0x0000000003a6642b: mov    %ecx,0xc(%rax)
  0x0000000003a6642e: xor    %rcx,%rcx
  0x0000000003a66431: mov    %rcx,0x10(%rax)    ;*new  ; - org.blackhat.concurrent.date20200312.MemoryOrderingCase2::getInstance@17 (line 24)

  0x0000000003a66435: movl   $0x1,0xc(%rax)     ;*putfield a
                                                ; - org.blackhat.concurrent.date20200312.MemoryOrderingCase2::@6 (line 16)
                                                ; - org.blackhat.concurrent.date20200312.MemoryOrderingCase2::getInstance@21 (line 24)

  0x0000000003a6643c: movl   $0x2,0x10(%rax)    ;*putfield b
                                                ; - org.blackhat.concurrent.date20200312.MemoryOrderingCase2::@11 (line 17)
                                                ; - org.blackhat.concurrent.date20200312.MemoryOrderingCase2::getInstance@21 (line 24)

  0x0000000003a66443: movabs $0x76b907160,%rsi  ;   {oop(a 'java/lang/Class' = 'org/blackhat/concurrent/date20200312/MemoryOrderingCase2')}
  0x0000000003a6644d: mov    %rax,%r10
  0x0000000003a66450: shr    $0x3,%r10
  0x0000000003a66454: mov    %r10d,0x68(%rsi)
  0x0000000003a66458: shr    $0x9,%rsi
  0x0000000003a6645c: movabs $0xf6fd000,%rax
  0x0000000003a66466: movb   $0x0,(%rsi,%rax,1)
  0x0000000003a6646a: lock addl $0x0,(%rsp)     ;*putstatic INSTANCE
                                                ; - org.blackhat.concurrent.date20200312.MemoryOrderingCase2::getInstance@24 (line 24)

关于 Java中Volatile关键字怎么使用就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。


当前标题:Java中Volatile关键字怎么使用
文章分享:http://azwzsj.com/article/pssppe.html