内存栅栏和volatile关键字

内存栅栏和volatile关键字

前言

本次主要讲解关于内存栅栏的一点小东西,主要是扫盲,给大家普及普及概念性的东西.以前我们说过在一些简单的案例中,比如一个字段赋值或递增该字段,我们需要对线程进行同步.

虽然lock可以满足我们的需要,但是一个竞争锁一定会导致阻塞,然后忍受线程上下文切换和调度的开销.有些高并发和性能比较关键的地方,这些是不能忍受的.

.net提供了非阻塞同步构造,为一些简单的操作提高了性能,它甚至都没有阻塞,暂停,和等待线程.

引入

Memory Barries and Volatility(内存栅栏和易失字段)

考虑下面的代码

int _answer;
        bool _complete;
        void A()
        {
            _answer = 123;
            _complete = true;
        }

        void B()
        {
            if (_complete)
            {
                Console.WriteLine(_answer);
            }
        }

如果方法A和B都在不同的线程下并发的执行,方法B可能输出”0”吗?回答是”yes”,原因如下:

1.编译器,CLR或CPU可能会为了性能而重新为程序的指令进行排序,例如可能会将方法A中的两句代码的顺序进行调整.

2.编译器,CLR或CPU可能会为变量 的赋值采用缓存策略,这样这些变量就不会立即对其他变量可见了,例如方法A中的变量赋值,不会立即刷新到内存中,变量B看到的变量并不是最新的值.

C#在运行时非常小心的保证这些优化策略不会影响正常的单线程的代码和多线程环境下加锁的代码.

除此之外,你必须显示的通过创建内存屏障(Memory fences)来限制指令重新排序和读写缓存对程序造成影响.

Full Fences:

最简单的完全栅栏的方法莫过于使用Thread.Memory.Barrier方法了.这个方法就是写完数据之后,调用MemoryBarrier,数据就会立即刷新,另外在读取数据之前调用MemoryBarrier可以确保读取的数据是最新的,并且处理器对Memorybarrier的优化小处理.so,以上代码可以下称如下这样:

 int _answer;
        bool _complete;
        void A()
        {
            _answer = 123;
            Thread.MemoryBarrier();//在写完之后,创建内存栅栏
            _complete = true;
            Thread.MemoryBarrier();//在写完之后,创建内存栅栏
        }

        void B()
        {
            //在读取之前,创建内存栅栏
            Thread.MemoryBarrier();
            if (_complete)
            {
                //在读取之前,创建内存栅栏
                Thread.MemoryBarrier();
                Console.WriteLine(_answer);
            }
        }

一个完全的栅栏在现在桌面应用程序中,大约需要花费10ns.

下面的一些构造都隐式的生成完全栅栏.

1.C# Lock 语句(Monitor.Enter / Monitor.Exit)

2.在Interlocked类的所有方法。

3.使用线程池的异步回调,包括异步的委托,APM 回调,和 Task continuations.

4.在一个信号构造中的发送(Settings)和等待(waiting) 

你不需要对每一个变量的读写都使用完全栅栏,假设你有三个answer 字段,我们仍然可以使用4个栅栏。例如:

int _answer1, _answer2, _answer3;
        bool _complete;

        void A()
        {
            _answer1 = 1; _answer2 = 2; _answer3 = 3;
            Thread.MemoryBarrier(); //在写完之后,创建内存栅栏
            _complete = true;
            Thread.MemoryBarrier(); //在写完之后,创建内存栅栏
        }

        void B()
        {
            Thread.MemoryBarrier(); //在读取之前,创建内存栅栏
            if (_complete)
            {
                Thread.MemoryBarrier(); //在读取之前,创建内存栅栏
                Console.WriteLine(_answer1 + _answer2 + _answer3);
            }
        }

我们真的需要lock和内存栅栏吗?

在一个共享可写的字段上不使用lock或者栅栏就是在自找麻烦,看一下面的代码:

