首页 > 其他分享 >面试10000次依然会问的【volatile】,你还不会?

面试10000次依然会问的【volatile】,你还不会?

时间:2023-11-02 21:00:39浏览次数:69  
标签:线程 10000 变量 synchronized 面试 volatile 内存 操作

面试10000次依然会问的【volatile】,你还不会?_同步机制

volatile关键字的定义

volatile是Java语言提供的一种轻量级的同步机制,主要用于「确保变量的修改对其他线程是立即可见的」,以及「防止指令重排序」。使用volatile修饰的变量,其读写操作直接作用于主存,而不是线程的工作内存。

这意味着一旦一个线程修改了volatile变量的值,其他线程立即就能看到这个修改。

volatile变量的内存语义主要体现在「两方面」

一是确保变量修改的可见性

二是禁止对其进行指令重排序。

虽然volatile可以保证内存可见性和禁止指令重排序,但它不能保证复合操作的原子性。

面试10000次依然会问的【volatile】,你还不会?_Java_02

volatile的内存语义

volatile关键字在Java中提供了一种轻量级的同步机制,主要体现在内存可见性和禁止指令重排序这两方面。

当一个变量被声明为volatile后,「对这个变量的写操作会立即刷新到主存」,而读操作会直接从主存中进行,这确保了不同线程间对该变量操作的可见性。此外,volatile变量的读写操作前后都会插入内存屏障,防止指令重排序,确保代码执行的顺序符合程序员的预期。

在底层实现上,volatile变量的读写操作通常会生成带有「lock前缀」的指令,这些指令会锁定被操作变量对应的缓存行,并将其写回到主存,同时使其他处理器的缓存行无效,从而保证了不同处理器间对volatile变量操作的可见性和有序性。

volatile与Java内存模型

在Java中,volatile是一个关键字,用于确保变量的修改对所有线程立即可见,从而避免了数据脏读的现象。当一个变量被声明为volatile时,Java内存模型(Java Memory Model, JMM)确保所有线程对这个变量的读写都是直接操作主内存,而不是工作内存。这意味着线程对volatile变量的修改会立即被其他线程所感知,确保了数据的“可见性”。

volatile提供了一种轻量级的同步机制,相比于synchronized,它不会引起线程的阻塞。但是,volatile并不适用于所有情况,特别是在涉及到复合操作时,仍然需要使用synchronized来保证线程安全。例如,在执行自增操作时,即使变量被声明为volatile,操作也不是原子的,仍然需要额外的同步措施来保证线程安全。

在使用volatile时,还需要注意其对指令重排序的影响。volatile变量的读写操作不会被重排序,这保证了代码的执行顺序符合预期,避免了潜在的并发问题。

面试10000次依然会问的【volatile】,你还不会?_Java_03

volatile的使用场景

在多线程环境下,为了保证线程安全和数据的实时可见性,我们可以使用 volatile 关键字。volatile 关键字能够确保变量的修改对其他线程立即可见,从而避免了数据脏读的问题。这是因为被 volatile 修饰的变量,当一条线程修改了这个变量的值,新值对其他线程来说是立即可见的,JVM 会立即将这个变量的值刷新到主内存中,当其他线程需要读取这个变量时,会直接从主内存中读取新值。而普通变量则不能保证这一点,普通变量的值可能会被缓存在线程的工作内存中,导致其他线程读到的是旧值。

以下是一些 volatile 的使用场景:

  1. 「状态标记」:可以使用 volatile 关键字来标记一个变量的状态,例如是否停止线程。当一个线程修改了这个状态时,其他线程能够立即看到这个改变,并作出相应的响应。
volatile boolean flag = false;
public void writeFlag() {
    flag = true; // 写操作,立即刷新到主存
}
public void readFlag() {
    if (flag) {
        // 读操作,直接从主存中读取flag的最新值
        // 执行相关操作
    }
}
  1. 「单例模式的实现 - 双重检查锁定(DCL)」
