`
greemranqq
  • 浏览: 975474 次
  • 性别: Icon_minigender_1
  • 来自: 重庆
社区版块
存档分类
最新评论

JAVA 内存模型(二) volatile理解

    博客分类:
  • JVM
阅读更多

    volatile 关键字是JAVA虚拟机提供的最轻量级的同步机制,在了解volatile的特性后,会让我们在多线程数据竞争方面更准确的使用。

  

一、概念解释

    一个volatile 变量,具备两种特性,第一是保证此变量多所有线程的可见性,这里的“可见性”是指当一个线程修改了该变量之后,新值对其他线程来说是可以立即得知的,而普通变量做不到,普通变量需要传递和主内存来完成,比如:线程A修改了普通变量的值,然后向主内存进行回写,另一条线程B在A线程回写完成之后再从主内存进行读取操作,变量的新值才会对线程B可见。(参考上一篇内存模型图)

 

    volatitle 变量的可见性描述是:“volatile变量对所有线程是立即可见的,对volatile 变量的所有写操作都能理解反应到其他线程中,也就是说volatile 变量再各个线程中是一致的。”这句话是对的,但是常常有点人就会通过上面描述得出:“基于volatile 变量在并发下是安全的。”这样的结论。volatile 变量再各个线程的工作内存中不存在一致性问题(因为volatile 变量再使用前,都会先刷新,即使刷新前不一致,执行引擎都是看到刷新后的值,因此可认为是一致的)。而Java 里面的运算并非原子性操作,导致volatile 变量的运算在并发下一样是不安全的,看代码:

package com;

/**
 * volatile 测试
 * @author Ran
 */
public class VolatileTest {
	public static volatile int race = 0;
	public  static void increase(){
		race ++ ;
	}
	// 线程数
	public static final int THREADS_COUNT = 20;
	
	public static void main(String[] args) {
		Thread[] threads = new Thread[THREADS_COUNT];
		for(int i = 0;i < THREADS_COUNT;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);
	}
	
	
}

 

以上代码理论结果应该是200000,但是你会发现值始终是小于这个数的,这是因为race 的自增运算不是原子性的,我们看反编译的字节码:

 

     从字节码看出,假设A线程执行getstatic 指令把race 的值取到操作栈顶时,volatile 保证了race 的的值此时是正确的假设现在是10,然后执行iconst_1 和iadd 的时候,可能另外B线程已经完成了上面的所有操作,值已经变成11了,这个时候A线程拿到的10 已经是过期数据,这时候A继续完成下面的操作的时候,即使增加了还是11(理论两个线程完成,会变成12)。在多线程情况下,累加的值也就小于预期了。

 

这里从上篇的内存模型来理解:

A,B 线程,分别从主内存拿到(read) volatile 变量race=0.然后放到(load)A,B的工作内存,这时候A线程把变量传递(use)给执行引擎,按字节码进行操作。同时B 执行同样的动作,由于JVM的不确定性,A在执行到iconst_1 和iadd 的时候,B已经执行完成,这时候A继续执行,最后刷新主内存的只1,结果就不是预期的了。

 

上述简单的解释:A,B 线程获取volatille 变量,每次都要从新从主内存读取,并且A线程改变了变量值,会告诉B线程告诉B线程,我已经改变了,你读取必须从主内存读取。但是在A线程改变,写入主内存和发送通知时,B线程获得的变量已经是主内存中读取的了,不需要从新读取,那么此时错误就产生了。 

 

 

二、解决指令从排序

       这里先看一段有趣的代码:

      

// 变量
	private  static boolean flag = false;;
	private  static int number;
	
	
	// 模拟初始化数字number
	private static class B extends Thread {
		@Override
        public void run() {	
			number = 100;
			flag = true;
        }
	}
	
	public static void main(String[] args) {
		new A().start();
		new B().start();
	}

	// 模拟获得B 初始化后的值
	private static class A extends Thread {
        @Override
        public void run() {
        	while(!flag){
        		System.out.println("A 线程 获得变量:"+number +":"+flag);
        		Thread.yield();
        	}
        	System.out.println("B 线程执行完成:"+number+":"+flag);
        }
    }

 

    上面操作是模拟多线程A线程要检测和获得B线程初始化Number 的值,也就是说B线程中当number = 100,flag = true .这是一个顺序操作。但是多次执行可能会得出这样的结果:
A 线程 获得变量:0:true
这里是A线程产生的,当A线程执行while(!flag) 为真,也就是flag = false.然后进入方法体,这时候B同时执行,将flag 变成了true,奇怪的是number 变量居然还是0.也就是说flag = true (可能)被提前执行了!!
但是我们的要求是flag = true 必须后于 number = 100 执行。JMM(JAVA内存模型) 为先限制这种指令重排序问题,加 入了volatile ,对于这两个操作的指令重排序问题,我仅仅找了一下规则,具体的还得根据JMM里面 的 8个基本操作入手,这里我转了一部分规则:
是否重排序               第二个操作
第一个操作 普通读/写 volatile读 volatile写
普通读/写     NO
volatile读 NO NO NO
volatile写   NO NO

我们以A,B操作,V,T变量,A:普通变量V 读/写  和 B:volatile变量T  写进行解释(第三行,最后一个格子)
如果A是对普通变量V进行读/写 操作。B对T 进行写操作,编译器不做重排序。
从表关系看出,我们的 number = 100,flag = true ,都有写操作。要想不重排序,我们可以在第二个变量flag 上加上volatile 就行了,但是给number 加上volatile 就不行,就是最后一行,第一个空格了,没效果。
 
小结:
        1.这里仅仅对volatile 进行概念性的理解
        2.关于重排序问题,涉及到内存和硬件、系统等差异,这里仅仅对JMM 这层进行介绍,关于volatile 对重排序的的原理,在上面贴出的链接有介绍。
        3.了解volatile 关键字,不至于我们乱用它,要想使用灵活准确,需要更加深入实践。
        4.个人理解对volatile 更多使用应该专注于它的可见性,多线程的情况下只允许一个线程有逻辑修改,而其他线程用于读取的一致性。当然在指令重排问题,在保证上一点的情况下,可以避免。
        
 
 
 
 
 
 
更多请参考:

 

  • 大小: 10.7 KB
分享到:
评论

相关推荐

    Java 内存模型

    理解Java内存模型对于编写正确、高效的多线程程序至关重要。在Java 5之前,Java内存模型的描述比较模糊,为了提升多线程程序的可靠性,Java社区重新定义并强化了Java内存模型,该工作主要在JSR-133专家组的领导下...

    深入理解Java内存模型 pdf 超清版

    深入理解Java内存模型对于编写高效的并发程序至关重要。本文将详细介绍JMM的核心概念、工作原理以及相关的编程实践。 1. **核心概念** - **线程私有区域**: 包括程序计数器、虚拟机栈、本地方法栈,这些区域中的...

    深入理解Java内存模型

    在深入理解Java内存模型之前,我们需要先了解并发编程模型的分类,然后掌握Java内存模型的基础知识,理解重排序和顺序一致性,以及volatile关键字的相关知识点。 首先,让我们探讨Java内存模型的基础知识。在并发...

    深入理解java内存模型

    这本书"深入理解Java内存模型"显然是为了帮助读者深入探讨这个主题,以便更好地理解和解决并发编程中的问题。 Java内存模型主要涉及以下几个核心概念: 1. **主内存**:所有线程共享的数据存储区域,包括类的静态...

    cpu 内存模型和java内存模型

    volatile关键字是Java内存模型中一个非常重要的概念,它保证了变量的可见性,即任何线程对该变量的修改都会立即被其它线程得知,但并不保证操作的原子性。 ### 锁机制 在Java中,锁是一种同步机制,用来控制多个...

    深入理解 Java 内存模型_程晓明_InfoQ_java_内存模型_

    深入理解Java内存模型可以帮助开发者避免并发编程中常见的问题,如数据竞争、死锁和活锁等。通过合理地使用同步机制,可以编写出高效且线程安全的代码,这对于大型分布式系统和高并发应用尤为重要。 总之,Java内存...

    深入理解 Java 内存模型

    《深入理解 Java 内存模型》这本书由程晓明编著,旨在帮助开发者深入理解和应用 JMM。 1. **内存层次结构**:Java 内存模型将内存分为堆内存、栈内存、方法区(在 Java 8 及以后版本中被元空间替代)和程序计数器等...

    深入理解Java内存模型.程晓明(带书签文字版).pdf

    Java 内存模型的抽象 4 重排序 6 处理器重排序与内存屏障指令 7 happens-before 10 重排序 13 数据依赖性 13 as-if-serial 语义 13 程序顺序规则 15 重排序对多线程的影响 15 顺序一致性 19 数据竞争与顺序...

    Java内存模型的历史变迁

    #### 二、旧Java内存模型 在JDK 5之前,Java采用了一种基于共享内存的并发模型。在这种模型下,多个线程通过共享变量进行数据交换。为了确保线程间数据的一致性,Java内存模型规定了一系列复杂的规则来管理线程与...

    java内存模型文档

    这些文档如"Java内存模型.docx"、"Java内存模型2.docx"、"深入Java核心 Java内存分配原理精讲.docx"、"java内存模型.pdf"将深入探讨这些概念,帮助开发者更深入地理解Java内存模型及其在实际编程中的应用。...

    深度剖析java内存模型

    在并发编程中,理解Java内存模型对于编写正确的多线程程序至关重要。 首先,线程之间的同步指的是程序控制不同线程之间操作发生相对顺序的机制。在Java的共享内存并发模型中,同步是显式进行的,程序员需要显式地...

    深入理解Java内存模型(二)共3页.pdf.zip

    Java内存模型,简称JMM(Java Memory Model),是Java虚拟机规范中定义的一个抽象概念,它...通过阅读"深入理解Java内存模型(二)共3页.pdf.zip"中的内容,开发者可以进一步了解JMM的细节,解决并发编程中的复杂问题。

    java内存模型详解--非常经典

    在并发编程中,Java内存模型提供了一些内置的同步机制,如volatile关键字、synchronized关键字以及final修饰符。这些机制确保了在多线程环境下,对共享变量的访问具有一定的可见性和有序性。 - volatile关键字:...

    深入理解JAVA内存模型(高清完整版)

    本教程《深入理解JAVA内存模型》将带你深入探讨这一主题,尤其关注Java中的同步原语——synchronized、volatile和final。 首先,我们要了解JMM的基础结构。JMM规定了程序中各个线程如何访问和修改共享变量,包括主...

    Java内存模型分析与其在编程中的应用.pdf

    在编程实践中,理解Java内存模型对于处理多线程程序尤为重要,因为多线程环境下对共享资源的访问需要考虑线程安全问题。而垃圾回收机制的了解也对优化程序性能、减少内存泄漏有很大帮助。 Java内存模型规定了对内存...

    java内存模型详解

    理解Java内存模型对于编写多线程并发程序至关重要,因为它直接影响到程序的正确性和性能。 在Java中,内存分为以下几个区域: 1. **程序计数器**:每个线程都有自己的程序计数器,用于存储当前线程执行的字节码...

Global site tag (gtag.js) - Google Analytics