Java多线程volatile详解

本文详细介绍了Java中的volatile关键字,包括其定义、作用、代码示例、底层实现和使用场景。volatile保证了多线程环境中的可见性和有序性,但无法保证原子性。适合于一个线程写,多个线程读的场景,或者与synchronized配合实现轻量级锁。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

volatile定义

Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。

volatile的作用

先让我们说说volatile关键字的作用。它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。

volatile代码示例

单例模式(重排序)

public class Singleton {
    public static volatile Singleton singleton;

    /**
     * 构造函数私有
     */
    private Singleton(){}

    /**
     * 单例实现
     * @author fuyuwei
     * 2017年5月14日 上午10:07:07
     * @return
     */
    public static Singleton getInstance(){
        if(singleton == null){
            synchronized (singleton) {
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

我们知道实例化一个对象经过分配内存空间、初始化对象、将内存空间的地址赋值给对应的引用,上面的单例模式可以解释为分配内存空间、将内存地址赋值给对应的应用、初始化对象。上面的代码如果我们不加volatile在并发环境下可能会出现Singleton的多次实例化,假如线程A进入getInstance方法,发现singleton==null,然后加锁通过new Singleton进行实例化,然后释放锁,我们知道new Singleton在JVM中其实是分为3步,假如线程啊在释放锁之后还没来得及通知其他线程,这时候线程B进入getInstance的时候发现singleton==null便会再次实例化。

可见性

一个线程修改了共享变量值,而另一个线程却看不到。引起可见性问题的主要原因是每个线程拥有自己的一个高速缓存区——线程工作内存。volatile关键字能有效的解决这个问题

public class Volatile {
    int m = 0;
    int n = 1;

    public void set(){
        m = 6;
        n = m;
    }

    public void print(){
        System.out.println("m:"+m+",n:"+n);
    }

    public static void main(String[] args) {
        while(true){
            final Volatile v = new Volatile();
            new Thread(new Runnable(){
                @Override
                public void run() {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    v.set();
                }

            }).start();

            new Thread(new Runnable(){
                @Override
                public void run() {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    v.print();
                }

            }).start();
        }
    }
}

正常情况下m=0,n=1;m=6,n=6,通过运行我们发现了m=0,n6(需要长时间运行)

m:6,n:6
m:6,n:6
m:6,n:6
m:6,n:6
m:6,n:6
m:6,n:6
m:6,n:6
m:6,n:6
m:6,n:6
m:6,n:6
m:0,n:1
m:6,n:6
m:6,n:6
m:6,n:6
m:6,n:6
m:6,n:6
m:6,n:6
m:6,n:6
m:6,n:6
m:6,n:6
m:0,n:6
m:6,n:6
m:6,n:6
m:6,n:6
m:0,n:1
m:0,n:1
m:0,n:1
m:6,n:6
m:0,n:1
m:0,n:1
m:0,n:1
m:6,n:6
m:6,n:6
m:6,n:6
m:0,n:1
m:6,n:6
m:6,n:6

对volatile变量的写操作与普通变量的主要区别有两点:
  (1)修改volatile变量时会强制将修改后的值刷新的主内存中。
  (2)修改volatile变量后会导致其他线程工作内存中对应的变量值失效。因此,再读取该变量值的时候就需要重新从读取主内存中的值。

volatile并不能保证原理性

package com.swk.thread;
public class Volatile {
    private volatile int m = 0;

    public void incr(){
        m++;
    }

    public static void main(String[] args) {
        final Volatile v = new Volatile();
        for(int i=0;i<1000;i++){
            new Thread(new Runnable(){
                @Override
                public void run() {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    v.incr();;
                }

            }).start();
        }
        try {
            Thread.sleep(10000);// 确保1000次循环执行完毕
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(v.m);
    }
}

输出结果:950,并不是我们想象中的1000,如果我们在incr加上synchronized,输出结果是1000
原因也很简单,i++其实是一个复合操作,包括三步骤:
  (1)读取i的值。
  (2)对i加1。
  (3)将i的值写回内存。
volatile是无法保证这三个操作是具有原子性的,我们可以通过AtomicInteger或者Synchronized来保证+1操作的原子性。

volatile底层实现

在了解volatile实现原理之前,我们先来看下与其实现原理相关的CPU术语与说明

术语英文单词术语描述
内存屏障memory barries是一组处理器指令,用于实现对内存操作的顺序限制
缓冲行cache line缓存中可以分配的最小存储单位。处理器填写缓存线时会加载整个缓存线,需要使用多个主内存周期
原子操作atomic operations不可中断的一个或一些列操作
缓冲行填充cache line fill当处理其识别到内存中读取操作数是可缓存的,处理器读取整个缓存行到适当的缓存
缓存命中cache hit如果进行高速缓存行填充操作的内存位置仍然是下次处理器访问的地址时,处理器从高速缓存中读取,而不是从内存中读取
写命中write hit当处理器操作数写回到一个内存缓存区域时,他首先会检查这个缓存的内存地址是否在缓存行中,如果存在一个有效的缓存行,则处理器将这个操作数写回到缓存,而不是会写到内存
写缺失write misses the cache一个有效的缓存行被写入到不存在的内存区域

volatile是如何来保证可见性的呢?让我们在X86处理器下通过工具获取JIT编译器生成的汇编指令来查看对volatile进行写操作时,CPU会做什么事情。Java代码如下:

private valatile Singleton instance = new Singleton();

转成汇编代码如下

0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);

当有volatile变量修饰时会出现lock addl $0×0,(%esp),Lock前缀的指令在多核处理器下会引发了两件事情
1)将当前处理器缓存行的数据写回到系统内存。
2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

volatile使用场景

一个线程写,多个线程读

volatile boolean shutdownRequested;
...
public void shutdown() { shutdownRequested = true; }
public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }
}

结合使用 volatile 和 synchronized 实现 “开销较低的读-写锁”

volatile 允许多个线程执行读操作,因此当使用 volatile 保证读代码路径时,要比使用锁执行全部代码路径获得更高的共享度 —— 就像读-写操作一样。

public class CheesyCounter {
    private volatile int value;
    public int getValue() { return value; }
    public synchronized int increment() {
        return value++;
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值