`
uule
  • 浏览: 6297538 次
  • 性别: Icon_minigender_1
  • 来自: 一片神奇的土地
社区版块
存档分类
最新评论

volatile关键字

阅读更多

Java 内存模型

2.1 Java 内存模型的基本原理

Java 内存模型,由于 Java 被设计为跨平台的语言,在内存管理上,显然也要有一个统一的 模型。系统存在一个主内存 (Main Memory) , Java 中所有变量都储存在主存中,对于所有线程都是共享的每条线程都有自己的工作内存 (Working Memory) ,工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。

因为当线程处于不同的cpu中时,它各自的变量保存在各自cpu的寄存器或者高速缓存中,这样回事的变量对于其它线程暂时不可见。

 

2.2 Volatile 的内存工作原理

Volatile 是保证多个线程之间变量可见性的,也就是说一个线程对变量进行了写操作,另外一个线程能够获取它最新的值。

它的工作原理是,它对写和读都是直接操作工作主存的。(这个可以通过操作字节码看到)

 

2.3 Synchronized 的内存操作模型 :

Synchronized, 有两层语义,一个是互斥,一个是可见性。在可见性上面,它的工作原理有点不同:当线程进入同步块时,会从主存里面获取最新值,然后当线程离开同步块时,再把变量的值更新到主存。

http://tomyz0223.iteye.com/blog/1001778

 

 

1、在JVM 1.2之前,Java的内存模型实现总是从主存读取变量,是不需要进行特别的注意的。而随着JVM的成熟和优化,现在在多线程环境下volatile关键字的使用变得非常重要。在当前的Java内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。要解决这个问题,只需要像在本程序中的这样,把该变量声明为volatile(不稳定的)即可,这就指示JVM,这个变量是不稳定的,每次使用它都到主存中进行读取一般说来,多任务环境下各任务间共享的标志都应该加volatile修饰。 

Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。 

Java语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才与共享成员变量的原始值对比。 

这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化。 

而volatile关键字就是提示VM:对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。 

使用建议:在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用。 

 

 

2、volatile关键字有什么用?

  恐怕比较一下volatile和synchronized的不同是最容易解释清楚的。volatile是变量修饰符,而synchronized则作用于一段代码或方法;看如下三句get代码:

int i1;                   int geti1() {return i1;}

volatile int i2;        int geti2() {return i2;}

int i3;                   synchronized  int geti3() {return i3;}

 

  geti1()得到存储在当前线程中i1的数值。多个线程有多个i1变量拷贝,而且这些i1之间可以互不相同。换句话说,另一个线程可能已经改变了它线程内的i1值,而这个值可以和当前线程中的i1值不相同。事实上,Java有个思想叫“主”内存区域,这里存放了变量目前的“准确值”。每个线程可以有它自己的变量拷贝,而这个变量拷贝值可以和“主”内存区域里存放的不同。因此实际上存在一种可能:“主”内存区域里的i1值是1,线程1里的i1值是2,线程2里的i1值是3——这在线程1和线程2都改变了它们各自的i1值,而且这个改变还没来得及传递给“主”内存区域或其他线程时就会发生。

  而geti2()得到的是“主”内存区域的i2数值。用volatile修饰后的变量不允许有不同于“主”内存区域的变量拷贝。换句话说,一个变量经volatile修饰后在所有线程中必须是同步的任何线程中改变了它的值,所有其他线程立即获取到了相同的值。理所当然的,volatile修饰的变量存取时比一般变量消耗的资源要多一点,因为线程有它自己的变量拷贝更为高效。

  既然volatile关键字已经实现了线程间数据同步,又要synchronized干什么呢?呵呵,它们之间有两点不同。首先,synchronized获得并释放监视器——如果两个线程使用了同一个对象锁,监视器能强制保证代码块同时只被一个线程所执行——这是众所周知的事实。但是,synchronized也同步内存:事实上,synchronized在“主”内存区域同步整个线程的内存。因此,执行geti3()方法做了如下几步:

1. 线程请求获得监视this对象的对象锁(假设未被锁,否则线程等待直到锁释放)

2. 线程内存的数据被消除,从“主”内存区域中读入(Java虚拟机能优化此步)

3. 代码块被执行

4. 对于变量的任何改变现在可以安全地写到“主”内存区域中(不过geti3()方法不会改变变量值)

5. 线程释放监视this对象的对象锁

  因此volatile只是在线程内存和“主”内存间同步某个变量的值,而synchronized通过锁定和解锁某个监视器同步所有变量的值。显然synchronized要比volatile消耗更多资源。 

 

 

3、性能考虑

使用 volatile 变量的主要原因是其简易性:在某些情形下,使用 volatile 变量要比使用相应的锁简单得多。使用 volatile 变量次要原因是其性能:某些情况下,volatile 变量同步机制的性能要优于锁。

volatile 操作不会像锁一样造成阻塞,因此,如果读操作的次数要远远超过写操作,与锁相比,volatile 变量通常能够减少同步的性能开销。

参考:http://www.ibm.com/developerworks/cn/java/j-jtp06197.html

 

 

 

在知道synchronized配和对象内部锁的机制以后,可以提高写出正确同步的并发程序成功率,但是这时候会遇到另一个大问题:性能!是的,对于synchronized带来的可能庞大的性能成本,开发者们总结出不同的优秀的优化方案:常见的是锁的分解和锁持有时间的最小化。有效的降低锁持有的时间对竞争线程激烈的调用会大大的提高性能,所以不要轻易的在方法上声明synchronized,应该在需要保护的代码块上添加synchronized。另一个方案是拆分锁的竞争颗粒的大小,与其几百个线程竞争一个对象的锁,不如几个或者几十个线程竞争多个对象的锁,常见的应用是ConcurrentHashMap的实现,其内部有类似的锁对象数组维护每段表内的线程竞争,默认16个对象锁,当然提供参数可调。这对于存储了成千上万个实例的map性能提升不言而喻,线程的竞争被分散到多段的小竞争,再也不用全部的堆在门口傻等了。 
   但是synchronized同步和类似的机制带来的性能成本,还是使得开发者不能不研究无锁和低成本的同步机制来保证并发的性能。volatile就是被认为“轻量级的synchronized”,但是使用其虽然可以简化同步的编码,并且运行开销相对于JVM没有优化的竞争线程同步低,但是滥用将不能保证程序的正确性。锁的两个特性是:互斥和可见。互斥保证了同时只有一个线程持有对象锁进行共享数据的操作,从而保证了数据操作的原子性,而可见则保证共享数据的修改在下一个线程获得锁后看到更新后的数据。volatile仅仅保证了无锁的可见性,但是不提供原子性操作的保证!这是因为volatile关键字作用的设计是JVM阻止volatile变量的值放入处理器的寄存器,在写入值以后会被从处理器的cache中flush掉,写到内存中去。这样读的时候限制处理器的cache是无效的,只能从内存读取值,保证了可见性。从这个实现可以看出volatile的使用场景:多线程大量的读取,极少量或者一次性的写入,并且还有其他限制。 
   由于其无法保证“读-修改-写”这样操作的原子性(当然java.util.concurrent.atomic包内的实现满足这些操作,主要是通过CAS--比较交换的机制),所以像++,--,+=,-=这样的变量操作,即使声明volatile也不会保证正确性。围绕这个原理的主题,我们可以大致的整理一下volatile代替synchronized的条件:对变量的写操作不依赖自身的状态。所以除了刚刚介绍的操作外,例如: 

  1. private volatile boolean flag;  
  2.   if(!flag) {  
  3.     flag == true;  
  4. }  

类似这样的操作也是违反volatile使用条件的,很可能造成程序的问题。所以使用volatile的简单场景是一次性的写入之后,大量线程的读取并且不再改变变量的值(如果这样的话,都不是并发了)。这个关键字的优势还是在于多线程的读取,既保证了读取的低开销(与单线程程序变量差不多),又能保证读到的是最新的值。所以利用这个优势我们可以结合synchronized使用实现低开销读写锁: 

如果读操作远远超过写操作,您可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销

  1. public class AnotherSyncSample {  
  2.     private volatile int counter;  
  3.   
  4.     public int getCounter() {   
  5.     return counter;   
  6.     }  
  7.   
  8.     public synchronized void add() {  
  9.         counter++;  
  10.     }  
  11. }  

 

 

之所以将这种技术称之为 “开销较低的读-写锁” 是因为您使用了不同的同步机制进行读写操作。因为本例中的写操作违反了使用 volatile 的第一个条件,因此不能使用 volatile 安全地实现计数器 —— 您必须使用锁。然而,您可以在读操作中使用 volatile 确保当前值的可见性,因此可以使用锁进行所有变化的操作,使用 volatile 进行只读操作。其中,锁一次只允许一个线程访问值,volatile 允许多个线程执行读操作,因此当使用 volatile 保证读代码路径时,要比使用锁执行全部代码路径获得更高的共享度 —— 就像读-写操作一样。然而,要随时牢记这种模式的弱点:如果超越了该模式的最基本应用,结合这两个竞争的同步机制将变得非常困难。


这个简单的例子在读的方法上没有使用synchronized关键字,所以读的操作几乎没有等待;而由于写的操作是原子性的违反了使用条件,不能得到保证,所以使用synchronized同步得到写的正确性保证,这个模型在多读取少写入的实际场景中应该要比都用synchronized的性能有不小的提升。 

来源:http://yanxuxin.iteye.com/blog/549211

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics