Java的volatile关键字

本文深入探讨了Java中Volatile关键字的作用及其如何解决多线程环境下因CPU缓存导致的可见性和有序性问题,并指出了Volatile不能保证的操作原子性。

一、CPU缓存给多线程带来的挑战

我们先看一段简单的程序:

public class VolatileTest {
    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        t.start();
        Thread.sleep(1000);
        t.stop = true; // 语句1
    }
}

class MyThread extends Thread {
    boolean stop = false;

    @Override
    public void run() {
        long startMs = System.currentTimeMillis();
        while(!stop) {
        }
        long endMs = System.currentTimeMillis();
        System.out.println(endMs - startMs);
    }
}

上面这段代码,按理说,我们在语句一t.stop设置为true时,MyThread应该停止运行,但是多次执行程序后,我们发现并不是这样的,当我们将t.stop设置为true时,MyThread仍在运行!

二、CPU缓存与可见性

上文的程序,导致结果与预期不符的是CPU缓存导致的。
在这里插入图片描述
MyThread对象存放在内存(堆)中,程序在执行时为了提高执行效率,会将部分程序内容从内存加载到CPU缓存中。线程(CPU)在修改变量的值以后,也不会马上将值冲刷回主内存,而其它线程也并不会每次都去主内存读取值,有可能读取自己CPU缓存那一份旧的数据。因此,一个线程更新了某个值,另一个线程可能“看不到”,这就导致了可见性问题。

三、volatile解决可见性问题

Java的volatile关键字,可以解决以上可见性问题。

Java语言规范保证:每次读取一个volatile变量时,都会从主内存读取,每次更改一个volatile变量时,都会立马写会主内存。

通过volatile关键字,我们能保证,一个线程更新了一个变量,另一个线程能够立马看到。
我们稍微修改一下上面的程序:

public class VolatileTest {
    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        t.start();
        Thread.sleep(1000);
        t.stop = true;
    }
}

class MyThread extends Thread {
    volatile boolean stop = false;

    @Override
    public void run() {
        long startMs = System.currentTimeMillis();
        while(!stop) {
        }
        long endMs = System.currentTimeMillis();
        System.out.println(endMs - startMs);
    }
}

这样子,程序在执行t.stop = true后,MyThread会立即停止。

四、volatile与有序性

什么是有序性?CPU在执行指令时,并不会按顺序执行,如果它认为,调换两个命令的顺序并不会影响程序的结果,那么为了提高效率,它可能会将这两条命令的执行顺序调换(重排序)。

class Date {
    private int year;
    private int month;
    private int day;

    public void setDate(Date date) {
        this.year = date.year; // 语句1
        this.month = date.month;  // 语句2
        this.day = date.day; // 语句3
    }
    
    public void getDate(Date date) {
        date.day = this.day; // 语句4
        date.month = this.month; // 语句5
        date.year = this.year; // 语句6
    }
}

以上程序,调用setDate()方法是,语句1,2,3哪一条先被调用无法得到保证。调用getDate()方法时,语句4,5,6哪个条先被调用无法得到保证。

volatile在保证可见性的同时,也保证了有序性。

Java语言规范保证:

  1. 一个线程在读取一个volatile变量时,所有的对这个线程可见的变量都会重新从内存中加载;
  2. 一个线程在写一个volatile变量时,这个线程在执行完以后,会将cpu缓存写回主内存;
  3. 对于volatile读操作,不允许将后面的读操作重排序到volatile读操作之前;
  4. 对于volattile写操作,不允许将前面的写操作重排序到volatile写操作之前。

因此,我们如果要保证上面程序的有序性,那么可以将day变量标记为volatile

class Date {
    private int year;
    private int month;
    private volatile int day;

    public void setDate(Date date) {
        this.year = date.year; // 语句1
        this.month = date.month;  // 语句2
        this.day = date.day; // 语句3
    }
    
    public void getDate(Date date) {
        date.day = this.day; // 语句4
        date.month = this.month; // 语句5
        date.year = this.year; // 语句6
        
    }
}

