多线程基础(一)— Java 内存模型

  为了更好的理解 Java 内层模型,我们需要简单地将 CPU 缓存模型回忆一下。

CPU Cache 缓存

  在计算机中,虽然 CPU 的计算速度很快,但是计算机中绝大多数的任务不能只靠 CPU 的计算就能完成。还需要包括与内层的数据交互,读写、存储元算结果等。但是由于计算机的存储设备和 CPU 的运算速度有着几个数量级的差距,现代计算机中一般都会在内存设备和 CPU 之间添加一层读写速度尽可能接近 CPU 处理速度的高速缓存,用于将运算需要的数据复制到缓存中,让运算能够快速进行,当运算结束后再从缓存刷新到主内存中,这样 CPU 就无需等待缓慢的内层读写了。

  虽然高速缓存的存储交互很好的解决了处理器与内存的速度矛盾,但它也引入了新的矛盾:内存一致性问题。当多个处理器的运算任务都设及同一块主内存区域时,就会导致内存一致性问题,为了解决这个问题,各个处理器访问缓存的时候都需要遵循一些规则,来保持数据的一致性。这些规则被专家们命名为缓存一致性协议。其中最为出名的是 Intel 的 MESI 协议。MESI 协议保证了每一个缓存中使用的共享变量副本保持一致。大致思想是:当 CPU 在操作缓存中的数据时,如果发现该变量是一个共享变量,也就是说在其他的缓存中也存在该变量的副本,那么就要进行如下操作:

    1)读取操作,不做任何处理,可直接将缓存中的数据读取到 CPU 中的寄存器;

    2)写入操作,发出信号通知其他缓存,该变量的副本已经作废,如果要进行变量读取,必须到主内存中读取新值。

  除了增加高速缓存外,为了使处理器内部的运算单元能够尽量被充分利用,处理器可能会将输入代码进行乱序执行实现优化,同时保证整体代码的计算结果与代码按照逻辑顺序执行的结果是一致的。与 CPU 的乱序执行优化类似,Java 虚拟机的即时编译器也有类似的指令重排序(Instruction Reorder)优化。

Java 内存模型

  Java 内存模型(Java Memory Model, JMM)定义了 Java 虚拟机与计算机主内存进行工作的交互细节。即在 JVM 中将变量存储到内存与从内存中取出变量这样的底层细节。此处的变量包括了实例字段、静态字段和数组对象,也就是可以被共享的数据。不包括局部变量和方法参数,因为他们是线程私有的,不会被共享,也就不会存在竞争问题。JMM 定义了线程和主内存之间的抽象关系,具体如下:

  1)共享变量存储与主内存中,每个线程都可以访问,这里的主内存可以类比CPU 缓存模型中的的主内存,但此处只是虚拟机内存的一部分。

  2)每个线程都有自己的私有内存,称之为本地内存,可以类比 CPU 缓存模型中的高速缓存。

  3)工作内存只能存储该线程的共享变量的副本。

  4)线程对变量的所有操作(读取、写入)都必须在工作内存中进行,不能直接操作主内存中的变量。

  5) 不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

  了解了 Java 内存模型可以为我们以后学习多线程、valatile 关键字打下基础。

 

  在并发编程中,有三个至关重要的特性,分别是原子性、可见性和有序性。我们来看一下JMM如何保证这三大特性的。

JMM 与 原子性

  原子性的意思是对于以一个操作所包含的所有步骤,要么全部执行,要么全都不执行。这个概念可以类比关系数据库中事物的一致性。

在 Java 中,对基本数据类型和引用类型的变量进行读取、赋值操作是原子性的。但是几个具有原子性的操作组合在一起,就不一定是原子性的了。比如下面这个常见的自增操作: 

y++

  这条语句其实由3个操作组合而来:

  1)执行线程从主内存中读取 y 值(如果 y 的副本已经存在于执行线程的工作内存中,则直接获取),然后将其副本存入当前线程的工作内存中;

  2)在执行线程中为 y 执行加一操作;

  3)将 y 的值刷新进主内存。

  JMM 只保证了基本的读取与赋值操作的原子性,其他的均不保证,如果想要某些代码获得原子性,可以使用在后面会学到的 synchronized 关键字,或者 JUC 中的 lock。

 

JMM 与 可见性

  可见性是指一个程序对一个共享变量进行了修改,其他线程能立即得知这个修改。JMM 通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷入变量新值,这种依赖主内存作为传递媒介的方式来实现可见性的。Java 中提供了很多保证有序性的方式:

  1)使用 volatile 关键字,当一个变量被 volatile 修饰时,对于共享资源的读操作会直接在主内存中进行(当然也会刷新到工作内存),对于共享资源的写操作会首先修改工作内存,然后将值立马刷新到主内存中。

  2)使用 synchronized 关键字,synchronized 关键字保证在同一时间只有一个线程获得锁,然后执行同步方法,对共享变量进行操作。并且会确保在锁释放之前,会将对变量的修改刷新到主内存中。

  3)使用 JUC 提供的显式锁lock,作用和 synchronized 关键字相似,但与synchronized 关键字相比,更加轻量级。

 

JMM 与 有序性

  在 JMM 中,允许编译器和 CPU 对指令进行重排序,也就是在 CPU 缓存中讲的乱序执行优化和指令重排序优化。在单线程的情况下,随便你们想怎么优化,都没问题,但对于多线程,指令重排可能会引起错误。JMM 天生具备一些有序性规则,不需要任何显示的同步手段就可以保证有序性(其实我相信底层也是用的同步手段),这个规则就是 Happens-before 原则,比如

  程序的启动规则:Tread 的 start 方法先于该线程的任何动作,只有线程 start 之后才能真正运行,否则仅仅就是一个对象。

  对象的终结规则:一个对象初始化的完成先行发生于 fanalize 方法前。

  锁定规则:一个 unlock 操作要先行发生于对同一个锁的 lock 操作。

  传递规则:Happens-before 原则具有传递性。

如果一个操作无法从 Happens-before 原则推导出来,那么就无法保证有序性。

  与保证可见性一样,也可以使用 volatile 关键字、synchronized 关键字、JUC 提供的显式锁lock来保证有序性。其中 synchronized 保证有序性是利用“同一时间只有一个线程获得锁,然后执行同步方法,对共享变量进行操作”的这一特点来保证有序性;而 volatile 则直接暴力的禁止编译器和 CPU 的任何指令重排序,因此保证了有序性。

 

  在了解了 JMM 模型之后可以更方便学习多线程知识,特别是在后面学习 volatile 关键字的时候就会显得得心应手。

 

猜你喜欢

转载自www.cnblogs.com/dogeLife/p/11391863.html