百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 文章教程 > 正文

吊打面试官(五)--Java关键字volatile一文全掌握

yund56 2025-07-28 23:31 4 浏览

前言

volatile 是 Java 中的一个关键字,用于声明变量。当一个变量被声明为 volatile时,它可以确保线程对这个变量的读写都是直接从主内存中进行的。这也是面试官最爱问的点,接下来我们详细介绍这个关联字各个方面。


volatile关键字使用详细介绍

1. 可见性

当一个线程修改了一个 volatile变量的值,其他线程能够立即看到修改后的值。

这是因为 volatile变量不会被缓存在寄存器或其他处理器不可见的地方,因此保证了每次读取 volatile变量都会从主内存中读取最新的值。

2. 有序性

volatile变量的读写操作具有一定的有序性,即禁止了指令重排序优化,就是禁止编译器自动重新排序。

这意味着,在一个线程中,对 volatile变量的写操作一定发生在后续对这个变量的读操作之前。

3. 使用场景

volatile 关键字通常用于以下场景:* 当多个线程共享一个变量,并且至少有一个线程会修改这个变量时。* 当需要确保变量的修改对所有线程立即可见时。* 当变量的状态不需要依赖于之前的值,或者不需要与其他状态变量共同参与不变约束时。


4. 代码示例

下面是一个使用 volatile 关键字的简单示例:

public class VolatileExample {
    private volatile boolean flag = false;

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    public boolean getFlag() {
        return this.flag;
    }

    public static void main(String[] args) {
        VolatileExample example = new VolatileExample();

        // 线程1:修改flag的值
        new Thread(() -> {
            try {
                Thread.sleep(1000); // 模拟一些操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            example.setFlag(true);
            System.out.println("Flag被设置为true");
        }).start();

        // 线程2:检查flag的值
        new Thread(() -> {
            while (!example.getFlag()) {
                // 忙等待,直到flag变为true
            }
            System.out.println("检测到Flag为true");
        }).start();
    }
}

在这个示例中,我们有两个线程。

线程1在休眠1秒后将 `flag` 设置为 `true`,而线程2则不断检查 `flag` 的值,直到它变为 `true`。由于 `flag` 被声明为 volatile,因此线程2能够立即看到线程1对 `flag` 的修改,并退出循环。


5.使用注意事项*

volatile关键字不能保证原子性。如果需要对变量进行复合操作(例如自增),则应该使用 `synchronized` 关键字或其他并发工具(如 `AtomicInteger`)来确保线程安全。

* 过度使用 volatile可能会导致性能下降,因为它会禁止编译器和处理器对代码进行某些优化。因此,在使用 volatile时应该仔细考虑其必要性。



volatile关键字使用场景举例

1.状态标志位

在多线程程序中, volatile 关键字用于表示一个状态标志位,例如程序运行状态或中断使能状态。这些状态标志位通常会被多个线程访问和修改,使用 volatile 可以确保它们的可见性和有序性。使用 volatile 关键字可以防止线程间的数据不一致性问题,确保每个线程都能看到最新的状态标志位值。这对于控制线程行为和同步操作非常关键。


代码举例:

public class VolatileExample {
    private volatile boolean flag = false;

    public void startTask() {
        new Thread(() -> {
            try {
                Thread.sleep(1000); // 模拟耗时操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = true;
            System.out.println("Flag has been set to true.");
        }).start();
    }

    public void monitorTask() {
        new Thread(() -> {
            while (!flag) {
                // 循环等待,直到flag变为true
            }
            System.out.println("Flag is now true. Task can proceed.");
        }).start();
    }

    public static void main(String[] args) {
        VolatileExample example = new VolatileExample();
        example.startTask();
        example.monitorTask();
    }
}


2.单例模式的双重检查锁

在单例模式中, volatile 关键字用于确保单例实例在多线程环境下的唯一性和可见性。通过将实例声明为 volatile ,可以防止线程在读取和写入实例时看到不一致的值。在多线程环境中, volatile 关键字可以防止指令重排序,确保单例实例的初始化操作在所有线程中都完成,从而避免潜在的线程安全问题。

代码举例:

public class Singleton {
    private static volatile Singleton instance = null;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}


3.线程安全的计数器

volatile 关键字也适用于简单的计数器,如统计某个事件的发生次数。虽然 volatile 不能保证复合操作的原子性,但它可以确保每次读取和写入操作都是对主内存的访问。在需要统计事件发生次数的场景中, volatile 关键字可以确保计数的准确性,防止线程在读取和写入计数器时看到不一致的值。

代码举例:

public class VolatileCounter {
    private volatile int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args) {
        VolatileCounter counter = new VolatileCounter();

        // 启动多个线程进行计数
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    counter.increment();
                }
            }).start();
        }

        // 等待所有线程完成
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final count: " + counter.getCount());
    }
}


