在介绍 Java 内存模型之前, 先来了解一下为什么要有内存模型, 以及内存模型是什么. 然后我们基于对内存模型的了解, 学习 Java 内存模型以及并发编程的三大特性.
为什么要有内存模型
在计算机中, 所有的运算操作都是由 CPU 的寄存器来完成的, CPU 指令的执行需要涉及到数据的读写操作, 而 CPU 只能访问主存中的数据. 随着技术的发展, CPU 的执行速度越来越快, 而内存的访问速度没有太大的变化, 导致 CU 每次操作主存都要等待很长的时间. 于是就有了在 CPU 与主存之间添加缓存的设计.
内存模型: CPU Cache 模型
目前缓存的数量达到了3级, 最接近 CPU 的缓存称为 L1, 然后为L2,L3和主存. 由于程序指令和数据的行为和热点分布差异比较大, 因此将 L1又细分为 L1i(istruction),L1d(data).
CPU 的出现是为了解决 CPU 直接访问主存效率低下的问题. 程序在运行过程中, 会将运算所需的数据从主存中复制一份到 Cache 中, 这样 CPU 在计算时就可以直接对 CPU Cache 中的数据进行读写, 当运算结束后, 再将 Cahce 中的最新数据刷新到主存中.
虽然缓存的出现极大地提升了 CPU 的吞吐能力, 但是也导致了缓存不一致的问题. 这是因为 CPU 都是对 Cache 中的数据进行读写, 不同线程之间的工作内存是相互独立的, 对某个线程工作空间中的数据进行更新, 可能会无法及时同步到其它缓存中.
为了保证数据的正确性, 内存模型定义了共享内存系统中多线程程序读写操作行为的规范.
Java 内存模型
Java 内存模型 (Java Memory Model ), 简称 JMM, 是一种符合内存模型规范的, 屏蔽了各种硬件和操作系统的访问差异的, 保证了 Java 程序在各种平台下对内存的访问都能得到一致效果的机制及规范. 其目的是解决多线程通过主内存进行通信时, 存在的原子性, 可见性(缓存一致性) 以及有序性问题.(关于原子性, 可见性 (缓存一致性) 以及有序性, 我们将会在 "并发编程的三大特性" 中详细讲解)
JMM 决定了一个线程对共享变量的写入何时对其它线程可见, 定义了线程与主存之间的关系:
共享变量存储于主存中, 每个线程都可以访问
每个线程都有私有的工作内存, 也称为本地内存
工作内存中只存储共享变量的副本
线程不能直接操作主存, 只有操作了本地内存后才能写入主存
每一个线程都不能访问其他线程的本地内存
并发编程的三大特性
并发编程有三大特性: 原子性, 可见性, 有序性.
原子性: 是指在一次操作或多次操作中, 要么所有的操作都得到执行, 要么都不执行.[类似于事务]
JMM 只保证了基本读取和赋值的原子性操作
多个原子性操作的组合不再是原子性操作
可以使用 synchronized/lock 保证某些代码片段的原子性
对于 int 等类型的自增操作, 可以通过 java.util.concurrent.atomic.* 保证原子性
可见性: 是指一个线程对共享变量进行了修改, 其他线程可以立即看到修改后的值.
有序性: 是指代码在执行过程中的先后顺序是有序的.[Java 编译器会对代码进行优化, 执行顺序可能与开发者编写的顺序不同(指令重排)]
并发编程时, 保证三大特性的方式有三种:
使用 volatile 关键字修饰变量
当一个变量被 volatile 关键字修饰时, 对于共享变量的读操作会直接在主存中进行, 对于共享变量的写操作是先修改本地内存, 修改结束后直接刷到主存中.(未被 volatile 修饰的变量被修改后, 什么时候最新值会被刷到主存中是不确定的)
使用 synchronized 关键字修饰方法或代码块
synchronized 关键字能保证同一时刻只有一个线程获得锁然后执行同步方法, 并且确保锁释放之前, 会将修改的变量刷入主存.
使用 JUC 提供的显式锁 Lock
Lock 能保证同一时刻只有一个线程获得锁然后执行同步方法, 并且确保锁释放之前, 会将修改的变量刷入主存.
补充
Java 中提供了一系列和并发处理相关的关键字, 比如 volatile,synchronized 等, 其实这些就是 Java 内存模型封装了底层的实现后提供给程序员使用的一些关键字.
参考文献
汪文君《Java 高并发编程详解 - 多线程与架构设计》
来源: https://www.cnblogs.com/BlueStarWei/p/11278621.html