3.volatile基本原理及缺陷

目录

  • 概述
  • 案例
    • 代码如下
    • 执行结果
  • 原理
    • 实现内存可见性的过程
    • 硬件上的内存屏障
    • 底层分析
      • java中的四种内存屏障
  • 缺陷
    • 代码
    • 执行结果
  • 结束

概述

想要多线程程序正确的执行,必须要保证原子性可见性有序性。只要有一个没有被保证,就有可能导致程序运行不正确。

案例

代码如下

package com.fun.demo;

public class DemoTicketVolatile {
    public static void main(String[] args) {
        TicketTask task = new TicketTask();
        new Thread(task, "窗口1").start();
        task.flag = false;
        System.out.println(task.flag);
    }

    static class TicketTask implements Runnable {

        public volatile boolean flag = true;

        @Override
        public void run() {
            System.out.println("了线程执行");
            while (flag) {

            }
            System.out.println("了线程结束");
        }
    }
}

执行结果

3.volatile基本原理及缺陷_第1张图片

原理

java语言对 volatile 的定义:java允许线程访问共享变量,为了确保共享变量被准确和一致的更新,线程应保证通过排他锁单独获得这个变量,即 volatile 可以保证多线程场景下变量的 可见性有序性。如果某变量用 volatile 修饰,则可以确保所有线程看到变量的值是一致的。

注意:volatiile 使用起来简单,但用好并不容易,对变量的操作一般会选择 JUC 中原子类,boolean 是它的一种常见用法。

实现内存可见性的过程

volatile 实现内存可见性的过程:

  • 线程写 volatile 变量的过程:
    • 1.改变线程本地内存中 volatile 变量副本的值
    • 2.将改变后副本的值从本地内存刷新至主内存
  • 线程读 volatile 变量的过程:
    • 1.从主内存中读取 volatile 变量的最新值至线程的本地内存中
    • 2.从本地内存中读取 volatile 变量的副本

硬件上的内存屏障

Load屏障,在x86上是 ifence 指令,在其它指令前插入 ifence 指令,可以让cpu寄存器中的数据失效,强制当前线程从主内存里面加载数据到cpu寄存器中

Store屏障,在x86上是 sfence 指令,在其它指令后插入 sfence 指令,能让当前线程写入cpu寄存器的最新数据 ,写入到主内存,并让其它线程可见

底层分析

volatile 实现内存可见性原理:内存屏障 (Memory Barrier)

内存屏障是一种cpu指令(系统层面的操作),用于控制特定条件下的重排序和内存可见性问题。
java编译器也会根据内存屏障的规则禁止重排序。

  • 写操作时,通过在写操作指令后加入一条store屏障指令,让本地内存中变量的值能够刷新到主内存中
  • 读操作时,通过在读操作前加入一条load屏障指令,及时读取到变量在主内存的值

java中的四种内存屏障

屏障 例子 详解
LoadLoad屏障 Load1;LoadLoad;Load2 LoadLoad中两个Load,前一个对应Load1,后一个对应Load2;保证Load1从主内存里读取过程完成,在Load2及后续读操作之前。
StoreStore屏障 Store1;StoreStore;Store2 StoreStore中两个Store,前一个对应Store1存储代码,后个对应Store2存储代码,在Store2及后续写操作执行前,保证Store1的写入操作已将数据写入到主内存里,确认Store1的写入操作对其它线程可见。
LoadStore屏障 Load1;LoadStore;Store2 LoadStore中的Load对应Load1加载代码,Store对应Store2存储代码,在Store2及后结代码写入操作执行前,保证Load1从主内存里读取完毕所需要的数据。
StoreLoad屏障 Store1;StoreLoad;Load2 在Load2及后续读取操作之前,保证Store1的写入操作已经将数据写入到主内存里,确认Store1的写入操作对其它处理器可见。

缺陷

代码

package com.fun.demo;

public class DemoTicketVolatile {
    public static void main(String[] args) {
        TicketTask task = new TicketTask();
        new Thread(task, "窗口1").start();
        new Thread(task, "窗口2").start();
        new Thread(task, "窗口3").start();

    }

    static class TicketTask implements Runnable {

        private volatile int tickets = 100;

        @Override
        public void run() {
            while (true) {
                if (tickets > 0) {
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "-正在卖票:" + tickets--);
                }
            }
        }
    }
}

执行结果

由下图可知,tickets-- 不是原子性操作。导致执行出了问题。
3.volatile基本原理及缺陷_第2张图片

结束

至此,volatile基本原理及缺陷 就结束了,如有疑问,欢迎评论区留言。

你可能感兴趣的:(并发编程,java,并发编程,volatile)