4.直接访问硬件寄存器

在嵌入式系统编程中,直接与硬件设备交互时,使用 volatile 可以确保每次读写操作都直接从内存中进行,而不是使用寄存器缓存中的值。这可以避免编译器对寄存器访问的优化,确保与硬件的交互是准确的。在嵌入式系统编程中, volatile 关键字的作用至关重要。它可以防止编译器对硬件寄存器访问的优化,确保每次读写操作都是对实际硬件的访问,从而提高系统的稳定性和可靠性。


使用JNI作为代码举例:

Java代码:
public class HardwareAccess {
    // 声明本地方法
    public native int readRegister();

    // 加载动态链接库
    static {
        System.loadLibrary("hardware");
    }

    public static void main(String[] args) {
        HardwareAccess ha = new HardwareAccess();
        int registerValue = ha.readRegister();
        System.out.println("Register value: " + Integer.toHexString(registerValue));
    }
}
生成JNI头文件:
javac HardwareAccess.java
javac -h . HardwareAccess.java
编写C代码实现本地方法:
#include <jni.h>
#include "HardwareAccess.h"
#include <stdio.h>

JNIEXPORT jint JNICALL Java_HardwareAccess_readRegister(JNIEnv *env, jobject obj) {
    // 模拟读取寄存器数据
    printf("Reading register...\n");
    int simulatedRegisterValue = 0x1234;
    return simulatedRegisterValue;
}
编译C代码为动态链接库:
Linux:gcc -shared -fpic -o libhardware.so -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux HardwareAccess.c
运行Java程序:
java -Djava.library.path=. HardwareAccess


5.中断处理程序中的标志位

在处理中断时,通常需要对中断标志进行读写操作。使用 volatile 可以确保中断处理程序对标志位的修改能够立即被其他线程看到,从而确保中断处理的正确性。在中断处理程序中使用 volatile 关键字可以确保中断标志位的修改对所有线程立即可见,避免因中断处理导致的线程间数据不一致问题。

代码举例:

public class InterruptExample {
    private volatile boolean interrupted = false;

    public void run() {
        while (!interrupted) {
            // 执行一些任务
        }
        // 线程被中断,执行清理操作
    }

    public void setInterrupted() {
        interrupted = true;
    }

    public static void main(String[] args) {
        InterruptExample example = new InterruptExample();
        Thread thread = new Thread(example::run);
        thread.start();

        // 模拟中断线程
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        example.setInterrupted();
    }
}


6.信号处理程序中的标志位

在信号处理程序中,使用 volatile 可以确保信号处理程序对标志位的修改能够立即被其他线程看到,从而确保信号处理的正确性。

在信号处理程序中使用 volatile 关键字可以防止指令重排序,确保信号处理程序对标志位的修改对所有线程立即可见,从而提高信号处理的可靠性和稳定性。

信号处理通常通过 sun.misc.Signal 和 sun.

misc.SignalHandler 来实现,但需要注意的是,这些类并不是Java标准API的一部分,可能在不同的JDK实现中有所不同。


代码举例:

import sun.misc.Signal;
import sun.misc.SignalHandler;

public class SignalExample {
    private volatile boolean signalReceived = false;

    public void handleSignal() {
        Signal.handle(new Signal("INT"), new SignalHandler() {
            @Override
            public void handle(Signal signal) {
                signalReceived = true;
            }
        });
    }