class Singleton {
    private volatile static Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

在这个例子中,instance 变量被声明为 volatile,确保当一个线程创建单例实例时,其他所有线程都能看到这个新创建的实例。

请注意,虽然 volatile 可以保证单个变量读/写的原子性,但复合操作(如自增、检查后行动等)仍然需要额外的同步措施。

面试10000次依然会问的【volatile】,你还不会?_同步机制_04

volatile与synchronized的比较

在Java中,volatilesynchronized都是用于多线程编程的关键字,但它们在功能和使用上有着明显的区别。

volatile是一种轻量级的同步机制,它主要用于确保变量的修改对其他线程是立即可见的,以及防止指令重排序。使用volatile修饰的变量,其读写操作直接作用于主存,而不是线程的工作内存。这意味着一旦一个线程修改了volatile变量的值,其他线程立即就能看到这个修改。然而,volatile不能保证复合操作的原子性。例如,volatile变量的自增操作就不是原子性的。

synchronized是一种重量级的同步机制,它不仅能保证变量的修改对其他线程的可见性,还能保证复合操作的原子性。当一个线程访问某对象的synchronized方法或代码块时,其他试图访问该对象的synchronized方法或代码块的线程将被阻塞。这提供了一种互斥的手段,确保同一时刻只有一个线程能执行某个方法或代码块,从而保证了线程安全。

虽然synchronized能够保证线程安全,但它也有性能开销,特别是在高并发的环境下。因此,在选择使用哪种同步机制时,需要根据具体的应用场景和需求来决定。如果对性能要求较高,且操作比较简单,可以优先考虑volatile。如果需要保证复合操作的原子性,或者需要一种更强的线程同步机制,应该使用synchronized

volatile的限制

在Java中,volatile关键字是一种轻量级的同步机制,用于确保变量的修改对其他线程立即可见,并防止指令重排序。然而,volatile并不是万能的,它也有一些限制和不适用的场景。

1.volatile不能保证复合操作的原子性。例如,对于自增操作i++,虽然你可以将i声明为volatile变量,但i++操作实际上包括三个步骤:读取i的值,将值加1,写回新值。这三个步骤不是原子性操作,其他线程可能在这三个操作之间执行,导致不正确的结果。

2.volatile不适用于变量之间有依赖关系的情况。如果一个变量的值依赖于另一个变量的值,或者变量的值需要根据某些条件来更新,仅仅使用volatile是不够的,你可能需要使用synchronized或者java.util.concurrent包下的原子类。

3.volatile也不能替代锁机制。锁不仅可以保证变量操作的原子性,还可以保证变量操作的有序性,并且提供了一种机制来实现线程间的协作。

总结

volatile是Java中的一个轻量级同步机制,主要用于确保变量的修改对所有线程立即可见,从而避免了数据脏读的现象。它通过直接操作主内存来实现变量的读写,确保了变量的可见性和有序性,但并不能保证复合操作的原子性。与synchronizedLock相比,volatile不会引起线程的阻塞,因此开销更小,适用于一些简单的同步场景,如状态标记、单例模式等。

虽然volatile提供了一种「轻量级的同步机制」,但它并不适用于所有情况,特别是在涉及到复合操作时,仍然需要使用synchronizedLock来保证线程安全。在使用volatile时,开发者需要仔细考虑其适用场景,并注意其使用的限制,以避免出现线程安全问题。

volatile是Java并发编程中的一个重要工具,但需要谨慎使用,并结合其他同步机制来保证线程安全。

标签:线程,10000,变量,synchronized,面试,volatile,内存,操作
From: https://blog.51cto.com/u_16326109/8154885

相关文章

  • 美团面试:Redis 除了缓存还能做什么?可以做消息队列吗?
    这是一道面试中常见的Redis基础面试题,主要考察求职者对于Redis应用场景的了解。即使不准备面试也建议看看,实际开发中也能够用到。内容概览:Redis除了做缓存,还能做什么?分布式锁:通过Redis来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于Redisson来实现分布......
  • Java面试题:链表-反转链表
    问题描述给定一个单链表的头结点pHead(该头节点是有值的,比如在下图,它的val是1),长度为n,反转该链表后,返回新链表的表头。如当输入链表{1,2,3}时,经反转后,原链表变为{3,2,1},所以对应的输出为{3,2,1}。示例输入:{1,2,3}返回值:{3,2,1}原题地址:https://www.nowcoder.com/practice/7......
  • Java面试题2
    Java面试题(第二天)1.重载和重写的区别重载:发生在同一个类中,方法名必须相同,参数类型不同,个数不同,顺序不同,方法返回值和访问修饰符可以不同,发生在编译时期重写:发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父......
  • Java面试题3
    Java面试题(第三天)1.HashMap和HashTable的区别?a.区别多线程环境下,HashTable比HashMap更安全,因为HashTable都加了一个synchronized修饰HashMap允许key和value为null,而HashTable不允许b.HashMap底层实现数组+链表jdk8开始链表高度到8,数组长度超过64,链表转变为红黑树,元素以......
  • Java面试题4
    Java面试题(第四天)1.双亲委派机制双亲委派机制是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给父类加载器。每个类加载器都是如此,只有在父类加载器在自己的搜索范围内找不到指定类时,子类加载器才会尝试自己去加载。1.当ApplicationClassLoader收到一......
  • Java面试题5
    Java面试题(第五天)1.对线程安全的理解不是线程安全,应该是内存安全,堆是共享内存,可以被所有线程访问当多个线程访问一个对象时,如果不用进行额外的同步控制或其他协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象时线程安全的堆是进程和线程共有的空间,分全局......
  • Java面试题1
    Java面试题(第一天)1.JDK、JRE、JVM三者区别和联系区别:JDK:JavaDevelopmentKit(Java开发环境)JRE:JavaRuntimeEnvironment(Java运行环境)JVM:JavaVirtualMachine(Java虚拟机)联系:.java文件被javac编译成.class文件然后被jvm使用lib类库编译为机器码然后通过jvm映射到操......
  • React面试题: 我是否可以在项目中不断使用React.Component来优化项目
    React.PureComponent是React的一个组件,主要用于性能优化,可以避免不必要的渲染。它的主要特点是:如果组件的props和state没有发生变化,则不会重新渲染(此处原理类似React.memo)。可以自动检查对象和数组,判断其是否需要重新渲染(是浅比较)。但是,React.PureComponent的缺点......
  • 面试题: 前端处理滚动穿透这个顽疾
    诚如你所知:滚动穿透是指在移动端滑动页面的过程中,页面滚动受到了触摸事件的干扰,导致无法流畅地滚动,这主要是由于事件冒泡造成的。解决方法如下:监听touchmove事件,阻止默认行为监听touchmove事件,阻止事件的默认行为,防止事件冒泡到父元素上。禁止滚动可以通过over......
  • React面试题: useCallBack()与React.memo的区别与使用常见场景
    React.usecallback与React.memo的区别:React.useCallback和React.memo是两种不同的优化方式:主要功能:它们都可以避免不必要的渲染,提高React应用的性能。React.useCallback是Hoc(高阶组件)的解决方案,可以用于优化函数组件和Class组件,注意这里适用于React高阶组件的渲染解......