谈谈Java中的volatile

简介: 谈谈Java中的volatile

在《死磕GOF23种设计模式之单例模式》中,其中双重检查锁使用到了volatile关键字,本篇文章就带大家深入了解一下volatile相关的知识。


简介

volatile是Java提供的一种轻量级的同步机制,在并发编程中扮演着比较重要的角色。与synchronized相比,volatile更轻量级。


示例说明

首先,我们先来看一段代码:


package com.secbro2.others.testVolatile;
/**
 * @author zzs
 */
public class TestVolatile {
    private static boolean status = false;
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (!status) {
            }
        }).start();
        Thread.sleep(100L);
        status = true;
        System.out.println("status is " + status);
    }
}

一个实体类,包含一个status属性,默认值为false,在main方法中启动一个线程,线程内当status变为true时停止,当为false时一直执行,然后线程睡眠100毫秒,随后将status改为true,并打印修改之后的结果。那么,线程中的while方法此时是否也随之结束呢?答案是否定的!


当执行此端代码时,我们会发现,虽然已经打印出“status is true”,但线程并没有停止,一直在执行。这是为什么呢?


内存可见性

上面的例子如果在单线程中,上面的业务逻辑肯定和我们预期的结果一致。但在多线程模型中,共享变量status在线程之间是“不可见”的。


所谓可见性,是当一个线程修改了共享变量,修改之后的值对其他线程来说可以立即获得,这便是线程之间的可见性。上面的例子正是因为没有做到线程之间的可见性,因此在主线程上修改了status值,另外一个线程却没有获取到,因此一致循环执行。


Java内存模型

Java虚拟机的内存模型(Java Memory Model,JMM),决定线程对共享变量的写入是否对其他线程可见。JMM定义了线程和主线程内存之间的抽象关系:共享变量存储在主内存(Man Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存的变量。


用synchronized和Lock可以解决线程同步的问题,但针对上面的问题使用它们太重量级了。此时volatile的作用彰显出来了,当volatile修饰变量后有以下作用:

- 当写一个volatile变量时,JMM会把该线程对应的本地缓存中的变量强制刷新到主内存中去;

- 写操作会导致其他线程中的缓存无效;


修改过后的变量为:


package com.secbro2.others.testVolatile;
/**
 * @author zzs
 */
public class TestVolatile {
    private static volatile boolean status = false;
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (!status) {
            }
        }).start();
        Thread.sleep(100L);
        status = true;
        System.out.println("status is " + status);
    }
}

此时再执行程序,会发现当status被修改之后,程序马上停止了。

volatile是否能够保持原子性

多线程的另外一个问题就是原子性操作,当一个操作不是原子性的,那么多线程同时操作就可能导致并发问题。首先看一个示例:

package com.secbro2.others.testVolatile;
/**
 * @author zzs
 */
public class Counter {
    private volatile int inc = 0;
    private void increase() {
        inc++;
    }
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    counter.increase();
                }
            }).start();
        }
        Thread.sleep(3000L);
        System.out.println(counter.inc);
    }
}

执行结果为:


6847

1

执行结果并不是预期的10000。这就说明volatile虽然可以保证可见性,但并不能保证原子性。可见性能够保证,每个线程每次读取到的值为最新值,但读取之后的再操作就没办法保证。比如上面的例子,inc的自增操作包含三步:读取inc的值,进行加1,写入工作内存,也就是说inc的自增操作并不是原子性的。


对上面的代码进行修改,使用synchronized关键字,即可保证线程的安全:


package com.secbro2.others.testVolatile;
/**
 * @author zzs
 */
public class Counter {
    private volatile int inc = 0;
    private synchronized void increase() {
        inc++;
    }
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    counter.increase();
                }
            }).start();
        }
        Thread.sleep(3000L);
        System.out.println(counter.inc);
    }
}

输出结果是预期的10000。当然,也可以使用AtomicInteger来保证递增的原子性,这里不再举例说明。


volatile是否可以保证有序性

指令重排:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。


int a = 10;    //语句1
int r = 2;    //语句2
a = a + 3;    //语句3
r = a*a;     //语句4

比如上面的代码,可能的执行顺序为:语句2,语句1,语句3,语句4,但不会是:语句2,语句1,语句4,语句3。


volatile关键字还能禁止指令的重排序,所以能在一定程序上保证有序性。


volatile关键字禁止指令重排序有两层意思:


当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;


在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。


