volatile原理

volatile原理

今天总结一下java多线程机制,以及volatile

       首先,为什么需要多线程?

      主要是因为计算机的运算能力远远大于I/O,通信传输,还有数据库访问等操作。所以缓存出现了,从而提高了访问速度。但是由于会有多个缓存,以及数据读写问题,很有可能会读到脏数据,其实这也就是缓存的一致性。

      另外为了提高效率,处理器会对程序进行乱序执行优化,而对于虚拟机来说,就是指令重排序。意思就是说代码顺序与实际执行顺序无关,实际执行顺序是虚拟机根据前后依赖关系,结合运算器来决定的,但是结果是一样的。

      走入正题,先介绍一下java内存模型,内存模型主要用来屏蔽硬件与内存访问的差异。

     

     对于每一个线程会有工作内存,多个线程共享一个主内存,例如对象实例就在主内存会多个线程共享,而引用这个对象的变量实际在每个线程的工作内存,工作内存拥有主内存实例的副本拷贝,通过它来对实例进行,读取与赋值都在工作内存,并且线程之间无法读取对方的变量,都是通过主内存做一个过渡作用。(这里工作内存与主内存跟堆内存与栈内存不是一个概念,这是为了好理解)

   接下来工作内存与主内存怎么进行交互?虚拟机定义了8种原子操作,包括lock(锁定主内存的变量,使其被某一线程独占),unlock(同理),read(把一个主内存的变量传递到工作内存中,以便load),load(将从主内存传递的值传递到工作内存的变量副本中),store(将工作内存中变量副本传递到主内存中去,以便write),write(将工作内存传递过来的值赋到主内存中变量),use(将工作内存的值传递给执行引擎),assign(将执行引擎的值传递到工作内存),这8中操作可以用来确定你的访问是否安全。

  下面介绍一下volatile,经常被问到的一个关键字,他的作用主要有两个,我们一一说明:1 保证变量在各个线程的可见性,意思就是说这个变量的值一修改,其他线程可以立即得知。而一个普通变量需要先写回主内存,然后其他线程去读取这个值。2:禁止指令重排序优化。然而它并不能保证原子性,以及运算的线程安全,下面代码来解释一下第一个特性。

  1.  public class VolatileTest extends Thread{  
  2.    public  static volatile int a=0;  
  3.    public void run() {  
  4.             a++;  
  5.             }  
  6.     public static void main(String[] args) {  
  7.         // TODO Auto-generated method stub  
  8.         VolatileTest array[]=new VolatileTest[10000];  
  9.              for (int i = 0; i < array.length; i++) {  
  10.                  array[i]=new VolatileTest();  
  11.                  array[i].start();  
  12.                                     }  
  13.          System.out.println(VolatileTest.a);  
  14.     }  
  15. }  

我们希望结果会是10000,然而并不是,原因就是a++这一条指令并不是原子操作,volatile的确保证从主内存获得的数据是最正确的,但是当你运算的时候,其他线程很有可能会把一个值穿进去,导致值会变小。

那么什么情况下用volatile呢?一定要明白,它的开销一定会小与同步块。下面就是使用的情况,不符合这两条就要用同步块了。

1:运算结果并不依赖与当前值。

2:变量不需要与其他变量参与不变约束。

同样用代码解释一下。

  1. public class VolatileTest extends Thread{  
  2.    public   static volatile int a=0;  
  3.    public void run() {  
  4.                    a=2;  
  5.             }  
  6.     public static void main(String[] args) {  
  7.         // TODO Auto-generated method stub  
  8.         VolatileTest array[]=new VolatileTest[100];  
  9.              for (int i = 0; i < array.length; i++) {  
  10.                  array[i]=new VolatileTest();  
  11.                  array[i].start();  
  12.                   System.out.print(array[i].a+" ");  
  13.      }  
  14.       }}  


     个人理解就是a的值不依赖与现在在主内存a的实际值,不管a是几,都变成1,而其他线程也会立即受到通知,因为也没有运算,也会直接变为1.

接下来讲一下指令重排序优化的东东,其实指令重排序对于单线程来说有利无害,反正最后的结果是一样的,而且还提高了效率,但是对于多线程,可能会出现一些问题,而volatile修饰的变量,会在操作的时候,设置一个屏障,后面的操作,肯定不会比这个提前。否则后面的操作先执行,从而提前影响其他的线程。

  好的,下面介绍几个概念1:原子性   就像前面说的那8张操作就是,粒度小到多线程也不可能拆开它,而用synchronized,内部的东西其实就是一个组装的“大原子”,但是记住volatile是不可以的 2 可见性 意思就是线程修改了值之后会立即同步到主内存,并且获取值会从主内存直接获取,而非缓存,volatile和synchronized都可以保证 3有序性 意思是保证线程内部执行顺序,volatile可以保证禁止指令重排序,而synchronized,直接就锁上了,所以它能解决几乎所有同步问题,造成了滥用。

