>>分享Java编程技术,对《Java面向对象编程》等书籍提供技术支持 书籍支持  卫琴直播  品书摘要  在线测试  资源下载  联系我们
发表一个新主题 开启一个新投票 回复文章 您是本文章第 21584 个阅读者 刷新本主题
 * 贴子主题:  Java多线程volatile详解 回复文章 点赞(0)  收藏  
作者:flybird    发表时间:2020-02-07 00:58:35     消息  查看  搜索  好友  邮件  复制  引用

                                                                                                                      

volatile定义

     Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。            

volatile的作用

     先让我们说说volatile关键字的作用。它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。            

volatile代码示例

单例模式(重排序)

  public   class  Singleton {
     public  static  volatile Singleton singleton;

     /**
     * 构造函数私有
     */

     private  Singleton(){}

     /**
     * 单例实现
     *  @author fuyuwei
     * 2017年5月14日 上午10:07:07
     *  @return
     */

     public  static Singleton  getInstance(){
         if(singleton ==  null){
             synchronized (singleton) {
                 if(singleton ==  null){
                    singleton =  new Singleton();
                }
            }
        }
         return singleton;
    }
}

     我们知道实例化一个对象经过分配内存空间、初始化对象、将内存空间的地址赋值给对应的引用,上面的单例模式可以解释为分配内存空间、将内存地址赋值给对应的应用、初始化对象。上面的代码如果我们不加volatile在并发环境下可能会出现Singleton的多次实例化,假如线程A进入getInstance方法,发现singleton==null,然后加锁通过new Singleton进行实例化,然后释放锁,我们知道new Singleton在JVM中其实是分为3步,假如线程啊在释放锁之后还没来得及通知其他线程,这时候线程B进入getInstance的时候发现singleton==null便会再次实例化。            

可见性

     一个线程修改了共享变量值,而另一个线程却看不到。引起可见性问题的主要原因是每个线程拥有自己的一个高速缓存区——线程工作内存。volatile关键字能有效的解决这个问题                

  public  class Volatile {
     int m =  0;
     int n =  1;

     public  void  set(){
        m =  6;
        n = m;
    }

     public  void  print(){
        System. out.println( "m:"+m+ ",n:"+n);
    }

     public  static  void  main(String[] args) {
         while( true){
            final Volatile v =  new Volatile();
             new Thread( new Runnable(){
                @Override
                 public  void  run() {
                     try {
                        Thread.sleep( 1000);
                    }  catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    v. set();
                }

            }).start();

             new Thread( new Runnable(){
                @Override
                 public  void  run() {
                     try {
                        Thread.sleep( 1000);
                    }  catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    v.print();
                }

            }).start();
        }
    }
}

     正常情况下m=0,n=1;m=6,n=6,通过运行我们发现了m=0,n6(需要长时间运行)                

  m :6, n :6
m :6, n :6
m :6, n :6
m :6, n :6
m :6, n :6
m :6, n :6
m :6, n :6
m :6, n :6
m :6, n :6
m :6, n :6
m :0, n :1
m :6, n :6
m :6, n :6
m :6, n :6
m :6, n :6
m :6, n :6
m :6, n :6
m :6, n :6
m :6, n :6
m :6, n :6
m :0, n :6
m :6, n :6
m :6, n :6
m :6, n :6
m :0, n :1
m :0, n :1
m :0, n :1
m :6, n :6
m :0, n :1
m :0, n :1
m :0, n :1
m :6, n :6
m :6, n :6
m :6, n :6
m :0, n :1
m :6, n :6
m :6, n :6

     对volatile变量的写操作与普通变量的主要区别有两点:

  (1)修改volatile变量时会强制将修改后的值刷新的主内存中。

  (2)修改volatile变量后会导致其他线程工作内存中对应的变量值失效。因此,再读取该变量值的时候就需要重新从读取主内存中的值。            

volatile并不能保证原理性

  package com.swk.thread;
public   class  Volatile {
     private  volatile  int m =  0;

     public  void  incr(){
        m++;
    }

     public  static  void  main(String[] args) {
         final Volatile v =  new Volatile();
         for( int i= 0;i< 1000;i++){
             new Thread( new Runnable(){
                 @Override
                 public  void  run() {
                     try {
                        Thread.sleep( 1000);
                    }  catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    v.incr();;
                }

            }).start();
        }
         try {
            Thread.sleep( 10000); // 确保1000次循环执行完毕
        }  catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(v.m);
    }
}

     输出结果:950,并不是我们想象中的1000,如果我们在incr加上synchronized,输出结果是1000

