深入多线程之:内存栅栏与volatile关键字的使用分析

以前我们说过在一些简单的例子中,比如为一个字段赋值或递增该字段,我们需要对线程进行同步,
虽然lock可以满足我们的需要,但是一个竞争锁一定会导致阻塞,然后忍受线程上下文切换和调度的开销,在一些高并发和性能比较关键的地方,这些是不能忍受的。
.net framework 提供了非阻塞同步构造,为一些简单的操作提高了性能,它甚至都没有阻塞,暂停,和等待线程。

Memory Barriers and Volatility (内存栅栏和易失字段 )
考虑下下面的代码:


代码如下:

int _answer;
        bool _complete;
        void A()
        {
            _answer = 123;
            _complete = true;
        }
        void B()
        {
            if (_complete)
                Console.WriteLine(_answer);
        }

如果方法A和B都在不同的线程下并发的执行,方法B可能输出 “0” 吗?

回答是“yes”,基于以下原因:
    编译器,clr 或 cpu 可能会为了性能而重新为程序的指令进行排序,例如可能会将方法A中的两句代码的顺序进行调整。
    编译器,clr 或 cpu 可能会为变量的赋值采用缓存策略,这样这些变量就不会立即对其他变量可见了,例如方法A中的变量赋值,不会立即刷新到内存中,变量B看到的变量并不是最新的值。

C# 和运行时非常小心的保证这些优化策略不会影响正常的单线程的代码和在多线程环境下加锁的代码。
除此之外,你必须显示的通过创建内存屏障(Memory fences) 来限制指令重新排序和读写缓存对程序造成的影响。

Full fences:

最简单的完全栅栏的方法莫过于使用Thread.MemoryBarrier方法了。

以下是msdn的解释:
Thread.MemoryBarrier: 按如下方式同步内存访问:执行当前线程的处理器在对指令重新排序时,不能采用先执行 MemoryBarrier 调用之后的内存访问,再执行 MemoryBarrier 调用之前的内存访问的方式。
按照我个人的理解:就是写完数据之后,调用MemoryBarrier,数据就会立即刷新,另外在读取数据之前调用MemoryBarrier可以确保读取的数据是最新的,并且处理器对MemoryBarrier的优化小心处理。


代码如下:

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

一个完全的栅栏在现代桌面应用程序中,大于需要花费10纳秒。
下面的一些构造都隐式的生成完全栅栏。