    public void run() {
        while (!signalReceived) {
            // 执行一些任务
        }
        // 信号处理程序已设置标志位,执行清理操作
    }

    public static void main(String[] args) {
        SignalExample example = new SignalExample();
        example.handleSignal();
        new Thread(example::run).start();

        // 模拟发送信号
        try {
            Thread.sleep(1000);
            Signal.raise(new Signal("INT"));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


7.防止优化编译器优化

在某些情况下,编译器可能会对代码进行优化,导致变量的值在多线程环境中不一致。使用 volatile 可以防止这种优化,确保每次访问变量时都从内存中读取最新的值。在需要防止编译器优化的场景中, volatile 关键字可以确保变量的值始终是最新的,避免因编译器优化导致的线程间数据不一致问题。


代码举例:

public class OptimizationExample {
    private volatile int counter = 0;

    public void increment() {
        counter++;
    }

    public int getCounter() {
        return counter;
    }

    public static void main(String[] args) {
        OptimizationExample example = new OptimizationExample();

        // 启动多个线程进行计数
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    example.increment();
                }
            }).start();
        }

        // 等待所有线程完成
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final counter value: " + example.getCounter());
    }
}

在这个示例中, volatile 关键字确保了对 counter 变量的每次访问都直接从内存中进行,而不是使用寄存器缓存中的值,从而防止了编译器优化导致的不一致问题。


在多线程编程中,volatile关键字与synchronized关键字有何不同?


volatile 关键字

可见性:

volatile 确保了变量的修改对所有线程是可见的。当一个线程修改了 volatile 变量的值,这个变化会立即被写入主内存,而其他线程在读取该变量时会从主内存中获取最新的值。


禁止指令重排序:

volatile 可以防止编译器和处理器对代码进行优化,确保指令按照程序的顺序执行。


适用场景:

volatile 适用于那些被多个线程访问但并不涉及复合操作(例如递增操作)的变量。它主要用于状态标志、控制变量等场景。


性能:

由于 volatile 不需要使用锁,因此它的性能开销相对较小。


synchronized关键字

互斥性:

synchronized 确保同一时刻只有一个线程可以访问被保护的代码块或方法,从而避免了多个线程之间的竞争条件。


有序性:

synchronized 通过锁定和解锁机制,隐式地保证了代码执行的顺序性。


适用场景:

synchronized 适用于需要保证原子性和线程安全性的场景,例如对共享资源的读写操作。


性能:

synchronized 可能会带来较大的性能开销,因为它涉及到线程的阻塞和唤醒,以及上下文切换。总的来说, volatilesynchronized 在多线程编程中各有其独特的用途。 volatile 适用于需要保证变量可见性且不涉及复杂操作的场景,而 synchronized 则适用于需要保证代码块或方法原子性和有序性的场景。


volatile关键字在Java中的实现机制是什么?

volatile 关键字在Java中的实现机制主要涉及Java内存模型(JMM)、内存屏障(Memory Barrier)和缓存一致性协议(如MESI协议)。


Java内存模型(JMM):

主内存与工作内存:

JMM定义了线程如何与主内存和线程本地内存交互。主内存是所有线程共享的内存区域,存储所有变量的值。每个线程有自己的本地内存,存储从主内存中读取的变量副本。


内存可见性:当一个线程修改了 volatile 变量的值,这个修改会立即刷新到主内存,并通知其他线程更新缓存。其他线程在读取该变量时,会从主内存中重新加载最新的值。


内存屏障(Memory Barrier):

写屏障(Store Barrier):

在写操作之后插入,确保写操作对其他线程可见。

读屏障(Load Barrier):在读操作之前插入,确保读操作从主内存中加载最新值。volatile 通过插入内存屏障来禁止指令重排序,确保变量的读写操作按照程序的顺序执行。

缓存一致性协议(MESI协议):

MESI协议:现代CPU通常有多级缓存(L1、L2、L3),为了保证缓存一致性,CPU使用缓存一致性协议(如MESI协议)。

