热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

音视频开发(四十五):Java并发编程之内存模型与volatile

目录JVM内存结构和内存模型并发编程中的三个概念与重排序happens-before原则volatile原理volatile使用场景一、JVM内存结构和内存模型1.1JVM内存

目录


  1. JVM内存结构和内存模型

  2. 并发编程中的三个概念与重排序

  3. happens-before原则

  4. volatile原理

  5. volatile使用场景


一、JVM内存结构和内存模型


1.1 JVM内存结构

Java虚拟机在运行程序时会把其自动管理的内存划分为以上几个区域,每个区域都有的用途以及创建销毁的时机

方法区属于线程共享的内存区域,主要用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

Java 堆也是属于线程共享的内存区域,它在虚拟机启动时创建,是Java 虚拟机所管理的内存中最大的一块,主要用于存放对象实例,几乎所有的对象实例都在这里分配内存,注意Java 堆是垃圾收集器管理的主要区域

** 程序计数器** 属于线程私有的数据区域,是一小块内存空间,主要代表当前线程所执行的字节码行号指示器。

虚拟机栈属于线程私有的数据区域,与线程同时创建,总数与线程关联,代表Java方法执行的内存模型。每个方法执行时都会创建一个栈桢来存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。每个方法从调用至结束就对应一个栈桢在虚拟机栈中的入栈和出栈过程

** 本地方法栈** 属于线程私有的数据区域,这部分主要与虚拟机用到的 Native 方法相关。


关注+私信我,领取2022最新最全学习提升资料,内容包括(C/C++,Linux,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)



1.2 JVM内存模型


Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成
引用自:全面理解Java内存模型(JMM)及volatile关键字 


 


二、 并发编程中的三个概念与重排序


2.1 原子性


原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响



2.2 可见性


可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值
由于线程对共享变量的操作都是线程拷贝到各自的工作内存进行操作后才写回到主内存中的,这就可能存在一个线程A修改了共享变量x的值,还未写回主内存时,另外一个线程B又对主内存中同一个共享变量x进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题,
另外指令重排以及编译器优化也可能导致可见性问题,通过前面的分析,我们知道无论是编译器优化还是处理器优化的重排现象,在多线程环境下,确实会导致程序轮序执行的问题,从而也就导致可见性问题。



2.3 有序性


程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未必一致



重排序

计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种


  • 编译器优化的重排
    编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

  • 指令并行的重排
    现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序

  • 内存系统的重排
    由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差

重排只会保证单线程中串行语义的执行的一致性,但并不会关心多线程间的语义一致性


三、happens-before原则

《JSR-133:Java Memory Model and Thread Specification》中定义了happens-before的规则,具体如下:


3.1 程序顺序规则

一个线程中的每个操作,happens-before于该线程中的任意后续操作

这里有个疑惑:** 程序顺序规则和编译器的指令重排序不冲突吗?**
今天和朋友讨论了这个问题,发现自己对happens-before的含义没有理解。
其实happens-before关注的是结果上的可见性,而不是执行顺序上的可见性。
引用朋友的一句很到位的话:执行结果没有相关性是一种特殊的可见

double pi = 3.14; //A
double r = 1.0; //B
double area = pi * r * r; //C

程序顺序规则
根据happens- before的程序顺序规则,上面计算圆的面积的示例代码存在三个happens- before关系:A happens- before B;
B happens- before C;
A happens- before C;
这里的第3个happens- before关系,是根据happens- before的传递性推导出来的。

这里A happens- before B,但实际执行时B却可以排在A之前执行(看上面的重排序后的执行顺序)。

如果A happens- before B,JMM并不要求A一定要在B之前执行。JMM仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。这里操作A的执行结果不需要对操作B可见;而且重排序操作A和操作B后的执行结果,与操作A和操作B按happens- before顺序执行的结果一致。在这种情况下,JMM会认为这种重排序并不非法(not illegal),JMM允许这种重排序。


3.2 监视器锁规则

对于一个锁的解锁,happens-before于随后对这个锁的加锁


3.3 volatile变量规则

对于一个volatile变量的写,happens-before于任意后续对这个volatile变量的读


3.4 传递性

如果A happens-before B,且B happens-before C,那么 A happens-before C.


3.5 start()规则

