Java内存模型与volatile关键字浅析

2021-07-15 19:05

阅读:607

标签:技巧   方向   访问规则   catch   ges   关键字   ble   ase   col   

volatile关键字在java并发编程中是经常被用到的,大多数朋友知道它的作用:被volatile修饰的共享变量对各个线程可见,volatile保证变量在各线程中的一致性,因而变量在运算中是线程安全的。但是经过深入研究发现,大致方向是对的 ,但是细节上不是这样。

首先,引出volatile的作用。
情景:当线程A遇到某个条件时,希望线程B做某件事。像这样的场景应该是经常会遇到的吧,下面我们来看一段模拟代码:

package com.jack.jvmstudy;
public class TestVolatile extends Thread{
    private boolean isRunning = true;//标识线程是否运行
    public boolean isRunning() {
        return isRunning;
    }
    public void setRunning(boolean isRunning) {
        this.isRunning = isRunning;
    }
    @Override
    public void run() {
        while(isRunning()) {
            //若 isRunning = true 此处将陷入死循环
        }
        System.out.println("循环线程结束...");
    }
    public static void main(String[] args) {
        TestVolatile tv = new TestVolatile();
        Thread t1 = new Thread(tv);
        t1.start();//启动线程,此时进入无限的循环之中
        try {
            //让主线程暂停 1 秒钟
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        tv.setRunning(false);//主线程将isRunning改为false,想终止循环线程
        System.out.println("主线程结束...");
    }
}

运行此段程序发现,主线程结束了,但是循环线程依旧在不停地循环,这是正确结果,虽然不是我们想看到的结果。我们的目的是想让主线程终止循环线程的执行,但是上面的程序显然做不到,要达到这种效果,有多种方式,今天我们就看看使用volatile关键字,只需要给 isRunning 加上 volatile 即可,然后执行程序,我们发现循环线程终止了,是不是很神奇,其实我们都知道这并不神奇,道理也很简单,就是最上面的那段话,但是再深一点呢?就涉及到了java的内存模型。


java内存模型:
技术分享图片

原谅我的画图技巧!!!

java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量与java编程中的变量有所区别,它包括了实例字段,静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。


下面根据以上的内存模型,来看看内存间是如何进行交互操作的。
一个新的变量是先在主内存中诞生,如果各个线程使用到了这个变量,会在各自的工作空间中保留这个变量的副本。线程修改了副本之后,会立即同步到主内存,但是如果没有经过特别处理,其他线程依旧是原来的那个值,也就是一个过期的值。拿最上面的例子来说,首先共享变量isRunning=true诞生于主内存,然后主线程和循环线程各自保留了一份副本,然后主线程修改了isRunning的值并同步回主内存,但是循环线程依旧是原先的值,所以就造成了死循环的结果。
接下来就该volatile登场了,被volatile修饰的变量对每个线程可见,意思就是说被volatile修饰的变量,各个线程如果要使用它的话,都会去主内存中取最新值,而不是直接使用副本,这样就保证了此变量在各个线程中的一致性。虽然被volatile修饰的变量能保证各线程都拿到了最新的数据,但是并不代表基于volatile变量的运算在并发下是安全的,为什么呢?先上代码

package com.jack.jvmstudy;
public class TestVolatile2 {
    public volatile static int race = 0;
    public static void increase() {
        race ++;
    }
    public static void main(String[] args) {
        Thread[] threads = new Thread[20];
        for(int i = 0; i  1)
            Thread.yield();
        System.out.println("race = " + race);
    }
}

运行上面的程序,我们期望输出20000,但是执行完之后发现并不是这样,并且相距很大。为什么呢?
因为 race ++ 不是原子操作,虽然race被volatile修饰,保证了主内存中变量的修改第一时间反映给了各个线程,但是 ++ 操作并不是一步完成的,简单分析一下,race ++ 操作分为三步,a、获取race的值;b、race的值加1;c、返回race值。由于volatile的作用,线程每次获取race的值都是最新的,但是某个线程可能在执行完a之后被挂起了,别的线程完成了race++整个操作,并将值写入了主内存之中,此时这个线程接着执行b操作的时候,race的值已经过期了,再写入主内存的值就小了。很简单,在increase()方法上加上 synchronized 关键字保证 race++是原子操作就行了。

好了,今天关于volatile的分析就到这里了,这篇博文主要参考《深入理解java虚拟机》,只是作了简单的概述,有兴趣的朋友可以去阅读原书。

Java内存模型与volatile关键字浅析

标签:技巧   方向   访问规则   catch   ges   关键字   ble   ase   col   

原文地址:http://blog.51cto.com/13925439/2164138


评论


亲,登录后才可以留言!