class Program
    {
        static void Main(string[] args)
        {
            bool complete = false;
            var t = new Thread(() =>
            {
                bool toggle = false;
                while (!complete)
                {
                    toggle = !toggle;
                }
            });

            t.Start();
            Thread.Sleep(1000);
            complete = true;
            t.Join();
        }
    }
 

如果你在Visual Studio中选择发布(Release)模式,生成该应用程序,那么如果你直接运行应用程序(不使用VS调试器,直接双击运行exe文件.),程序都不会中止.

因为CPU寄存器把complete变量的值给缓存了.在寄存器中,complete永远都是false.

通过在while循环中插入Thread.MemoryBarrier,或者是在读取complete的时候加锁都可以解决这个问题.

volatile关键字

为_complete字段加上volatile关键字也可以解决这个问题.

volatile bool _complete;

volatile关键字指导道编译器自动的为读写字段加屏障,以下是MSDN的解析:

volatile关键字指示一个字段可以由多个同时执行的线程修改.声明为volatile的字段不受编译器优化(假定由单个线程访问)的限制.这样可以确保该字段在任何时间呈现的都是最新的值.

使用volatile字段可以被总结成下表:


第一条指令


第二条指令


可以被交换吗?


Read


Read


No


Read


Write


No


Write


Write


No(CLR会确保写和写的操作不被交换,甚至不使用volatile关键字)


Write


Read


Yes!

注意到应用volatile关键字,并不能保证写后面跟读的操作不被交换.这就可能会造成莫名其妙的问题.例如:

  volatile int x, y;
        void Test1()
        {
            x = 1; //volatile write
            int a = y;//volatile read
        }
        void Test2()
        {
            y = 1;//volatile write
            int b = x;//volatile read
        }
 

这是Test1和Test2在不同的线程中并发执行,有可能a和b字段的值都是0(尽管在x和y上应用了volatile关键字).这段代码的意思是说,即使使用了volatile,也无法保证操作的顺序不被交换.volatile关键字可以确保线程读取到最新的值,但保证不了操作顺序.

这是一个避免使用volatile关键字的好例子,甚至假设你彻底明白了这段代码,是不是其他在你的代码上工作的人也全部明白呢?

在Test1和Test2方法中使用完全栅栏或者是lock都可以解决这个问题.

还有一个不适用volatile关键字的原因是性能问题,因为每次读写都会创建内存栅栏,例如:

volatile m_amount;
m_amount+=m_amount;

volatile关键字不支持引用传递的参数,和局部变量.再这样的情况下,你必须使用VolatileRead和VolatileWrite方法.例如:

volatile int m_amount;
Boolean success=Int32.TryParse(“123”,out m_amount);
//以上这段代码会发生错误

VolatileRead和VolatileWrite

从技术上来说,Thread类的静态方法VolatileRead和VolatileWrite在读取一个变量上和volatile关键字作用一致.

它们的实现是一样低效的,尽管事实上它们都创建了内存栅栏.下面是它们在Integer类型上的实现:

  public static void VolatileWrite(ref int address, int value)
        {
            Thread.MemoryBarrier(); address = value;
        }

        public static int VolatileRead(ref int address)
        {
            int num = address; Thread.MemoryBarrier(); return num;
        }
 

你可以看到如果你在调用VolatileWrite之后调用VolatileRead,在中间没有栅栏会被创建,着同样会导致我们上面讲到写之后再读顺序可能变换的问题.

小小的结一下

volatile修饰的变量对于读操作它具有acquire语意,而对于写操作它具有release语意.

int _answer;
volatile  bool _complete; 

 void A()
 {
     _answer = 123;
     _complete = true;
 } 

 void B()
 {
     if (_complete)
         Console.WriteLine(_answer);
 }

这样可以确保CW方法输出123了.

Thread.MemoryBarrier这个方法有两种作用

 

1.组织编译器和CPU对调用MemoryBarrier前后的内存操作进行优化乱序.