C# Lock 语句(Monitor.Enter / Monitor.Exit)
    在Interlocked类的所有方法。
    使用线程池的异步回调,包括异步的委托,APM 回调,和 Task continuations.
    在一个信号构造中的发送(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 或者栅栏 就是在自找麻烦,在msdn上有很多关于这方面的主题。
考虑下下面的代码:


代码如下:

public static void Main()
        {
            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中选择发布模式,生成该应用程序,那么如果你直接运行应用程序,程序都不会中止。
因为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关键字的好例子,甚至假设你彻底的明白了这段代码,是不是其他在你的代码上工作的人也全部明白呢?。

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

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


代码如下:

volatile m_amount
m_amount  = m_amount + m_amount.

Volatile 关键字不支持引用传递的参数,和局部变量。在这样的场景下,你必须使用

VolatileRead和VolatileWrite方法。例如


代码如下:

volatile int m_amount;
Boolean success =int32.TryParse(“123”,out m_amount);
//生成如下警告信息:
//cs0420:对volatile字段的引用不被视为volatile.

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,在中间没有栅栏会被创建,这同样会导致我们上面讲到写之后再读顺序可能变换的问题。

(0)

相关推荐

  • java多线程并发中使用Lockers类将多线程共享资源锁定

    复制代码 代码如下: package com.yao; import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Future;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReadWriteLock;import java.util.c

  • java多线程编程之线程的生命周期

    复制代码 代码如下: // 开始线程public void start( );public void run( ); // 挂起和唤醒线程public void resume( ); // 不建议使用public void suspend( );// 不建议使用public static void sleep(long millis);public static void sleep(long millis, int nanos); // 终止线程public void stop( );   /

  • java多线程编程之使用thread类创建线程

    在Java中创建线程有两种方法:使用Thread类和使用Runnable接口.在使用Runnable接口时需要建立一个Thread实例.因此,无论是通过Thread类还是Runnable接口建立线程,都必须建立Thread类或它的子类的实例.Thread类的构造方法被重载了八次,构造方法如下: 复制代码 代码如下: public Thread( );public Thread(Runnable target);public Thread(String name);public Thread(Ru

  • java基本教程之线程休眠 java多线程教程

    本章涉及到的内容包括:1. sleep()介绍2. sleep()示例3. sleep() 与 wait()的比较 1. sleep()介绍sleep() 定义在Thread.java中.sleep() 的作用是让当前线程休眠,即当前线程会从"运行状态"进入到"休眠(阻塞)状态".sleep()会指定休眠时间,线程休眠的时间会大于/等于该休眠时间:在线程重新被唤醒时,它会由"阻塞状态"变成"就绪状态",从而等待cpu的调度执行

  • java基本教程之线程让步 java多线程教程

    本章涉及到的内容包括:1. yield()介绍2. yield()示例3. yield() 与 wait()的比较 1. yield()介绍yield()的作用是让步.它能让当前线程由"运行状态"进入到"就绪状态",从而让其它具有相同优先级的等待线程获取执行权:但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权:也有可能是当前线程又进入到"运行状态"继续运行! 2. yield()示例下面,通过示例查看它

  • java多线程编程之join方法的使用示例

    在上面的例子中多次使用到了Thread类的join方法.我想大家可能已经猜出来join方法的功能是什么了.对,join方法的功能就是使异步执行的线程变成同步执行.也就是说,当调用线程实例的start方法后,这个方法会立即返回,如果在调用start方法后后需要使用一个由这个线程计算得到的值,就必须使用join方法.如果不使用join方法,就不能保证当执行到start方法后面的某条语句时,这个线程一定会执行完.而使用join方法后,直到这个线程退出,程序才会往下执行.下面的代码演示了join的用法.

  • java多线程编程之慎重使用volatile关键字

    volatile关键字相信了解Java多线程的读者都很清楚它的作用.volatile关键字用于声明简单类型变量,如int.float.boolean等数据类型.如果这些简单数据类型声明为volatile,对它们的操作就会变成原子级别的.但这有一定的限制.例如,下面的例子中的n就不是原子级别的: 复制代码 代码如下: package mythread; public class JoinThread extends Thread{public static volatile int n = 0;p

  • java基本教程之java线程等待与java唤醒线程 java多线程教程

    本章,会对线程等待/唤醒方法进行介绍.涉及到的内容包括:1. wait(), notify(), notifyAll()等方法介绍2. wait()和notify()3. wait(long timeout)和notify()4. wait() 和 notifyAll()5. 为什么notify(), wait()等函数定义在Object中,而不是Thread中 wait(), notify(), notifyAll()等方法介绍在Object.java中,定义了wait(), notify()

  • java多线程编程之java线程简介

    一.线程概述 线程是程序运行的基本执行单元.当操作系统(不包括单线程的操作系统,如微软早期的DOS)在执行一个程序时,会在系统中建立一个进程,而在这个进程中,必须至少建立一个线程(这个线程被称为主线程)来作为这个程序运行的入口点.因此,在操作系统中运行的任何程序都至少有一个主线程.进程和线程是现代操作系统中两个必不可少的运行模型.在操作系统中可以有多个进程,这些进程包括系统进程(由操作系统内部建立的进程)和用户进程(由用户程序建立的进程):一个进程中可以有一个或多个线程.进程和进程之间不共享内存

  • java多线程并发executorservice(任务调度)类

    复制代码 代码如下: package com.yao; import java.util.concurrent.Executors;import java.util.concurrent.ScheduledExecutorService;import java.util.concurrent.ScheduledFuture;import java.util.concurrent.TimeUnit; /** * 以下是一个带方法的类,它设置了 ScheduledExecutorService ,2

  • java多线程编程之使用runnable接口创建线程

    1.将实现Runnable接口的类实例化. 2.建立一个Thread对象,并将第一步实例化后的对象作为参数传入Thread类的构造方法. 最后通过Thread类的start方法建立线程.下面的代码演示了如何使用Runnable接口来创建线程: package mythread;public class MyRunnable implements Runnable{ public void run() {  System.out.println(Thread.currentThread().get

  • java基本教程之join方法详解 java多线程教程

    本章涉及到的内容包括:1. join()介绍2. join()源码分析(基于JDK1.7.0_40)3. join()示例 1. join()介绍join() 定义在Thread.java中.join() 的作用:让"主线程"等待"子线程"结束之后才能继续运行.这句话可能有点晦涩,我们还是通过例子去理解: 复制代码 代码如下: // 主线程public class Father extends Thread {    public void run() {     

随机推荐