volatile 写操作会触发缓存行状态变为Modified,并强制将数据写回主内存; volatile 读操作会触发缓存行状态变为Shared,并从主内存中加载最新数据。

JVM层面的实现:

字节码层面的实现:在字节码层面, volatile 变量的读写操作会被标记为ACC_VOLATILE标志。JVM在执行这些操作时会插入内存屏障。


JIT编译器的优化:JIT编译器在生成机器码时,会根据 volatile 的语义插入内存屏障。例如,在x86架构下, volatile 写操作会插入StoreLoad屏障,确保写操作对其他线程可见。

硬件层面的实现:

x86架构:在x86架构下, volatile 写操作会使用LOCK前缀指令,强制将数据写回主内存,并通知其他CPU缓存失效。

ARM架构:在ARM架构下, volatile 通过内存屏障指令(如DMB)来实现。通过上述机制, volatile 关键字确保了多线程环境下变量的可见性和有序性,从而避免了由于线程间数据不一致导致的问题。


volatile关键字可能导致性能下降问题


volatile关键字在以下情况下可能会导致性能下降:

1. 缓存行争用:

当多个线程同时访问被volatile修饰的变量时,可能会导致缓存行争用。这是因为每个处理器都有自己的缓存,当多个线程访问同一个缓存行中的数据时,可能会导致缓存失效,从而需要从主内存中重新加载数据。这种缓存失效和重新加载的过程会增加访问延迟,从而降低性能。

2. 内存屏障开销:

volatile关键字会引入内存屏障,以确保变量的修改对所有线程都是可见的。内存屏障是一种特殊的指令,用于在编译器和处理器之间同步内存访问顺序。虽然内存屏障可以确保正确的内存可见性,但它也可能导致性能下降,因为它会限制编译器和处理器对指令进行重排序的能力。

3. 禁止编译器优化:

volatile关键字禁止编译器对变量进行优化,以确保每次访问该变量时都能获取到最新的值。这可能会导致生成的代码相对较多,从而影响程序性能。

4. 原子操作开销:

volatile关键字可以确保对变量的读取和写入都是原子的,这意味着它们不会被其他线程的操作中断。原子操作本身可能比非原子操作更昂贵,因为它们需要额外的处理器资源来保证操作的完整性。尽管volatile关键字可能会导致性能下降,但在许多情况下,这种影响是可以接受的。例如,当多个线程需要共享一个简单的状态变量(如计数器)时,使用volatile关键字可以确保所有线程都能看到最新的值,而不会引入不必要的复杂性或性能开销。



前言

volatile 是 Java 中的一个关键字,用于声明变量。本文我们讲述了它的详细使用场景,典型使用案例,和synchronized关键字的对比,它的实现原理,性能问题等。基本覆盖了它涉及的各个方面,请各位看官自行取用。


求关注哦


相关推荐

在这款15年老端游的手机版中,正在上演着“萌新拯救计划”

以往我们判断一款刚公测的新手游到底火不火,不是瞅苹果的免费榜畅销榜,就是看各家数据网站的预估流水。不过如今这个法子放在《剑网3无界》身上似乎就不那么适用了。作为一款与原端游完全数据互通的手游,点卡制收...

708090后集体回忆!88款经典街机游戏,你通关过几部?

街机厅的霓虹灯在夜色中闪烁,投币口“叮当”的声响此起彼伏,摇杆与按键的碰撞声混合着玩家的欢呼与叹息,构成了那个年代独有的电子交响乐。对于70后、80后、90后来说,街机不仅是游戏,更是一段无法复制的...

爷青回!这10款童年小游戏,玩过5个以上的都当爸妈了吧?

当手机游戏被3A画质与开放世界统治的今天,那些藏在像素点阵里的童年记忆,才是真正刻进DNA的快乐密码!我们翻遍全网玩家回忆录,结合抖音、Steam等平台数据,为你揭开这代人的集体记忆封印一、经典益智三...

怀旧时刻:PS2十大经典动作游戏盘点,老玩家不可错过的青春回忆

