天天看点

深入理解Java虚拟机之(2)Java内存模型与线程

     处理器需要与内存交互,如 读取运算数据、存储运算结果等,这个 I/O 操作是很难消除的(无法仅靠寄存器来完成所有的运算任务)。由于计算机的存储设备 与 处理器 的运算速度有几个数量级的差距,所以现代计算机系统不得不加入一层 读写速度 尽可能接近 处理器运算速度 的高速缓存 (Cache) 来作为 内存与处理器之间的缓冲 :将运算需要使用到的数据复制到缓存中,让运算速度能快速进行,当运算结束后,再从缓存同步回内存中,这样处理器就无须等待缓慢的内存读写了。

     基于 高速缓存 的存储交互 很好地解决了 处理器与内存的速度矛盾,但是也为计算机系统带来更高的复杂度,因为 它引入了一个 新的问题:缓存一致性(Cache Coherence)。在多处理系统中,每个处理器都有自己的高速缓存,而它们又共享同一个主内存,当多个处理器的运算任务都涉及到同一块 主内存区域时,将可能导致 各自的缓存数据不一致,如果发生这样的情况,那 同步到主内存时 以谁的缓存数据为准呢? 为例解决一致性问题,需要各个处理器访问缓存时都遵守一些协议,在读写时要根据协议进行操作,协议有 MSI、Synapse、Firefly、Dragon Protocol等。

深入理解Java虚拟机之(2)Java内存模型与线程

CPU的内存模型

【 内存模型:在特定的操作协议下对特定的内存或高速缓存进行读写访问的过程抽象】

     工作内存是各个CPU共享的一个区域,像堆、方法区都在这里,而程序计数器、线程私有的都是在CPU各自的工作内存中的,主内存是处理数据的地方。

深入理解Java虚拟机之(2)Java内存模型与线程

Java内存模型

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

     Java 内存模型的主要目标是 定义程序中各个变量的访问规则,即 在虚拟机中 将变量存储到内存 和 从内存中取出变量这样的底层细节。此处的 “变量“ 与 Java 编程中所说的 变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但不包括 局部变量和方法参数 ,因为后者是线程私有的,不会被共享,也就不存在竞争问题。为了获得更好的执行效能,Java 内存模型并没有限制 执行引擎使用处理器的特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器进行调整代码执行顺序这类优化措施。

     当线程对变量进行写入操作时,先会把值写入该线程的一级缓存中,然后再刷新到二级缓存中,而读取值是先到自己的一级缓存中进行读取,如果没有找到该变量,才去二级缓存中。

深入理解Java虚拟机之(2)Java内存模型与线程

♥     Java 内存模型规定了 所有的变量都存储在主内存 (Main Memory)中,每条线程还有自己的工作内存。线程的工作内存中 保存了 被该线程使用到的变量的 主内存副本拷贝,线程对变量的所有操作(读取、赋值) 等,都必须在工作内存中进行,而不能直接读写 主内存 中的变量。不同的线程之间也无法之间直接访问 对方工作内存 的变量,线程间变量值的传递 均需要通过主内存 来完成。

♥线程、主内存、工作内存三者的交互关系如下:

深入理解Java虚拟机之(2)Java内存模型与线程

     主内存主要对应 Java 堆中的对象实例数据部分; 工作内存则对应虚拟机栈中的部分区域。

8 个原子性操作

深入理解Java虚拟机之(2)Java内存模型与线程
  • use使用:

         作用于 工作内存的变量,它把工作内存中一个变量的值 传递给 执行引擎,每当 虚拟机 遇到一个 需要使用到变量的值的 字节码指令时将会执行这个操作。

  • load 载入:

         作用于工作内存的变量,它把 read 操作 从主内存 中得到的变量值放入工作内存的变量副本中。

  • lock锁定:

         作用于主内存的变量,它把一个变量标识为一条线程独占的状态。

         如果对一个变量执行 lock 操作,那将会清空 工作内存中 此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign 操作初始化此变量的值。

  • unlock解锁:

        作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

  • assign赋值:

         把一个从执行引擎接受到的值赋给工作内存的变量。

(每个字节码都是由 1个 操作码 和 附加的操作数 组成。)

  • store存储:

          作用于工作变量,它把工作内存中的一个变量的值 传送到主内存中,以便随后的 write 使用。

     如果要把一个变量 从 主内存 复制到 工作内存,那就要 顺序地执行 read 和 load 操作;

     如果要把变量 从 工作内存 同步回 主内存 ,就要顺序执行 store 和 write 操作。

     只保证顺序,不保证连续。

