作业 面向对象原理

面向对象原理

关于java内存模型与生命周期以及JVM相关知识点

一、Java内存模型产生的背景

物理机遇到的并发问题与虚拟机中的情况有不少相似之处,因此物理机的解决方案对虚拟机的实现有相当的参考意义。

1.物理机的并发问题
1.1硬件的效率问题

计算机在处理事件时,不可能全部依靠CPU完成,处理器至少需要和内存进行“交流”,比如告诉内存:
“内存老弟,给我来点数据”(读取数据)或者是“老弟,我放点东西在你这”(存放数据)

计算机存储设备读写速度和CPU的运算速度有几个数量级的差距,为了避免处理器等待缓慢的内存完成读写操作,现代计算机系统通过加入一层读写速度尽可能接近处理器运算速度的高速缓存。

缓存就是为了解决高速CPU对慢速内存的存取。缓存作为内存和处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速运行,当运算结束后再从缓存同步回内存之中。

1.2缓存的一致性问题

基于高速缓存的存储交互解决了处理器与内存速度不匹配问题。但是计算机的处理器很少只配备一个核心,随着多核CPU的出现,为了让每一个处理器都可以与内存进行交互,那么就要为每一个处理器都要配备缓存,而这些高速缓存共享主内存。

那么当多个处理器同时处理一块相同区域的任务时,有可能出现各自缓存的数据不一致的情况。为此,需要各个处理器访问缓存时都遵循协议,在读写时要根据协议进行操作,来维护缓存的一致性。

1.3代码执行的优化

当我们看到杂乱的代码,我们也会感到烦躁,那么对于处理器来说也是一样。于是为了使得处理器内部的运算单元尽量被充分利用,提高运算效率,处理器可能会对输入的代码进行乱序执行。

处理器会在计算之后将乱序执行的结果重组,乱序优化可以保证在单线程下该执行结果与顺序执行的结果是一致的,但不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致。

int a;
int b;

int result=a*b;

int result=b*a;

简单来说,可以将处理器看作一个强迫症患者,在上述代码中,输入相同的a,b的数据,我们知道对于result来说并没有什么改变,因为结果都是一样的,在我们看来只不过说ab相乘的顺序不同,但处理器说我不要,它一定要让a先乘b

对于单核处理器,处理器保证做出的优化不会导致执行结果远离预期目标,但在多核环境下却并非如此。在多核环境下, 如果存在一个核的计算任务依赖另一个核计算任务的中间结果,而且对相关数据读写没做任何防护措施,那么其顺序性并不能靠代码的先后顺序来保证,处理器最终得出的结果和我们逻辑得到的结果可能会大不相同。

int a=1;
int b=2;
boolean flag=true;

if(a*b==2)flag=false;
if(flag=true)System.out.println("a");

可以看到如果调换最后两句的顺序,那么代码的结果就会不同

二、Java 内存模型的组成

2.1Java内存模型的概念

内存模型可以理解为在特定操作协议下,对特定的内存或高速缓存进行读写访问的抽象过程。

Java 虚拟机规范中试图定义一种 Java 内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果,不必因为不同平台上的物理机的内存模型的差异,对各平台定制化开发程序。

Java 内存模型提出目标在于,定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节

2.2Java内存模型的组成
2.2.1主内存

Java 内存模型规定了所有变量都存储在主内存(Main Memory)中

2.2.2工作内存

每条线程都有自己的工作内存(Working Memory,又称本地内存,有点类似于之前提到的高速缓存),线程的工作内存中保存了该线程使用到的变量的主内存中的共享变量的副本拷贝。

工作内存是 JMM 的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
在这里插入图片描述

2.3JVM内存操作的并发问题
2.3.1工作内存数据一致性

各个线程操作数据时会保存使用到的主内存中的共享变量副本,当多个线程的运算任务都涉及同一个共享变量时,将导致各自的共享变量副本不一致,如果真的发生这种情况,数据同步回主内存以谁的副本数据为准?

Java 内存模型主要通过一系列的数据同步协议、规则来保证数据的一致性

2.3.2.指令重排序优化

Java 中重排序通常是编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。

重排序分为两类:编译期重排序和运行期重排序,分别对应编译时和运行时环境。

同样的,指令重排序不是随意重排序,它需要满足以下两个条件:

  • 在单线程环境下不能改变程序运行的结果。即时编译器(和处理器)需要保证程序能够遵守 as-if-serial 属性。
    通俗地说,就是在单线程情况下,要给程序一个顺序执行的假象。即经过重排序的执行结果要与顺序执行的结果保持一致。
  • 存在数据依赖关系的不允许重排序。