说起PS2,那可是游戏史上最火的主机之一,上面好游戏多得数不清,给咱们带来过不少欢乐时光。今天,小核桃就带大家回忆一下PS2上那些超经典的动作游戏,一起重温那些热血沸腾的日子吧!当年在电玩店看到《战神...

又是一年仲夏,三十年前的暑假,你还记得在玩什么游戏吗?

今年山东的夏天似乎比往年都热,夜晚繁星点点,本该轻柔的晚风却没有丝毫凉意,伴随着远处草丛里此起彼伏的虫鸣声,听的让人心里愈加烦躁,翻来覆去睡不着的笔者,无聊且乏困地坐在院子里的老槐树下,思绪却不由自主...

十六年前的首款安卓1.0手机,内置物理全键盘,如今二手45元

周末聊点轻松的话题,说起智能手机系统之争,安卓和iOS绝对是两大“宿敌”。2007年苹果在乔布斯带领下发布了初代iPhone,也凭借iOS系统掀起了智能手机的新时代。短短一年后,谷歌联合HTC推出了...

HTC巅峰时期的安卓手机,自带全键盘,居然很多人用过

上次写三部最经典的侧滑盖全键盘手机,居然很多人报出了DesireZ的大名,这让我很吃惊。因为这部手机没有行货只有水货,你们咋都用过?那今天好好聊聊它。十多年前,HTC是安卓手机领域绝对的霸主,当年只...

十年前的 iPhone 6s 还在 “服役”:一部手机的 “超长待机” 启示录

当iPhone16系列已经开始预热,有人却还握着2015年发布的iPhone6s刷着微信、看视频——这部诞生已近十年的手机,至今仍在不少人的生活里扮演着重要角色。它的“超长寿命”...

SFC黄金时代10款动作RPG神作,每一款都是回忆满满的经典游戏!

任天堂的16位主机SFC可是游戏史上的一个高峰,它用像素画面打造出无数经典作品,其中结合动作和冒险的ARPG特别受欢迎。最近几年复古风又火起来,大厂们忙着移植或重制老游戏,模拟器也让玩家轻松重温旧梦。...

揭秘十年前真正的游戏手机:索尼爱立信R800魔改系统超乎想象!

对于游戏手机的起源,众说纷纭,有人认为是黑鲨问世,有人说是红魔领路,还有人坚称自从iPhone问世起,游戏手机就已然存在。然而,如果从更宏观的角度审视,这些所谓的游戏手机,其本质上仍旧是多功能的智能手...

专属于八零,九零后的插卡游戏,你还记得吗

1.魂斗罗这是我玩过的第一款插卡游戏,永远记得上上下下左左右右,BABA开始,这个可以有三十条命的“魔法。”也是第一次体验双打游戏的那行配合的责任感使命感。2.忍者神龟四只神龟的名字都是意大利著名的画...

Java编程的那些屎山代码分析之二(java编程神器)

以下是个人总结的一些代码习惯问题和优化,单独一个也许不起眼,但堆积起来,就让一个项目代码变成一座屎山。1.滥用`public`修饰符o重要性:滥用`public`修饰符可能导致类的成员变量或方法被不...

六种java的多线程设计模式详解和代码举例

java的多线程处理,有哪些模式可以使用呢,如何使用呢。本文列举了六种多线程设计模式供大家参考。1.生产者-消费者模式设计理念:生产者-消费者模式通过协调两个线程(生产者和消费者)来处理数据,生产者生...

java的四种引用(java 中都有哪些引用类型)

java中的引用分为4种1.强引用引用存在就不会被GC*2.软引用heapmemory(堆内存)满了就会被GC掉*3.弱引用每次GC就会回收掉(应用有:ThreadLocal)*4....

@程序员 2020了看不懂这些动图,你可能是个假的程序员

点击上方Java编程技术乐园,轻松关注!及时获取有趣有料的技术文章文章很有趣,开心一下,如果有收获,记得点赞和关注哦~「1」外包产品交付,给客户演示时「2」与领导斗智斗勇,躲猫猫「3」领导总是能识破程...