volitile变量

     Java 内存模型允许编译器和处理器对指令重排序以提高运行性能,并且只会对不存在数据依赖性的指令重排序。单线程下重排序可以保证最终执行的结果与程序顺序执行的结果一致,但在多线程下就会存在问题。

     volatile 关键字可以说是 Java虚拟机提供的最轻量级的同步机制。volatile 用在共享变量前,可以避免指令重排序,(它的语义:♥在读取变量之后的命令不能排在读取前面;在写变量之前的命令不能排在写后面。♥

    volatile 能保证内存可见性,但不能保证 线程安全。因为内存可见性是对于变量来说,该变量的读 与 写 操作受影响,而 Java 并不是每条语句都是是原子性的。比如:

    volatile 修饰的变量 i 初值为 0,进行自增操作 i++ ,这一条Java 语句,它是有 3 句指令的:

(1)读取 i 的值 。

(2)求 i +1 的值

(3)将 i+1 的值赋给 i。

        比如线程 A 先执行(1),进行 读 的操作,是直接从主内存中取值,这时 i=0,接下来切换到线程 B ,线程 B 先执行 (1),进行 读 的操作,从主内存中取值,i=0,然后执行(2),i+1 的值为 1,接下来执行(3),会将1直接刷新到主内存中,不会存在任何缓存中。切换到线程 A 中,这时已经执行过(1)读取操作了,该执行(2)了,I+1的值仍为 1 ,接下来执行(3),会将 1 刷新到主内存中,而经过这俩线程,自增操作的结果 是 ”1“,而不是预期中的 2。

     除了 volatile 可以保证内存可见性,synchronized、final也可以。

     volatile 变量 读操作的性能消耗 与普通变量几乎没有什么差别,但是 写操作则可能会慢一点,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

     对于 volatile变量V,是这样保证内存可见性的:(结合上面一个图记忆)

     读 —— 线程 T 对变量 V 的 use 操作可以认为是 和线程T 对变量 V 的 load、read 动作关联,必须连续一起出现。(这条规则要求在工作内存中,每次使用前都必须先从 主内存中刷新最新的值,用于保证能看见其他线程对变量 V 所作的修改后的值)。

     写 —— 线程 T 对变量 V 的 assign 操作可以认为是 和线程T 对变量 V 的 store、write 动作关联,必须连续一起出现。(这条规则要求在工作内存中,每次修改 V 后都必须立刻同步回主内存中,用于保证其他线程可以看到自己对变量 V 的修改。

     除了 volatile 以外,Java 还有两个关键字能实现可见性,synchronized 和 final 。同步块的可见性 是由 :对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存中(执行 store、write 操作); 而被 final 修饰的字段在构造器中一旦初始化完成,并且构造器没有把 “this” 引用传递出去,那在其他线程中,就能看见 final 字段的值。

     即使编译出来值只有一条字节码指令,也并不意味着这条指令就是一个原子操作。

先行性原则

     ♥先行发生原则:

     是判断数据是否存在竞争、线程是否安全的主要依据。

     先行发生是指: Java内存模型中 定义的两项操作之间的偏序关系,如果说 操作 A 先行发生于 操作 B ,或者说 发生在操作 B 之前,操作 A 产生的影响 能被操作 B 观察到,“ 影响 ”包括 修改了内存中共享变量的值、发送了信息、调用了方法等。

先行发生规则

     如果两个操作之间的关系不在此列,并且无法从下列规则中推导出来的话,它们就没有顺序性保证,虚拟机可以对它们随意进行重排序:

(无须任何同步手段保障 就能成立 的先行发生规则)

  • 程序次序规则:

         在一个线程内,按照程序代码顺序,书写在前面的操作 先行发生于 书写在后面的操作。

  • 管程锁定规则(Monitor Lock Rule):

         一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。

  • volatile 变量规则:

         对 volatile 变量的写操作先行发生于读操作。

  • 线程启动规则:

         Thread 对象的 start() 方法先行发生于 此线程的每一个动作。

  • 线程终止规则:

         线程的所有操作都先行发生于对此线程的终止检测(join、isAlive 方法等)。

  • 线程中断规则:

         对线程 interrupt() 方法调用 先行发生于被中断的代码检测到中断事件的发生。

  • 对象终结原则

         一个对象的初始化完成(构造函数执行结束)先行于它的 finalize() 方法的开始。

  • 传递性:

         如果操作 A 先行发生于 操作 B ,操作 B 先行发生于 操作 C ,可得 操作 A 先行发生于 操作 C。

         时间先后顺序 与 先行发生原则 之间 基本没有太多的关系。

线程的 5 种状态

(1)new ,使用 new 创建对象线程后。

(2)runnable,一start() ,线程就进入就绪状态,准备好了除 CPU 时间片以外的资源。

(3)等待分为untimed wait 不限时等待 和 timed wait 限时等待。

(4)blocked,线程被阻塞,当要进入同步块,但未持有资源对象的监视器锁时。

(5)terminal:终止,线程的run() 方法正常结束而退出或遇到异常。

状态转换图:

深入理解Java虚拟机之(2)Java内存模型与线程

     衡量服务 性能的高低好坏, 每秒事务处理数(Transactions Per Second,TPS) 是最重要的指标之一,它代表着一秒内 服务端平均能响应的请求总数。