多线程环境下,如果线程处理逻辑之间存在依赖关系,有可能因为指令重排序导致运行结果与预期不同,后面再展开 Java 内存模型如何解决这种情况。

2.4Java内存的基本操作

8 种基本操作:

  • lock (锁定) ,作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
  • unlock (解锁) ,作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read (读取) ,作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用。
  • load (载入) ,作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use (使用) ,作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时就会执行这个操作。
  • assign (赋值) ,作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store (存储) ,作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后 write 操作使用。
  • write (写入) ,作用于主内存的变量,它把 Store 操作从工作内存中得到的变量的值放入主内存的变量中。

三、Java内存模型运行规则

Java 内存模型的一系列运行规则是围绕原子性、可见性、有序性特征建立。

原子性(Atomicity)

原子性,即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。

可见性(Visibility)

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

正如上面“交互操作流程”中所说明的一样,JMM 是通过在线程 1 变量工作内存修改后将新值同步回主内存,线程 2 在变量读取前从主内存刷新变量值,这种依赖主内存作为传递媒介的方式来实现可见性。

有序性(Ordering)

有序性规则表现在以下两种场景:

  • 线程内,从某个线程的角度看方法的执行,指令会按照一种叫“串行”(as-if-serial)的方式执行,此种方式已经应用于顺序编程语言。

  • 线程间,这个线程“观察”到其他线程并发地执行非同步的代码时,由于指令重排序优化,任何代码都有可能交叉执行。
    唯一起作用的约束是:对于同步方法,同步块(synchronized 关键字修饰)以及 volatile 字段的操作仍维持相对有序。

    为了方便程序开发,Java 内存模型实现了下述支持 happens-before 关系的操作:

    • 锁定规则,一个 unLock 操作 happens-before 后面对同一个锁的 lock 操作。
    • volatile 变量规则,对一个变量的写操作 happens-before 后面对这个变量的读操作。
    • 传递规则,如果操作 A happens-before 操作 B,而操作 B 又 happens-before 操作 C,则可以得出操作 A happens-before 操作 C。
    • 线程启动规则,Thread 对象的 start() 方法 happens-before 此线程的每个一个动作。
    • 线程中断规则,对线程 interrupt() 方法的调用 happens-before 被中断线程的代码检测到中断事件的发生。
    • 线程终结规则,线程中所有的操作都 happens-before 线程的终止检测,我们可以通过 Thread.join() 方法结束、Thread.isAlive() 的返回值手段检测到线程已经终止执行。
    • 对象终结规则,一个对象的初始化完成 happens-before 它的 finalize() 方法的开始。
    volatile 型变量

    volatile 主要有下面 2 种作用:

    • 保证可见性
    • 禁止进行指令重排序

    保证可见性,保证了不同线程对该变量操作的内存可见性。这里保证可见性不等同于 volatile 变量并发操作的安全性,保证可见性具体一点解释:

    • 线程对变量进行修改之后,要立刻回写到主内存。
    • 线程对变量读取的时候,要从主内存中读,而不是从线程的工作内存。

    但是如果多个线程同时把更新后的变量值同时刷新回主内存,可能导致得到的值不是预期结果。

    举个例子:定义 volatile int count = 0,2 个线程同时执行 count++ 操作,每个线程都执行 500 次,最终结果小于 1000。

    原因是每个线程执行 count++ 需要以下 3 个步骤:

    • 线程从主内存读取最新的 count 的值。
    • 执行引擎把 count 值加 1,并赋值给线程工作内存。
    • 线程工作内存把 count 值保存到主内存。

    有可能某一时刻 2 个线程在步骤 1 读取到的值都是 100,执行完步骤 2 得到的值都是 101,最后刷新了 2 次 101 保存到主内存。

    禁止进行指令重排序,具体一点解释,禁止重排序的规则如下:

    • 当程序执行到 volatile 变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行。
    • 在进行指令优化时,不能将在对 volatile 变量访问的语句放在其后面执行,也不能把 volatile 变量后面的语句放到其前面执行。

    普通的变量仅仅会保证该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证赋值操作的顺序与程序代码中的执行顺序一致。

synchronized

通过 synchronized 关键字包住的代码区域,对数据的读写进行控制:

  • 读数据,当线程进入到该区域读取变量信息时,对数据的读取也不能从工作内存读取,只能从内存中读取,保证读到的是最新的值。
  • 写数据,在同步区内对变量的写入操作,在离开同步区时就将当前线程内的数据刷新到内存中,保证更新的数据对其他线程的可见性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值