如果线程A执行操作ThreadB.start(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B的任意操作


3.6 join()规则

如果线程A执行操作ThreadB.join()并成功返回,那么线程B中任意操作happens-before于线程A从ThreadB.join()操作成功返回


四、volatile作用及原理

在程序的执行过程中,涉及到两个方面:指令的执行和数据的读写。其中指令的执行通过处理器来完成,而数据的读写则要依赖于系统内存,但是处理器的执行速度要远大于内存数据的读写,因此在处理器中加入了高速缓存。在程序的执行过程中,会先将数据拷贝到处理器的高速缓存中,待运算结束后再回写到系统内存当中。


4.1 volatile的作用

这样如果在多个线程中多一个变量进行读写,就可能引起可见性的问题。而volatile可以很好的解决可见性和有序性问题。但不能保证原子性。它相比 synchronized 不会引起线程上下文的切换和调度。

那它是如何保证可见性和有序性的呐?


变量声明了volatile,在对该变量进行写操作时,

  1. JVM会向CPU发送一条Lock前缀的指令,将这个变量所在缓存行(CPU高速缓存中可以分配的最小存储单元,一个高速缓冲行通常是64个字节宽)写回到系统内存。

  2. 对于多处理器,为了保证各个处理器的缓存一致性,每个处理器通过嗅探(类似于观察者)在总线上传播的数据来检查自己的缓存是否过期,当处理器发现自己缓存行对应的内存地址被修改,就会将该处理器的高速缓存行设置为无效状态,当需要这个变量的数据时,会重新从系统内存中读取到该处理器的高速缓存中。
    引用自 《Java并发编程的艺术》


 

volatile在并发编程中很常见,但也容易被滥用
volatile变量带来可见性的保证,还防止了指令重排序。不过这一切是以牺牲优化(消除缓存,直接操作主存开销增加)为代价,所以不应该滥用volatile,仅在确实需要增强变量可见性的时候使用。


4.2 volatile写-读建立的happens-before关系

class VolatileExample {int a = 0;volatile boolean flag = false;public void write(){a = 1; //1flag = true; //2}public void reader(){if(flag) {. //3int i = a; //4}}
}

假设线程A执行write方法之后,线程B执行reader方法,根据happens-before规则
关系如下:
**根据程序顺序规则:** 1 happens-before 2; 3 happens-before 4
根据volatile规则: 2 happens-before 3
传递性规则: 1 happens-before 4

 


五、volatile使用场景


5.1 状态标志

线程a执行doWork()的过程中,可能有另外的线程b调用了release,给flag变量添加volatile标记,保证其可见性。

volatile boolean flag; public void release() { flag = true;
} public void doWork() { while (!flag) { // do something}
}

5.2 一次性安全发布

volatile可以禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。下面看一个非常典型的禁止重排优化的例子双重检测锁的例子

private volatile static Singleton mInstace; public static Singleton getInstance(){ //第一次null检查 if(mInstace == null){ synchronized(Singleton.class) { //第二次null检查 if(mInstace == null){ mInstace = new Singleton(); } } } return mInstace;

其中mInstace = new Singleton();可以分为以下3步完成(伪代码)
memory = allocate(); //1.分配对象内存空间
instance(memory); //2.初始化对象
instance = memory; //3.设置instance指向刚分配的内存地址,此时instance!=null

由于步骤2和步骤3间可能会重排序,如下:
memory = allocate(); //1.分配对象内存空间
instance = memory; //3.设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成!
instance(memory); //2.初始化对象

如果mInstace没有加volatile标记,当一条线程访问mInstace不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。


5.3 开销较低的“读-写锁”策略

使用 synchronized 确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能.

public class CheesyCounter { private volatile int value; //读操作,没有synchronized,提高性能 public int getValue() { return value; } //写操作,必须synchronized。因为x++不是原子操作 public synchronized int increment() { return value++; }

5.4 独立观察(independent observation)

public class UserManager { public volatile String lastUser;public boolean authenticate(String user, String password) { boolean valid = passwordIsValid(user, password); if (valid) { User u = new User(); activeUsers.add(u); //赋值操作,不涉及运算操作lastUser = user; } return valid; }
}

5.5 “volatile bean” 模式

volatile bean 模式的基本原理是:用volatile修饰易变数据容器,该容器中所有的数据成员都是volatile类型(并且 getter 和 setter 方法必须非常普通),放入这些容器中的对象必须是线程安全的。


推荐阅读
  • 深入理解Java虚拟机的并发编程与性能优化
    本文主要介绍了Java内存模型与线程的相关概念,探讨了并发编程在服务端应用中的重要性。同时,介绍了Java语言和虚拟机提供的工具,帮助开发人员处理并发方面的问题,提高程序的并发能力和性能优化。文章指出,充分利用计算机处理器的能力和协调线程之间的并发操作是提高服务端程序性能的关键。 ... [详细]
  • 关于CMS收集器的知识介绍和优缺点分析
    本文介绍了CMS收集器的概念、运行过程和优缺点,并解释了垃圾回收器的作用和实践。CMS收集器是一种基于标记-清除算法的垃圾回收器,适用于互联网站和B/S系统等对响应速度和停顿时间有较高要求的应用。同时,还提供了其他垃圾回收器的参考资料。 ... [详细]
  • HashMap的相关问题及其底层数据结构和操作流程
    本文介绍了关于HashMap的相关问题,包括其底层数据结构、JDK1.7和JDK1.8的差异、红黑树的使用、扩容和树化的条件、退化为链表的情况、索引的计算方法、hashcode和hash()方法的作用、数组容量的选择、Put方法的流程以及并发问题下的操作。文章还提到了扩容死链和数据错乱的问题,并探讨了key的设计要求。对于对Java面试中的HashMap问题感兴趣的读者,本文将为您提供一些有用的技术和经验。 ... [详细]
  • Tomcat/Jetty为何选择扩展线程池而不是使用JDK原生线程池?
    本文探讨了Tomcat和Jetty选择扩展线程池而不是使用JDK原生线程池的原因。通过比较IO密集型任务和CPU密集型任务的特点,解释了为何Tomcat和Jetty需要扩展线程池来提高并发度和任务处理速度。同时,介绍了JDK原生线程池的工作流程。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 本文介绍了操作系统的定义和功能,包括操作系统的本质、用户界面以及系统调用的分类。同时还介绍了进程和线程的区别,包括进程和线程的定义和作用。 ... [详细]
  • java drools5_Java Drools5.1 规则流基础【示例】(中)
    五、规则文件及规则流EduInfoRule.drl:packagemyrules;importsample.Employ;ruleBachelorruleflow-group ... [详细]
  • Servlet多用户登录时HttpSession会话信息覆盖问题的解决方案
    本文讨论了在Servlet多用户登录时可能出现的HttpSession会话信息覆盖问题,并提供了解决方案。通过分析JSESSIONID的作用机制和编码方式,我们可以得出每个HttpSession对象都是通过客户端发送的唯一JSESSIONID来识别的,因此无需担心会话信息被覆盖的问题。需要注意的是,本文讨论的是多个客户端级别上的多用户登录,而非同一个浏览器级别上的多用户登录。 ... [详细]
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • 本文介绍了Java高并发程序设计中线程安全的概念与synchronized关键字的使用。通过一个计数器的例子,演示了多线程同时对变量进行累加操作时可能出现的问题。最终值会小于预期的原因是因为两个线程同时对变量进行写入时,其中一个线程的结果会覆盖另一个线程的结果。为了解决这个问题,可以使用synchronized关键字来保证线程安全。 ... [详细]
  • 猜字母游戏
    猜字母游戏猜字母游戏——设计数据结构猜字母游戏——设计程序结构猜字母游戏——实现字母生成方法猜字母游戏——实现字母检测方法猜字母游戏——实现主方法1猜字母游戏——设计数据结构1.1 ... [详细]
  • 网络请求模块选择——axios框架的基本使用和封装
    本文介绍了选择网络请求模块axios的原因,以及axios框架的基本使用和封装方法。包括发送并发请求的演示,全局配置的设置,创建axios实例的方法,拦截器的使用,以及如何封装和请求响应劫持等内容。 ... [详细]
  • Java 11相对于Java 8,OptaPlanner性能提升有多大?
    本文通过基准测试比较了Java 11和Java 8对OptaPlanner的性能提升。测试结果表明,在相同的硬件环境下,Java 11相对于Java 8在垃圾回收方面表现更好,从而提升了OptaPlanner的性能。 ... [详细]
  • 本文整理了Java面试中常见的问题及相关概念的解析,包括HashMap中为什么重写equals还要重写hashcode、map的分类和常见情况、final关键字的用法、Synchronized和lock的区别、volatile的介绍、Syncronized锁的作用、构造函数和构造函数重载的概念、方法覆盖和方法重载的区别、反射获取和设置对象私有字段的值的方法、通过反射创建对象的方式以及内部类的详解。 ... [详细]
  • JavaScript简介及语言特点
    本文介绍了JavaScript的起源和发展历程,以及其在前端验证和服务器端开发中的应用。同时,还介绍了ECMAScript标准、DOM对象和BOM对象的作用及特点。最后,对JavaScript作为解释型语言和编译型语言的区别进行了说明。 ... [详细]
author-avatar
书友80433968_667
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有