《死磕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

执行结果并不是预期的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中的volatile插图

关注公众号:程序新视界,一个让你软实力、硬技术同步提升的平台

除非注明,否则均为程序新视界原创文章,转载必须以链接形式标明本文链接

本文链接:https://choupangxia.com/2020/08/21/java-volatile/