深入浅出多线程系列之八:内存栅栏和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);
        }
复制代码

 

如果方法AB都在不同的线程下并发的执行,方法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
        }
复制代码

 

如果Test1Test2在不同的线程中并发执行,有可能字段的值都是0,(尽管在xy上应用了volatile 关键字)

 

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

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

 

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

volatile  m_amount
m_amount  
=  m_amount  +  m_amount.

 

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

VolatileReadVolatileWrite方法。例如

 

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

 

VolatileRead VolatileWrite

 

从技术上讲,Thread类的静态方法VolatileReadVolatileWrite在读取一个 变量上和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,在中间没有栅栏会被创建,这同样会导致我们上面讲到写之后再读顺序可能变换的问题。

 






本文转自LoveJenny博客园博客,原文链接:http://www.cnblogs.com/LoveJenny/archive/2011/05/29/2060718.html,如需转载请自行联系原作者
目录
相关文章
|
5月前
|
存储 缓存 Java
【高薪程序员必看】万字长文拆解Java并发编程!(5):深入理解JMM:Java内存模型的三大特性与volatile底层原理
JMM,Java Memory Model,Java内存模型,定义了主内存,工作内存,确保Java在不同平台上的正确运行主内存Main Memory:所有线程共享的内存区域,所有的变量都存储在主存中工作内存Working Memory:每个线程拥有自己的工作内存,用于保存变量的副本.线程执行过程中先将主内存中的变量读到工作内存中,对变量进行操作之后再将变量写入主内存,jvm概念说明主内存所有线程共享的内存区域,存储原始变量(堆内存中的对象实例和静态变量)工作内存。
161 0
|
12月前
|
缓存 安全 Java
使用 Java 内存模型解决多线程中的数据竞争问题
【10月更文挑战第11天】在 Java 多线程编程中,数据竞争是一个常见问题。通过使用 `synchronized` 关键字、`volatile` 关键字、原子类、显式锁、避免共享可变数据、合理设计数据结构、遵循线程安全原则和使用线程池等方法,可以有效解决数据竞争问题,确保程序的正确性和稳定性。
295 57
|
10月前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
210 7
|
11月前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
122 4
|
11月前
|
监控 Java 数据库连接
线程池在高并发下如何防止内存泄漏?
线程池在高并发下如何防止内存泄漏?
376 6
|
12月前
|
监控 Java 数据库连接
使用线程池时,如何避免内存泄漏的问题?
使用线程池时,如何避免内存泄漏的问题?
|
12月前
|
Java 开发者
在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选
【10月更文挑战第6天】在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选。相比 `synchronized`,Lock 提供了更灵活强大的线程同步机制,包括可中断等待、超时等待、重入锁及读写锁等高级特性,极大提升了多线程应用的性能和可靠性。通过示例对比,可以看出 Lock 接口通过 `lock()` 和 `unlock()` 明确管理锁的获取和释放,避免死锁风险,并支持公平锁选择和条件变量,使其在高并发场景下更具优势。掌握 Lock 接口将助力开发者构建更高效、可靠的多线程应用。
81 2
|
12月前
|
监控 数据可视化 Java
如何使用JDK自带的监控工具JConsole来监控线程池的内存使用情况?
如何使用JDK自带的监控工具JConsole来监控线程池的内存使用情况?
|
12月前
|
缓存 Java 编译器
【多线程-从零开始-伍】volatile关键字和内存可见性问题
【多线程-从零开始-伍】volatile关键字和内存可见性问题
136 0
|
3月前
|
存储
阿里云轻量应用服务器收费标准价格表:200Mbps带宽、CPU内存及存储配置详解
阿里云香港轻量应用服务器,200Mbps带宽,免备案,支持多IP及国际线路,月租25元起,年付享8.5折优惠,适用于网站、应用等多种场景。
819 0

热门文章

最新文章