volatile关键字

volatile是java虚拟机提供的轻量级的同步机制

  1. 内存可见性 (保证可见性)
  2. 不保证原子性
  3. 禁止指令重排 (保证有序性)

可见性

  • volatile修饰的共享变量有如下特点
    • 线程中读取这个变量时,每次都会读取主内存中最新的值,然后将其复制到工作内存
    • 线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存

有序性

  • 计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排

    • 源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系统的重排 -> 最终执行的指令
    • 单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。
    • 处理器在进行重排序时必须要考虑指令之间的 数据依赖性
    • 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一执行是无法确定的,结果无法预测
  • 是通过内存屏障来实现禁止指令重排

    • 内存屏障其实就是一种IVM指令,java内存模型的重排规则会要求java编译器在生成JVM指令时插入特定的内存屏障指令
      • 内存屏障之前的所有写操作都要写回主内存
      • 内存屏障之后的所有读操作都能获取 内存屏障之前所有写操作的最新结果
    • 写屏障
      • 告诉处理器在写屏障之前将所有存储在缓存中的数据同步到主内存,也就是说当看到 store屏障指令,就必须把该指令之前的所有写入指令执行完毕才能继续往下执行
    • 读屏障
      • 处理器在读屏障之后的读操作,都在读屏障之后执行。也就是说在Load屏障指令之后,就能保证后面读取的数据一定是最新的
    • 细分屏障类型(调用底层,底层的分类)
      • LoadLoad
      • StoreStore
      • LoadStore
      • StoreLoad

volatile使用场景

  • 单一赋值,如boolean、或者int
  • 状态标志,判断业务是否结束
  • 读多写少的场景,读不用加锁,写操作需要加锁
  • 单例模式下的双重检查锁,利用它的禁止指令重排

代码证明

  • 可见性
    • 工作线程对变量的更改在写回主内存,并将新修改的值同步给其他工作线程。
public class VolatileModel {

    volatile int model = 0;

    private void updateModel(){
        model = 60;
    }

    public static void main(String[] args) {

        VolatileModel volatileModel = new VolatileModel();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " come in.");

            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            volatileModel.updateModel();
            System.out.println(Thread.currentThread().getName() + " data:" + volatileModel.model);
        }, "AAA").start();

        while(volatileModel.model == 0){

        }
        System.out.println(Thread.currentThread().getName() + " data:" + volatileModel.model);

    }
}

  • 不保证原子性

public class Data {

    volatile int n = 0;

    public void add(){
        n++;
    }

    //result: 结果小于20000
    //原因:由于20个线程在进行N++的操作时,都是在自己的工作内存中完成,然后在写会主内存的
    // n++是非原子性的,javac -p查看class文件对应的 是3个操作,线程在执行这3个操作会出现插队情况,就会出现原本+1后写会主内存的值 实际上已经被其他线程+1过了
    public static void main(String[] args) {
        Data d = new Data();

        for(int i = 0;i< 20;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int j = 0;j < 1000;j++){
                        d.add();
                    }
                }
            }).start();
        }

        while (Thread.activeCount() > 2){
            Thread.yield();
        }

        System.out.println(d.n);
    }
}

  • 解决volatile原子性问题

    • synchronized关键字
    • Lock锁
    • AtomicInteger类
      • cas实现,自旋锁
  • 有序性

posted @ 2023-04-08 14:24  李勇888  阅读(11)  评论(0编辑  收藏  举报