2.刷新寄存器,刷新寄存器的过程包括两个步骤,第一步将调用MemoryBarrier犯法之前,所有的缓存写入内存;第二步,清空所有缓存,或者使所有缓存失效.

比如:

int a=1;//可能存在缓存cache_a

int b=2;//可能存在缓存cache_b,并且a与b的写入顺序可任意优化

Thread.MemoryBarrier();//将a、b写入内存,写入过程:cache-->内存

int c=3;//a、b的写入顺序,不能在c写入内存之后

Console.Write(b)//这里的b此时如果造我的理解,它是从内存中读取,而不是cache_b,因为在调用MemoryBarrier时,cache_b已经失效

分析:写完数据之后,调用Memorybarrier,数据就会立即刷新,另外在读取数据之前调用MemoryBarrier可以确保读取的数据是最新的,并且处理器对MemoryBarrier的优化小心处理.

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-07-29 22:03:00

内存栅栏和volatile关键字的相关文章

Java同步内存模型和Volatile关键字

计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,数据的读取和写入.由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度.因此在CPU里面就有了高速缓存. 也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和

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

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

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

volatile关键字在java并发编程中是经常被用到的,大多数朋友知道它的作用:被volatile修饰的共享变量对各个线程可见,volatile保证变量在各线程中的一致性,因而变量在运算中是线程安全的.但是经过深入研究发现,大致方向是对的 ,但是细节上不是这样. 首先,引出volatile的作用.情景:当线程A遇到某个条件时,希望线程B做某件事.像这样的场景应该是经常会遇到的吧,下面我们来看一段模拟代码: package com.jack.jvmstudy; public class Test

java 内存模型与volatile关键字

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

内存管理_深入剖析volatile关键字

四.深入剖析volatile关键字 在前面讲述了很多东西,其实都是为讲述volatile关键字作铺垫,那么接下来我们就进入主题. 1.volatile关键字的两层语义 一旦一个共享变量(类的成员变量.类的静态成员变量)被volatile修饰之后,那么就具备了两层语义: 1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的. 2)禁止进行指令重排序. 先看一段代码,假如线程1先执行,线程2后执行: //线程1 boolean stop =

volatile关键字与内存可见性

前言 首先,我们使用多线程的目的在于提高程序的效率,但是如果使用不当,不仅不能提高效率,反而会使程序的性能更低,因为多线程涉及到线程之间的调度.CPU上下文的切换以及包括线程的创建.销毁和同步等等,开销比单线程大,因此需谨慎使用多线程. 在jdk1.5以后,提供了一个强大的java.util.current包,这个包中提供了大量的应用于线程的工具类. 下面开始介绍volatile关键字和内存可见性,虽然volatile是在jdk1.5之前就有的,但还是想放在这里讲一下. 举例说明 首先,我们先看

【Java并发编程】6、volatile关键字解析&内存模型&并发编程中三概念

转自:http://www.cnblogs.com/dolphin0520/p/3920373.html volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在Java 5之后,volatile关键字才得以重获生机. volatile关键字虽然从字面上理解起来比较简单,但是要用好不是一件容易的事情.由于volatile关键字是与Java的内存模型有关的,因此在讲述volatile关键之前,我们先来

由volatile关键字谈Java内存模型

volatile关键字虽然从字面上理解起来比较简单,但是要用好不是一件容易的事情.由于volatile关键字是与Java的内存模型有关的,因此在讲述volatile关键之前,我们先来了解一下与内存模型相关的概念和知识,然后分析了volatile关键字的实现原理,最后给出了几个使用volatile关键字的场景 1. 内存模型的相关概念 当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再

全面理解Java内存模型(JMM)及volatile关键字(转)

原文地址:全面理解Java内存模型(JMM)及volatile关键字 关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoader) 深入理解Java并发之synchronized实现原理 Java并发编程-无锁CAS与Unsafe类及其并发包Atomic 深入理解Java内存模型(JMM)及volatile关键字 剖析基于并发AQS的重入锁(Reetr