二、volatile关键字

volatile是JVM中最轻量级的同步机制,当一个变量被定义为volatile变量之后,它将具备两个特性,一是保证此变量对所有线程的可见性,而是禁止指令重排序。

1、可见性

变量对所有线程的可见性指的是,当一个线程修改了这个变量的值,新的值对于其他线程来说是可以立即可见的,也就是其他线程再读取这个变量时,总是能读取到这个变量的最新值。而普通变量由于Java内存模型中线程的工作内存的存在,是做不到这点的。比如一个普通变量,线程A修改了它的值,线程B只有等待线程A将普通变量的值回写到主内存中之后,然后再从主内存中读取这个变量的值,这个变量的新值才会对线程B可见。

volatile的可见性可以理解为,对volatile变量的写操作,会使线程将其直接写回主内存,并通知其他线程之前读取的值已经失效;对volatile变量的读操作,会使线程直接从主内存里面读取值。

volatile变量对所有线程是立即可见的,对volatile变量的所有写操作都能立刻反应到其他线程之中,但是由于Java里面的运算并非原子操作,所以volatile变量在并发场景下并不是安全的

对于一个volatile变量,在Java语言层面可以简单理解为,对此变量的读和写的原子操作是线程安全的,但是对a++这种复合操作是不安全的,代码层面可以理解如下两个示例是等效的:

class VolatileExample {
    volatile int a;

    private void setA(int n) {
        a = n;
    }

    private int getA() {
        return a;
    }

    private void increaseA() {
        a++;
    }

}
// 假设有多个线程调用上面的3个方法,那么在代码语义上,上述程序和以下程序是等价的

class VolatileExample {
    int a;
    
    private synchronized void setA(int n) {
        a = n;
    }
    
    private synchronized int getA() {
        return a;
    }
    
    private void increaseA() {
        int temp = getA();
        temp++;
        setA(temp);
    }
    
}

所以,在如下代码中,main线程中起了20个线程,每个线程都对变量a进行了10000次+1,但是最终的运行结果却总是一个小于200000的不确定的值。

public class VolatileTest {

    public static volatile int a = 0;

    public static void increase() {
        a++;
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i=0; i<20; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int i=0; i<10000; i++) {
                        increase();
                    }
                }
            });
            thread.start();
        }
        // idea中运行main方法时,会起两个线程
        // 除了main线程,还起了一个其他的线程(不知道是干啥的)
        while(Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("a = " + a);
    }
}

运行结果:

a = 192341

出现这样的结果的原因就是,虽然变量a是volatile变量,但是,increase()方法中,a++并不是一个原子性操作。从字节码层面来看,a++这一行语句是由4条字节码指令执行的:

 首先getstatic指令把a的值取到操作栈顶时,volatile关键字保证了a的值此时是正确的,但是在执行iconst_1,iadd这些指令的时候,其他指令可能已经把a的值又加大了,而在操作栈顶的值就成为了过期的数据,所以putstatic指令执行后就可能把较小的值同步回了主内存之中,所以最后的值小于20000。

上述使用字节码分析这个问题并不严谨,但是在一定层面上已经能够说明这个问题。

虽然volatile在并发场景下并不能够保证线程安全的,但是在以下两种场景中,使用volatile就可以满足我们的需求:

  • 只有一个线程会修改变量,其他线程只会读变量
  • 变量无需与其他的状态变量共同参与不变约束

 典型的适合使用volatile的场景:

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

2、禁止指令重排序

通过以下伪代码示例来进行说明:

Map configOptions;
char[] configText;
volatile boolean initialized = false;

// 假设以下代码在线程A中执行
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized = true;

// 假设以下代码在线程B中执行
while(!initialized) {
    sleep();
}
doSomethingWithConfig(); //使用线程A中配置好的信息

如果initilized变量没有使用volatile修饰,由于指令重排序优化的原因,就有可能导致initialized = true被提前执行,在配置信息初始化完成之前就被置为了true,这样就导致线程B在执行doSomethingWithConfig()方法时,配置信息的初始化并没有完成。而volatile关键字则可以避免此类情况。

下面是一个实际例子,双重检查的单例模式(非线程安全情况):

public class Singleton {
    private static Singleton instance;

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

 上述代码不是线程安全的,这是因为,instance = new Singleton();这行代码在内存中实际上有如下3个步骤,而且步骤2和步骤3之间有可能会发生重排序:

memory = allocate();  // 1、为对象分配内存;
ctorInstance(memory); //2、初始化对象;
instance = memory;    //3.将对象内存地址赋给引用变量

在单线程场景中,由于步骤4使用对象总是在步骤2和步骤3之后,因此就算是步骤2、3重排序了也没有问题。但是在多线程运行中,如果发生了重排序,就导致可能出现以下情况:线程B拿到对象开始使用时,该对象尚未完成初始化。

 由于volatile的禁止指令重排序特性,因此,只需要将instance设置为volatile变量就可以解决以上问题。当instance为volatile变量时,步骤2、3不会发生指令重排序,因此多线程场景下,每个线程获取到对象的引用时,该对象一定是已经初始化完成了,不会存在线程不安全的情况。

双重检查的单例模式(线程安全情况):

public 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;
    }
}

编译后,这段代码对Instance变量赋值部分如下所示:

从字节码层面分析,关键点在于,对于volatile修饰的instance变量,在其赋值后,多执行了一个lock操作,这个操作相当于是一个内存屏障。而在指令重排序优化过程中,是不能越过内存屏障的,也就是屏障后面的指令无法优化到内存屏障前面的位置来,屏障前面的指令也无法优化到内存屏障后面的位置去。因此,其赋值前的初始化对象的指令,不会被重排序到赋值指令的后面,也就是保证了赋值完成时,对象的初始化也一定已经完成。

おすすめ

転載: blog.csdn.net/sun_lm/article/details/119979783