zoukankan      html  css  js  c++  java
  • java并发编程 volatile关键字 精准理解

    1、volatile的作用

      一个线程共享变量(类的成员变量、类的静态成员变量等)被volatile修饰之后,就具有以下作用:

      1)并发中的变量可见性(不同线程对该变量进行操作时的可见性),即一个线程修改了某个变量的值,则该新值对其他线程立即可见(可立即访问新值/立即强制写入主存);

      2)禁止指令重排(包括java编译器和CPU运行时指令重排序);

          3)禁用缓存(java虚拟机规范)---子线程的工作内存(包括了CPU缓存)。

    2、相关概念

        2.1)指令重排序:

        (1)java编译器运行时指令重排序(实质是JVM的优化处理),java源码编译生成class文件后,JVM需要在运行时(runtime)将字节码文件(class文件)转化为操作系统能够执行的指令(JIT编译器),在转换的过程中jvm会对指令进行优化调整,以提高运行效率。

        (2)CPU运行时指令重排序,cpu优化的方式,为避免处理器访问主内存的时间开销,处理器采用缓存机制(三层缓存)提高性能(缓存之间的数据一致性遵循协议规范),当CPU写缓存时,发现缓存区块正被其他CPU占用,为了提高CPU的处理性能,可能将后面的读缓存命令优先执行。

      2.2)java内存模型规范:

           线程要操作共享变量时,需要从主内存读取到工作内存,改变值后需要从工作内存同步到主内存中。多线程的情况下,同步到主内存时遵循同步协议规范。

    3、相关现象分析

      3.1)先看一段代码,

    public class VolatileInfo {
    	
    	private static boolean flag = true;
    	
    	public static void main(String[] args) {
    		new Thread(new Runnable() {
    			@Override
    			public void run() {
    				long start = System.currentTimeMillis();
    				long end = 0;
    				int index = 0;
    				while (VolatileInfo.flag) {
    					index++;
    					end = System.currentTimeMillis();
    					if ((end-start)/1000==5) {//5秒之后结束任务
    						break;
    					}
    				}
    				System.out.println("index:"+index);
    				System.out.println("cost:"+(end-start)/1000+"s");
    			}
    		}).start();
    		
    		try {
    			TimeUnit.SECONDS.sleep(2);//阻塞线程2秒
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    		
    		VolatileInfo.flag = false;
    		System.out.println("条件置为false");
    	}
    }
    

      输出结果:

    条件置为false
    index:269460217
    cost:5s
    

      耗时5秒,并不是2秒后停止(注:静态变量flag用volatile 修饰后执行时间是2秒)。

      3.2)问题分析:

        1)主线程阻塞后修改的flag的值并没有及时写入到主存,子线程没有及时读取到flag值,导致循环继续执行---即存在缓存;

        2)指令重排序,先从主内存中执行读的操作,陷入死循环,不再向主内存写入。

      3.3)解决办法:

        1)flag变量使用volatile关键词修饰;

      2)使用Synchronized加同步锁(从主存中直接读取,相当于禁用缓存,参考4.3);

    	while (VolatileInfo.flag) {
    		synchronized (this) {
    		   index++;
    		}
    		end = System.currentTimeMillis();
    		if ((end - start) / 1000 == 5) {// 5秒之后结束任务
    			break;
    		}
    	}

        3)从eclipse中将jvm执行class文件的方式改为client(默认是server模式,jvm进行了一些优化调整);

        4)从eclipse中配置添加关闭server优化的参数---此处请自行百度^_^;              ---------- 禁止指令重排。

    4、扩展知识点

       4.1)java内存模型多线程情况下的工作内存和主内存同步协议的8种原子操作:

      lock(锁定):作用于主内存,将主内存中的变量锁定,为一个线程所独有;

          unlock(解锁):作用于主内存,解除lock的锁定,释放后的变量能够被其他线程访问;

      read(读取):作用于主内存,将主内存中的变量读取到工作内存中,以便随后的load动作使用;

      load(载入):作用于工作内存,它把read读取的值保存到工作内存中的变量副本中;

      use(使用):作用于工作内存,它把工作内存中的值传递给线程代码执行引擎;

      assign(赋值):作用于工作内存,它把从执行引擎处理返回的值重新赋值给工作内存中的变量;

      store(存储):作用于工作内存,将变量副本中的值传送到主内存中,以备随后的write操作使用;

      write(写入):作用于主内存,它把store传送值写到主内存的共享变量中。

      4.2)java内存模型操作规范:

      1)将一个变量从主内存复制到工作内存要顺序依次(不一定连续)执行read、load操作;

          2)做了assign操作,必须同步回主内存等。

      4.3)保证线程共享变量可见性的方式:

      1)用final修饰的变量

          2) Synchronized 同步锁

        Synchronized规范, 进入同步块前,先清空工作内存中的共享变量,从主内存中重新加载; 解锁前必须把修改的共享变量同步回主内存。

          锁机制,锁机制保护共享资源,只有获得锁的线程才能操作共享资源。

          3) 用volatile修饰的变量

        volatile语义规范,使用volatile修饰的变量时,必须从主内存中加载,并且read、load是连续的;修改volatile修饰的变量时,必须立即同步到主内存,并且store、write是连续的。

    一字一句敲的,支持原创,转载请注明出处,谢谢:https://www.cnblogs.com/huyangshu-fs/p/10225898.html

  • 相关阅读:
    Java实现监控目录下文件变化
    Postgresql 修改用户密码
    Swing清空jtable中的数据
    delphi登录用友的信息
    用友U8的SQL SERVER 数据库结构说明表
    候老师的讲堂:视频录制、笔记软件、思维导图、画图等工具
    DELPHI 关于内存数据与 JSON
    Delphi国内优秀网站及开源项目
    SQL Server 阻止了对组件Ad Hoc Distributed Queries访问的方法
    SQL Server跨服务器查询
  • 原文地址:https://www.cnblogs.com/huyangshu-fs/p/10225898.html
Copyright © 2011-2022 走看看