线程是cpu调度的基本单位,粒度比进程小,Thread的类很多方法是native,可能会为了效率,然而同时可能会平台相关,注意线程的优先级不太靠谱,以为可能与平台线程的优先级不一样,造成冲突。再次补充一个线程状态模型(本文章主要介绍java多线程模型,以及volatile,线程基础不再赘述)

阻塞状态与挂起状态的区别在于阻塞在等待一个排它锁,而挂起是等待时间到,或者是唤醒。

更多细节请查看我的线程基础的这篇博客,多谢大家支持

本博客知识来源于深入理解java虚拟机,值得一看,强力 推荐,特别底层!!!

 

13.Java中Volatile底层原理与应用

Volatile定义与原理

Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该通过排它锁单独获取这个变量

Java语言提供了Violatile来确保多处理开发中,共享变量的“可见性”,即当另外一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。它是轻量级的synchronized,不会引起线程上下文的切换和调度,执行开销更小。

使用Violatile修饰的变量在汇编阶段,会多出一条lock前缀指令,它在多核处理器下回引发两件事情:

  1. 将当前处理器缓存行的数据写回到系统内存
  2. 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

通常处理器和内存之间都有几级缓存来提高处理速度,处理器先将内存中的数据读取到内部缓存后再进行操作,但是对于缓存写会内存的时机则无法得知,因此在一个处理器里修改的变量值,不一定能及时写会缓存,这种变量修改对其他处理器变得“不可见”了。但是,使用Volatile修饰的变量,在写操作的时候,会强制将这个变量所在缓存行的数据写回到内存中,但即使写回到内存,其他处理器也有可能使用内部的缓存数据,从而导致变量不一致,所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期,如果过期,就会将该缓存行设置成无效状态,下次要使用就会重新从内存中读取。

追加字节优化Volatile性能

在某些情况下,通过将共享变量追加到64字节可以优化其使用性能。

在JDK 7 的并发包里,有一个队列集合类LinkedTransferQueue,它在使用volatile变量时,用一种追加字节的方式来优化队列出队和入队的性能。队里定义了两个共享结点,头结点和尾结点,都由使用了volatile的内部类定义,通过将两个共享结点的字节数增加到64字节来优化效率,具体分析如下:

部分CPU的L1、L2或L3缓存的高速缓存行64字节宽,不支持部分填充缓存行

这意味着,如果队列的头结点和尾结点都不足64字节,处理器会将他们读到同一个高速缓存行,在多处理器下每个处理器都会缓存同样的头尾结点,当一个处理器试图修改头结点时,会将整个缓存行锁定,那么在缓存一致性的机制下,其他处理器不能访问自己高速缓存中的尾节点,而头尾结点在队列中都是会频繁访问的,因此会影响使用性能。而通过填充字节使头尾结点加载到不同的缓存行,避免头尾结点在修改时相互锁定。 
但是在以下两种场景,不应该使用这种优化方式:

  1. 缓存行非64字节宽的处理器(自行调整补充字节长度,原理一样)
  2. 共享变量不会被频繁的写。追加字节会导致CPU读取性能下降,如果共享变量写的频率很低,那么被锁的几率也很小,就没必要避免相互锁定了

Volatile无法保证原子性

volatile是一种“轻量级的锁”,它能保证锁的可见性,但不能保证锁的原子性。 

如下面的例子

public class Test {
    public volatile int inc = 0;

    public void increase() {
        inc++;
    }

    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }

        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

上面程序输出的结果是多少?很多人可能都以为是10000,觉得对变量inc进行自增操作,由于volatile保证了可见性,那么在每个线程中对inc自增完之后,在其他线程中都能看到修改后的值啊,所以有10个线程分别进行了1000次操作,那么最终inc的值应该是1000*10=10000。这里面就有一个误区了,volatile关键字能保证可见性没有错,但是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。 
   
  由于自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行,就有可能导致下面这种情况出现: 
   
  假如某个时刻变量inc的值为10,线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。 
然后线程1接着进行加1操作,由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。那么两个线程分别进行了一次自增操作后,inc只增加了1。

  解释到这里,可能有朋友会有疑问,不对啊,前面不是保证一个变量在修改volatile变量时,会让缓存行无效吗?然后其他线程去读就会读到新的值,但是要注意,线程1对变量进行读取操作之后,被阻塞了的话,并没有对inc值进行修改。然后虽然volatile能保证线程2对变量inc的值读取是从内存中读取的,但是线程1没有进行修改,所以线程2根本就不会看到修改的值。

  根源就在这里,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。因此在使用Violatile修饰变量时,一定要保证对该变量的写操作是原子性的,例如程序中的状态变量,对该变量的修改不依赖于其当前值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值