关于java:深入理解volatile

36次阅读

共计 3659 个字符,预计需要花费 10 分钟才能阅读完成。

文章链接:深刻了解 volatile

volatile 原理

在 Java 并发编程中 synchronized 和 volatile 扮演者重要的角色,volatile 是轻量级的 synchronized,它在多处理器开发中保障了共享变量的可见性和程序执行的有序性。如果 volatile 应用失当的话,它比 synchronized 的应用和执行老本更低,因为它不会引起线程上下文切换和调度。咱们理解一下计算机,例如在咱们工作当中大多数都是多核,因为 CPU 和物理主存速度不统一问题,为了解决 CPU 读取内存指令和数据效率问题,诞生了 CPU 高速缓存。

private volatile instance = new Singleton();

   
在生成汇编代码时会在 volatile 润饰的共享变量进行写操作的时候会多出 Lock 前缀的指令 ,Lock 前缀指令在多核处理器下会产生两件事。

1. 将以后处理器缓存行的数据写回零碎内存
2. 这个写回内存的操作会使得其余 CPU 里缓存了该内存地址的数据有效

为了进步处理器速度,首先处理器不间接和内存进行通信,而是先将零碎内存的数据读到高速缓存(L1,L2,L3)后再进行操作,然而操作何时会写到内存,如果对申明 volatile 的变量进行写操作,JVM 就会向处理器发送一条 Lock 前缀的指令,将这个变量所在缓存行的数据写到零碎内存。然而就算写到内存,如果其余处理器存在的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保障各处理器的缓存是统一的,就会实现缓存一致性协定,每个处理器通过嗅探在总线上流传的数据来查看本人缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址别批改,就会将以后处理器的缓存行设置为有效状态,当处理器对这个数据进行批改操作的时候,会从新从零碎内存中把数据读到处理器缓存里。

因而,咱们得出一下论断:

1.Lock 前缀的指令会引起处理器缓存写回内存;
2. 一个处理器的缓存回写到内存会导致其余处理器的缓存生效;
3. 当处理器发现本地缓存生效后,就会从内存中重读该变量数据,即能够获取以后最新值。

这样针对 volatile 变量通过这样的机制就使得每个线程都能取得该变量的最新值,即可见性。

可见性

那么上面咱们通过代码证实 volatile 如何保障可见性?保障不同线程对这个变量进行操作时的可见性, 即变量一旦扭转所有线程立刻能够看到

/**
 * 保障可见性
 */
public class ResourceData {

    // voliate 关键字能保障变量的可见性,多个线程批改同一个变量时,一个线程批改完,另一个线程获取到的是批改之后的值。private volatile int number = 0;

    public void add() {this.number = 10;}


    public static void main(String[] args) {ResourceData resourceData = new ResourceData();

        new Thread(() -> {System.out.println(Thread.currentThread().getName() + "\t come in");
            try {TimeUnit.SECONDS.sleep(3);
                resourceData.add();
                System.out.println(Thread.currentThread().getName() + "\t update number value" + resourceData.number);
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }, "AA").start();


        // 第二个线程是 main
        while (resourceData.number==0) { }
        System.out.println(Thread.currentThread().getName()+"\t mission is over ,main get number value :" +resourceData.number);
    }
}
AA  come in 
AA  update number value 10

如果共享变量不加 volatile,没有可见性,程序无奈进行,加了 volatile 保障可见性,程序能够进行。

原理解释:

1. 没有增加 volatile 关键字, 线程 A 对共享变量扭转了当前将 number 批改为 10, 主线程 (线程 B) 拜访 number 的值还是 0, 这就是不可见。

2. 增加 volatile 之后, 线程 A 对共享数据进行了扭转当前, 那么 main 线程再次拜访,number 的值就是扭转之后的 number=10

原子性
不保障原子性,代码实现:

/**
 * 不保障原子性
 */
public class ResourceData2 {

    // volatile 关键字不能保障变量的原子性,当多个线程批改一个变量的时候可能回呈现写失落的状况。// 如何保证数据的原子性呢
    private volatile int number = 0;

    // AtomicInteger 包装类能够保障变量的原子性
    AtomicInteger atomicInteger = new AtomicInteger();
    public  void addAtomic() {atomicInteger.getAndIncrement();
    }

    public void add() {number++;}

    public static void main(String[] args) {ResourceData2 resourceData = new ResourceData2();
        for (int i = 0; i <20 ; i++) {new Thread(()-> {for (int j = 0; j < 1000; j++) {resourceData.add();
                        resourceData.addAtomic();}
            },String.valueOf(i)).start();}

        // 期待下面的线程全副计算完,再通过 main 线程获取最终的后果
        while (Thread.activeCount()>2) {
            // 指 main 和 Gc
            Thread.yield();}
        System.out.println(Thread.currentThread().getName()+"\t finally number value :"
                +resourceData.number+"\t finally atomicInteger value :"+resourceData.atomicInteger);
    }
}

预期后果是:20000,然而理论后果是:19778,那么是什么起因导致的呢?

首先对于一读一写操作, 不会有数据问题,因为假如主内存的共享变量 number=1, 须要对主内存的 number++ 解决, 对于两个线程 T1、T2 如果是一读一写的操作是不会有数据失落的状况, 某一时刻,t1 抢到 CPU 的执行权, 将共享数据读回 T1 的工作内存, 进行 number++ 的操作, 这个时候 number=2, 将 2 从工作内存写回到主内存中。写回后马上告诉 T2 线程, 将 number= 2 读到 T2 的工作线程, 所以不会造成数据失落问题。

对于两个写, 会呈现数据问题,假如主内存的共享变量 number=0, 须要对主内存进行 10 次的 number++ 解决, 最终的后果就是 10, 对于两个线程 T1、T2 如果是两个写的操作会造成数据失落的状况,T1 和 T2 将主内存的共享数据读取到各自的工作内存去, 某一时刻,T1 线程抢到 CPU 的执行权, 进行 number++ 的解决, 将工作内存中的 number= 1 写回到主内存中, 就在这一刻,T2 也抢到 CPU 执行权, 进行 number++ 的解决, 这个时候 number++ 后的后果也等于 1,T1 将 number= 1 写回到主内存中去, 并告诉 T2 线程, 将主内存中的 number= 1 读到 T2 的工作内存中去, 这个时候对于 T2, 它之前也进行了一次 number++ 的操作将会有效, 回从新进行一次 number++ 的操作。这也数据也就写丢了一次, 那么 10 次 number++ 后的后果也就不会等于 10。

禁止指令重排

重排序是指编译器和处理器为了优化程序性能而对指令序列进行从新排序的一种伎俩, 有时候会改变程序语句的先后顺序 (不存在数据依赖关系, 能够重排序; 存在数据依赖关系, 禁止重排序) 重排序的分类和执行流程

1. 编译器优化的重排序: 编译器在不扭转单线程串行语义的前提下,能够从新调整指令的执行程序
2. 指令级并行的重排序: 处理器应用指令级并行技术来讲多条指令重叠执行,若不存在数据依赖性,处理器能够扭转语句对应机器指令的执行程序
3. 内存零碎的重排序: 因为处理器应用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是乱序执行

数据依赖性: 若两个操作拜访同一变量, 且这两个操作中有一个为写操作, 此时两操作间就存在数据依赖性(存在数据依赖关系,会禁止重排序,因为会导致程序运行后果不同),如果不存在依赖关系,能够从新排序。

文章链接:深刻了解 volatile

👉 如果本文对你有帮忙的话,欢送点赞|关注,非常感谢

正文完
 0