对于写操作,我们就可以保证:语句1,2一定会在语句3之前执行,且yearmonth在语句3执行完后会和day一样被写回主内存;
对于读操作,我们可以保证:语句4,5一定在语句6之前执行,且yearmonth在语句4执行完以后会更新为主内存的最新值。

五、volatile不保证原子性

volatile并不保证原子性。例如:

volatile counter = 1;

counter = counter + 1; //语句1

语句1在执行时,如果读取完counter后,有其它线程修改了counter的值,那么程序就会出现预期之外的结果。

<think>好的,用户想了解Javavolatile关键字的用法和作用,包括示例和使用场景。我需要根据提供的引用资料来整理回答。首先,volatile的作用主要有两点:可见性和禁止指令重排。需要详细解释这两个特性,并结合引用中的内容。 可见性方面,引用[1]提到volatile不会让线程阻塞,比synchronized轻量,但效果有限。引用[2]指出volatile保证变量的读写在主内存进行,确保多线程下的可见性。需要说明当线程写入volatile变量时,其他线程能立即看到新值。 禁止指令重排方面,引用[5]解释了volatile变量的操作不会被编译器或CPU重排序,这对单例模式的双重检查锁定很重要。引用[3]和[4]中的示例展示了双重检查锁中使用volatile来防止对象初始化时的指令重排,避免返回未完全初始化的实例。 使用场景需要包括状态标志和双重检查锁定。状态标志的例子可以是一个简单的布尔变量控制线程运行,而双重检查锁定则用于单例模式,确保线程安全且高效。引用[3]和[4]的代码示例需要解释,说明volatile如何防止问题。 同时,要指出volatile不保证原子性,比如自增操作需要synchronized或Atomic类。引用[1]提到volatile的适用场景有限,所以需要强调适用的具体情况。 最后生成相关问题,可能涉及与synchronized的对比、不适用场景、底层原理等,结合已有引用内容中的知识点。确保回答结构清晰,分点说明,并正确使用LaTeX格式,如变量名用$volatile$等。检查引用标识是否正确对应,如[^1]、[^2]等。</think>### Java volatile关键字详解 #### 一、核心作用 1. **可见性保证** $volatile$ 修饰的变量修改后,会立即将新值同步到主内存,并使其他线程的缓存失效[^2]。例如: ```java volatile boolean flag = false; // 线程A修改后,线程B能立即读取到最新值 ``` 2. **禁止指令重排序** 编译器/CPU不会对$volatile$变量的读写操作进行重排序[^5]。在单例模式中可防止返回未完全初始化的对象: ```java instance = new Singleton(); // 实际包含三个步骤:分配内存、初始化对象、地址赋值 ``` #### 二、典型使用场景 1. **状态标志位** 适用于多线程间简单的状态控制: ```java volatile boolean shutdownRequested; public void shutdown() { shutdownRequested = true; } public void doWork() { while (!shutdownRequested) { /* 任务执行 */ } } ``` 2. **双重检查锁定(Double-Checked Locking)** 解决单例模式延迟初始化时的线程安全问题[^3][^4]: ```java class Singleton { private volatile static Singleton instance; public static Singleton getInstance() { if (instance == null) { // 第一次检查 synchronized (Singleton.class) { if (instance == null) { // 第二次检查 instance = new Singleton(); } } } return instance; } } ``` 若缺少$volatile$,可能返回**部分初始化**的对象(由于指令重排导致)[^4]。 #### 三、关键限制 1. **不保证原子性** $volatile$无法保证复合操作的原子性,如$i++$需要配合`synchronized`或`AtomicInteger`[^2]。 2. **性能考量** 每次读写都直接操作主内存,会降低局部性原理带来的性能优势[^1]。 #### 四、与synchronized对比 | 特性 | volatile | synchronized | |--------------------|-----------------------|-----------------------| | 上下文切换 | 无 | 有 | | 原子性 | 不保证 | 保证 | | 适用场景 | 单变量可见性/指令排序 | 代码块/方法级同步保护 | --- 相关问题
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值