<
Java内存模型与线程
>
上一篇

Synchronized,ReentrantLock
下一篇

Android如何捕获异常

Java内存模型与线程

20点18分

一、Java内存模型

Java内存模型,描述的是:多线程并发,CPU缓存等方面的内容。

大多数情况下,我们会这样理解:

在每一个线程中,都会有一块内部的工作内存 (working memory)。这块工作内存保存了主内存共享数据的拷贝副本。我们了解到JVM内存结构中有一块线程独享的内存空间——虚拟机栈,所以可能会将线程工作内存理解为虚拟机栈。

实际上,这是不准确的!!!

虚拟机栈和线程的工作内存并不是一个概念。在Java线程中并不存在所谓的工作内存 ,它只是对CPU寄存器和高速缓存的抽象描述

二、CPU的调度

我们都知道线程是CPU调度的最小单位,线程中的字节码指令最终都是在CPU中执行的。所以CPU在执行过程中需要和各种数据打交道,而Java中所有数据都是存放在主内存(RAM) 中的。每次CPU都要直接操作主内存。

随着CPU执行速度越来越快,但内存的技术无太大改变,所以在内存中读写的速度和CPU执行的速度,差别越来越大。所以为了压榨CPU性能,达到高并发,在CPU中添加了高速缓存(cache)来作为缓冲

在执行任务时,CPU会先将运算所需要使用到的数据复制到高速缓存 中,让运算能快速进行,当运算完成之后 ** ,再 **将缓存中的结果刷回(flush back)主内存 ,这样CPU就不用等待主内存的读写。

但同时会产生一个问题,每个处理器都有自己的高速缓存,同时又共同操作同一块主内存 ,当多个处理器同时操作主内存时,可能导致数据不一致,这就是缓存一致性问题

三、缓存一致性问题

现在的设备通常有多个CPU,其中一些CPU还有多核,每个CPU在某一时刻都能运行一个线程,意味着,如果程序有多线程,那么就可能存在多个线程在同一时刻被不同的CPU执行 。并且可能会因为某些线程执行完但并没有将结果刷回主内存。所以会导致多种结果,这就是缓存一致性问题。

四、指令重排

除了缓存一致性问题,还存在另一种硬件问题:为了使CPU内部的运算单元能够尽量被充分利用,处理器可能会对输入的字节码指令进行重排序处理,也就是处理器优化。除了CPU外,一些编程语言也会有类似的优化,如JVM的即时编译器也会做指令重排。

也即是说在CPU层面,有时代码并不会严格按照Java文件中的顺序执行。如果我们仍由CPU优化或编译器指令重排,那我们编写的Java代码最终执行效果可能极大的出乎意料。为了解决这个问题,让Java代码在不同硬件,不同操作系统中,输出的结果达到一致,Java虚拟机规范提出了一套机制——Java内存模型

五、什么是内存模型

内存模型是一套 共享内存系统中 多线程读写操作行为的规范 ,这套规范屏蔽了底层各种硬件和操作系统的内存访问差异,解决了CPU多级缓存,CPU优化,指令重排等导致的内存访问问题,从而保证Java程序(尤其多线程程序)在各种平台下对内存的访问效果一致

在Java内存模型中,统一用工作内存(working memory)来当作CPU寄存器或高速缓存的抽象。线程之间的共享变量存储在主内存 (main memory)中,每个线程都有一个私有工作内存 (类比CPU寄存器或高速缓存),本地工作内存中存储了该线程读/写 共享变量的副本

这套规范有一个非常重要的规则——happens-before。

六、happens-before 先行发生原则

happens-before用于描述两个操作的内存可见性 ,通过保证可见性的机制,可以让程序免于数据竞争干扰 。定义如下:

如果一个操作A happens-before 另一个操作B,那么操作A的执行结果对操作B可见。

反过来同样:如果操作A的结果需要对另一个操作B可见,那么操作A必须happens-before 操作B。

那在Java中的两个操作如何就算符合happens-before规则呢?JMM中定义了一下集中情况,是自动符合happens-before规则的。

  1. 程序次序规则:

在单线程内部,如果一段代码的字节码顺序也隐式符合happens-before原则,那么逻辑顺序靠前的字节码执行结果,一定对后续逻辑字节码可见。

  1. 锁定规则

无论在单线程环境还是多线程环境,一个锁如果处于被锁定状态,那么必须先执行unlock操作后,才能进行lock操作。

  1. 线程启动规则

Thread对象的star()方法先行发生于此线程的每一个动作。假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改,在线程B开始执行后,确保对线程B可见。

  1. 线程中断规则

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

  1. 线程终结规则

线程中所有的操作都发生在线程的终结检测之前,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值等方法检测线程是否终止执行。假定线程A在执行过程中,通过调用TreadB.join()等待线程B终止,那么线程B在终止之前对共享变量的修改在线程A等待返回后可见。

  1. 对象终结规则

一个对象的初始化完成发生在它的finalize()方法开始前。

此外happens-before原则还具有传递性。

七、Java内存模型

上述的happens-before很重要,我们是根据此规则来判断数据是否存在竞争,线程是否安全。在此基础上,我们可以通过Java提供的一系列关键字,将我们自己实现的多线程操作 “happens-before 化。”

“happens-before 化”就是将本来不符合happens-before原则的操作,通过某种手段使其符合happens-before原则。

  1. 使用volatile修饰变量 private volatile int value = 0;

  2. 使用synchronized修饰操作

    1. public int getValue(){
          synchronized{
              return value;
          }
      }
      

通过上述两种操作,都可以使其符合happens-before原则——当在某一线程中调用方法时,再在其他线程中调用,数据一定是具有可见性的。

Top
Foot