具体示例在将单例模式的双重检查锁中已经讲到,实现代码如下:


package com.secbro2.gof23.singleton;
/**
 * Singleton Patterns<br/>
 * <p>
 * Double checked lock and volatile;
 *
 * @author zzs
 */
public class SingletonThreadSafe2 {
    private static volatile SingletonThreadSafe2 instance;
    private SingletonThreadSafe2() {}
    public static SingletonThreadSafe2 getInstance() {
        if (instance == null) {
            synchronized (SingletonThreadSafe2.class) {
                if (instance == null) {
                    instance = new SingletonThreadSafe2();
                }
            }
        }
        return instance;
    }
    public void helloSingleton() {
        System.out.println("Hello SingletonThreadSafe1!");
    }
}

小结

通过上面的讲解,我们了解的volatile的基本作用和示例,它的应用场景比如状态标记量和双重检查。但我们需要明白的是,volatiled可以解决一部分线程并发问题,但它并不能像synchronized那样真正的达到同步锁的目的。

目录
相关文章
|
存储 安全 Java
Java面试题:深入探索Java内存模型,Java内存模型中的主内存与工作内存的概念,Java内存模型中的happens-before关系,volatile关键字在Java内存模型中的作用
Java面试题:深入探索Java内存模型,Java内存模型中的主内存与工作内存的概念,Java内存模型中的happens-before关系,volatile关键字在Java内存模型中的作用
101 1
|
11月前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
270 5
Java 并发编程——volatile 关键字解析
|
存储 SQL 缓存
揭秘Java并发核心:深度剖析Java内存模型(JMM)与Volatile关键字的魔法底层,让你的多线程应用无懈可击
【8月更文挑战第4天】Java内存模型(JMM)是Java并发的核心,定义了多线程环境中变量的访问规则,确保原子性、可见性和有序性。JMM区分了主内存与工作内存,以提高性能但可能引入可见性问题。Volatile关键字确保变量的可见性和有序性,其作用于读写操作中插入内存屏障,避免缓存一致性问题。例如,在DCL单例模式中使用Volatile确保实例化过程的可见性。Volatile依赖内存屏障和缓存一致性协议,但不保证原子性,需与其他同步机制配合使用以构建安全的并发程序。
220 0
|
缓存 安全 算法
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
170 0
|
11月前
|
缓存 安全 Java
Java volatile关键字:你真的懂了吗?
`volatile` 是 Java 中的轻量级同步机制,主要用于保证多线程环境下共享变量的可见性和防止指令重排。它确保一个线程对 `volatile` 变量的修改能立即被其他线程看到,但不能保证原子性。典型应用场景包括状态标记、双重检查锁定和安全发布对象等。`volatile` 适用于布尔型、字节型等简单类型及引用类型,不适用于 `long` 和 `double` 类型。与 `synchronized` 不同,`volatile` 不提供互斥性,因此在需要互斥的场景下不能替代 `synchronized`。
3309 3
|
缓存 安全 Java
Java面试题:解释volatile关键字的作用,以及它如何保证内存的可见性
Java面试题:解释volatile关键字的作用,以及它如何保证内存的可见性
238 4
|
设计模式 缓存 安全
Java面试题:工厂模式与内存泄漏防范?线程安全与volatile关键字的适用性?并发集合与线程池管理问题
Java面试题:工厂模式与内存泄漏防范?线程安全与volatile关键字的适用性?并发集合与线程池管理问题
152 1
|
缓存 安全 Java
深入理解java中的volatile关键字
深入理解java中的volatile关键字
304 1
|
存储 缓存 Java
(一) 玩命死磕Java内存模型(JMM)与 Volatile关键字底层原理
文章的阐述思路为:先阐述`JVM`内存模型、硬件与`OS`(操作系统)内存区域架构、`Java`多线程原理以及`Java`内存模型`JMM`之间的关联关系后,再对`Java`内存模型进行进一步剖析,毕竟许多小伙伴很容易将`Java`内存模型(`JMM`)和`JVM`内存模型的概念相互混淆,本文的目的就是帮助各位彻底理解`JMM`内存模型。
387 0
|
存储 缓存 安全
Java面试题:介绍一下jvm中的内存模型?说明volatile关键字的作用,以及它如何保证可见性和有序性。
Java面试题:介绍一下jvm中的内存模型?说明volatile关键字的作用,以及它如何保证可见性和有序性。
131 0
下一篇
开通oss服务