原因也很简单,i++其实是一个复合操作,包括三步骤:

  (1)读取i的值。

  (2)对i加1。

  (3)将i的值写回内存。

volatile是无法保证这三个操作是具有原子性的,我们可以通过AtomicInteger或者Synchronized来保证+1操作的原子性。            

volatile底层实现

     在了解volatile实现原理之前,我们先来看下与其实现原理相关的CPU术语与说明

    
术语 英文单词 术语描述
内存屏障 memory barries 是一组处理器指令,用于实现对内存操作的顺序限制
缓冲行 cache line 缓存中可以分配的最小存储单位。处理器填写缓存线时会加载整个缓存线,需要使用多个主内存周期
原子操作 atomic operations 不可中断的一个或一些列操作
缓冲行填充 cache line fill 当处理其识别到内存中读取操作数是可缓存的,处理器读取整个缓存行到适当的缓存
缓存命中 cache hit 如果进行高速缓存行填充操作的内存位置仍然是下次处理器访问的地址时,处理器从高速缓存中读取,而不是从内存中读取
写命中 write hit 当处理器操作数写回到一个内存缓存区域时,他首先会检查这个缓存的内存地址是否在缓存行中,如果存在一个有效的缓存行,则处理器将这个操作数写回到缓存,而不是会写到内存
写缺失 write misses the cache 一个有效的缓存行被写入到不存在的内存区域


         volatile是如何来保证可见性的呢?让我们在X86处理器下通过工具获取JIT编译器生成的汇编指令来查看对volatile进行写操作时,CPU会做什么事情。Java代码如下:        

  private valatile Singleton instance =  new Singleton();

     转成汇编代码如下                

  0x01a3de1d: movb  $0× 0, 0× 1104800( %esi); 0x01a3de24: lock addl  $0× 0,( %esp);

     当有volatile变量修饰时会出现lock addl $0×0,(%esp),Lock前缀的指令在多核处理器下会引发了两件事情

1)将当前处理器缓存行的数据写回到系统内存。

2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。            

volatile使用场景

一个线程写,多个线程读

  volatile  boolean shutdownRequested;
...
public  void  shutdown() { shutdownRequested =  true; }
public  void  doWork() {
     while (!shutdownRequested) {
         // do stuff
    }
}

结合使用 volatile 和 synchronized 实现 “开销较低的读-写锁”

     volatile 允许多个线程执行读操作,因此当使用 volatile 保证读代码路径时,要比使用锁执行全部代码路径获得更高的共享度 —— 就像读-写操作一样。                

  public  class CheesyCounter {
     private  volatile  int  value;
     public  int  getValue() {  return  value; }
     public synchronized  int  increment() {
         return  value++;
    }
}

                            ----------------------------
原文链接:https://blog.csdn.net/fuyuwei2015/article/details/71939591

程序猿的技术大观园:www.javathinker.net



[这个贴子最后由 flybird 在 2020-02-24 12:50:47 重新编辑]
  Java面向对象编程-->内部类
  JavaWeb开发-->Servlet技术详解(Ⅰ)
  JSP与Hibernate开发-->映射对象标识符
  Java网络编程-->非阻塞通信
  精通Spring-->组合(Composition)API
  Vue3开发-->虚拟DOM和render()函数
  解密Java类文件的数据结构
  NIO模式的IO多路复用底层原理
  Eclipse和MyEclipse的区别
  内部类的编程练习题
  内部类的种类和用法
  JDBC API中的桥接模式
  害怕面试被问HashMap?
  java中的Static、final、Static final各种用法
  Java读取大文件的高效率实现_java大文件
  Eclipse使用指南:创建Java项目的步骤
  Java设计模式:备忘录模式
  Java设计模式:享元模式
  Java入门实用代码:获取远程文件大小
  Java入门实用代码:打印平行四边形
  Java性能优化总结
  更多...
 IPIP: 已设置保密
楼主      
1页 0条记录 当前第1
发表一个新主题 开启一个新投票 回复文章


中文版权所有: JavaThinker技术网站 Copyright 2016-2026 沪ICP备16029593号-2
荟萃Java程序员智慧的结晶,分享交流Java前沿技术。  联系我们
如有技术文章涉及侵权,请与本站管理员联系。