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

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 < 20; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int i = 0; i< 10000; i++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }
        //等待所有累加线程都结束
        while(Thread.activeCount() > 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虚拟机》,只是作了简单的概述,有兴趣的朋友可以去阅读原书。

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

时间: 2024-10-24 01:12:39

Java内存模型与volatile关键字浅析的相关文章

深入理解JVM读书笔记五: Java内存模型与Volatile关键字

12.2硬件的效率与一致性 由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了. 基于高速缓存的存储交互很好地理解了处理器与内存的速度矛盾,但是也为计算机系统带来了更高的复杂度,因为它引入了一个新的问题: 缓存一致性(Cache Coherenc

java 内存模型与volatile关键字

java内存模型可以大致理解分为两个模块,主内存和私有内存.主内存中主要是存放一些共享的全局变量,私有内存主要是存放线程所需的私有变量.一般情况下,如果某个线程需要使用主内存的全局变量.首先,它会拷贝一份主内存里面的全局变量到私有内存,进行操作,操作完成以后再把这个变量同步到主内存.如下图:如果是单线程的,到没什么问题,但是如果是多线程的,就有可能出现数据不一致的问题,因为线程之间是不可见的.看下面一个例子: package org.hzg.volatilekeyword; /** * Crea

Java内存模型与volatile

内存模型描述的是程序中各变量(实例域.静态域和数组元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存取出变量这样的低层细节. 每一个线程有一块工作内存区,其中保留了被所有线程共享的主内存中的变量的值的拷贝.为了存取一个共享的变量,一个线程通常先获取锁定并且清除它的工作内存区,这保证该共享变量从所有线程的共享内存区正确地装入到线程的工作内存区,当线程解锁时保证该工作内存区中变量的值写回到共享内存中. 下面简单给出了规则的重要推论:1. 适当运用同步结构,能够正确地把一个或一组值通过共

深入java内存模型(二) volatile详解

对于volatile修饰符,我们应该不会陌生,在多线程中使用常见,比如多个线程想用一个全局变量作为标识符,或者一个共享变量,我们都会给该变量加上一个volatile的修饰符.volatile用中文解释是易变的,不稳定的.说明该变量会被多个线程访问并可能修改.那么jvm是怎样发挥volatile关键字的作用,如何实现的呢? 上一篇深入java内存模型中解释了jvm中的重排序以及四种内存屏障等.jvm总是会以一些易懂,使用方便的方式来实现相关功能.比如垃圾回收器,对于内存的申请与释放时一个令人头疼的

Java内存模型与Volatile,Happen-Before原则等

 Java的内存模型 Java内存模型(JMM)是一个抽象的模型.决定了线程主要定义了线程和内存间的抽象关系:主内存存放的是线程共享变量,每个线程有自己的工作内存,存放变量的副本,只能对副本进行读写,副本的变量再刷新到主内存中.具体体现为多核CPU,每核有一个高速缓存,每个核的线程对高速缓存读写,并且有共同的主存. 主内存与工作线程交互的操作有以下八种: lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态unlock(解锁):作用于主内存的变量,释放锁定状态的变量read

深入理解Java内存模型(四)——volatile

volatile的特性 当我们声明共享变量为volatile后,对这个变量的读/写将会很特别.理解volatile特性的一个好方法是:把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步.下面我们通过具体的示例来说明,请看下面的示例代码: class VolatileFeaturesExample { //使用volatile声明64位的long型变量 volatile long vl = 0L; public void set(long l) { vl = l;

java内存模型-volatile

volatile 的特性 当我们声明共享变量为 volatile 后,对这个变量的读/写将会很特别.理解 volatile 特性的一个好方法是:把对 volatile 变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步.下面我们通过具体的示例来说明,请看下面的示例代码: class VolatileFeaturesExample { //使用volatile声明64位的long型变量 volatile long vl = 0L; public void set(long l) {

深入理解Java内存模型-volatile

volatile的特性 当我们声明共享变量为volatile后,对这个变量的读/写将会很特别.理解volatile特性的一个好方法是:把对volatile变量的单个读/写,看成是使用同一个监视器锁对这些单个读/写操作做了同步.下面我们通过具体的示例来说明,请看下面的示例代码: class VolatileFeaturesExample { volatile long vl = 0L; //使用volatile声明64位的long型变量 public void set(long l) { vl =

深入理解JMM(Java内存模型) --(四)volatile

volatile的特性 当我们声明共享变量为volatile后,对这个变量的读/写将会很特别.理解volatile特性的一个好方法是:把对volatile变量的单个读/写,看成是使用同一个监视器锁对这些单个读/写操作做了同步.下面我们通过具体的示例来说明,请看下面的示例代码: [java] view plain copy class VolatileFeaturesExample { volatile long vl = 0L;  //使用volatile声明64位的long型变量 public