架构系列——面试必问:volatile的可见性、防止指令重排序以及不能保证原子性的解决方式

简介: 架构系列——面试必问:volatile的可见性、防止指令重排序以及不能保证原子性的解决方式

前言

volatile的使用与线程安全关系密切,主要作用是使变量在多个线程间可见,另外也有防止指令重排的作用。

比如主内存中有变量a=0,线程1设置a=10,线程2再操作a的时候,是以a=10的基础上进行操作,否则会影响逻辑!

一、volatile的可见性

要了解volatile的可见性,首先得了解java内存模型

java内存模型

Java内存模型由Java虚拟机规范定义,用来屏蔽各个平台的硬件差异。简单来说:

1. 所有变量储存在主内存。

2. 每条线程拥有自己的工作内存,其中保存了主内存中线程使用到的变量的副本。

3. 线程不能直接读写主内存中的变量,所有操作均在工作内存中完成。

线程,主内存,工作内存的交互关系如下图所示

如下列代码所示,rt启动之后修改isRunning的值为false,此时while循环不会停止,因为run方法里得不到改变之后的isRunning

解决:使用volatile修饰isRunning,这样当isRunning的值改变之后,会立即刷新到主内存里,工作内存也能立即获取到新的值

public class RunThread extends Thread {
  private boolean isRunning = true;
  private void setRunning(boolean isRunning){
    this.isRunning = isRunning;
  }
  public void run () {
    System.out.println("进入run方法");
    while(isRunning == true){
      //...
    }
    System.out.println("线程停止");
  }
  public static void main(String[] args) {
    RunThread rt = new RunThread();
    rt.start();
    try {
      Thread.sleep(3000);
      rt.setRunning(false);
      System.out.println("isRunning的值已经被设置成false");
      Thread.sleep(1000);
      System.out.println(rt.isRunning);
    } catch (InterruptedException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
  }
}

二、volatile能防止指令重排

如下列代码所示,这是单例模式的双检锁写法

public class SingletonTest {
    private volatile static SingletonTest instance = null;
    private SingletonTest() { }
    public static SingletonTest getInstance() {
        if(instance == null) {
            synchronized (SingletonTest.class){
                if(instance == null) {
                    instance = new SingletonTest();  //非原子操作
                }
            }
        }
        return instance;
    }
}

我们看到instance用了volatile修饰,由于 instance = new SingletonTest();可分解为:

1.memory =allocate(); //分配对象的内存空间
2.ctorInstance(memory); //初始化对象
3.instance =memory; //设置instance指向刚分配的内存地址.m

操作2依赖1,但是操作3不依赖2,所以有可能出现1,3,2的顺序,当出现这种顺序的时候,虽然instance不为空,但是对象也有可能没有正确初始化,会出错。

而使用volatile修饰instance之后,不会出现乱序的行为!

三、volatile不保证原子性以及解决方式

1.什么是原子性?

下列语句中,哪些是原子性操作?

x = 10;         //语句1
y = x;          //语句2
x++;            //语句3
x = x + 1;      //语句4

语句1 是直接将数值 10 赋值给 x,也就是说线程执行这个语句的会直接将数值 10 写入到工作内存中;


语句2 实际上包含两个操作,它先要去读取 x 的值,再将 x 的值写入工作内存。虽然,读取 x 的值以及 将 x 的值写入工作内存这两个操作都是原子性操作,但是合起来就不是原子性操作了;


同样的,x++ 和 x = x+1 包括3个操作:读取 x 的值,进行加 1 操作,写入新的值。


只有 语句1 的操作具备原子性。也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作!

2.举例

如下列代码所示,使用volatile修饰的变量count,利用10个线程分别对count进行++操作,而根据上面的表述,++操作不是原子操作!每个线程加1000个数,打印的结果中一定有一个是10000才是对的,但是实际上并不是这样!因为volatile不保证原子性!

public class VolatileNoAtomic extends Thread{
  private static volatile int count;
//  private static AtomicInteger count = new AtomicInteger(0);
  private static void addCount(){
    for (int i = 0; i < 1000; i++) {
      count ++;
//      count.incrementAndGet();
    }
    System.out.println(count);
  }
  public void run(){
    addCount();
  }
  public static void main(String[] args) {
    VolatileNoAtomic[] arr = new VolatileNoAtomic[10];
    for (int i = 0; i < arr.length; i++) {
      arr[i] = new VolatileNoAtomic();
    }
    for (int i = 0; i < arr.length; i++) {
      arr[i].start();
    }
  }
}

解决:


方法1:使用原子类Atomic类的系列对象,这样既不会阻塞,又能保证原子性!


方法2:使用synchronized修饰addCount方法,这样做的话,线程同步之后会有阻塞,运行时间加长,而且volatile将会失效,不建议这么改


方法3:使用Lock加锁,当然,跟方法2一样的有阻塞参考文献:

[1].架构系列——线程安全的简单探索

[2].彻底理解volatile关键字

[3].Java中volatile关键字的最全总结

[4].volatile关键字的作用、原理

[5].并发中的volatile

相关文章
|
15天前
|
存储 缓存 Java
大厂面试高频:Volatile 的实现原理 ( 图文详解 )
本文详解Volatile的实现原理(大厂面试高频,建议收藏),涵盖Java内存模型、可见性和有序性,以及Volatile的工作机制和源码案例。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:Volatile 的实现原理 ( 图文详解 )
|
2月前
|
设计模式 Java 关系型数据库
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
本文是“Java学习路线”专栏的导航文章,目标是为Java初学者和初中高级工程师提供一套完整的Java学习路线。
434 37
|
1月前
经典面试题:用预处理指令#define 声明一个常数,用以表明1年中有多少秒(忽略闰年问题)
在 C 语言中,使用 `#define` 预处理指令可以为常量命名,提高代码可读性和易维护性。通过基本时间单位换算(1 年 = 365 天 × 24 小时 × 60 分钟 × 60 秒),可以计算出一年中的总秒数,并将其定义为 `SECONDS_IN_A_YEAR`。示例代码展示了如何定义和打印这一常量,最终输出一年中有 31536000 秒。
69 15
|
3月前
|
存储 NoSQL Java
一天五道Java面试题----第十一天(分布式架构下,Session共享有什么方案--------->分布式事务解决方案)
这篇文章是关于Java面试中的分布式架构问题的笔记,包括分布式架构下的Session共享方案、RPC和RMI的理解、分布式ID生成方案、分布式锁解决方案以及分布式事务解决方案。
一天五道Java面试题----第十一天(分布式架构下,Session共享有什么方案--------->分布式事务解决方案)
|
3月前
|
存储 消息中间件 缓存
这些年背过的面试题——架构设计篇
本文是技术人面试系架构设计篇,面试中关于架构设计都需要了解哪些内容?一文带你详细了解,欢迎收藏!
|
3月前
|
缓存 安全 Java
面试官:说说volatile应用和实现原理?
面试官:说说volatile应用和实现原理?
46 1
|
3月前
|
缓存 Java
【多线程面试题二十三】、 说说你对读写锁的了解volatile关键字有什么用?
这篇文章讨论了Java中的`volatile`关键字,解释了它如何保证变量的可见性和禁止指令重排,以及它不能保证复合操作的原子性。
|
3月前
|
缓存 Java 编译器
一文搞懂volatile面试题
这篇文章是关于Java关键字volatile的详细介绍和分析,volatile是多线程访问共享变量时保证一致性的方案,性能优于synchronized,但不保证操作原子性,需要同步处理。
|
4月前
|
缓存 安全 Java
Java面试题:解释volatile关键字的作用,以及它如何保证内存的可见性
Java面试题:解释volatile关键字的作用,以及它如何保证内存的可见性
77 4
|
4月前
|
设计模式 安全 Java
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
80 1

热门文章

最新文章

下一篇
无影云桌面