Java内存模型我们大致会分为两篇来讲,主要解决一下问题:
第6条和第7条我放在下一篇讲,话不多说,一起掀开Java高并发内容的第一章。
1. 为什么需要并发处理
1.1 提升性能
1.1.1 提升CPU的使用效率
目前计算机上会有多个CPU核心,我们可以创建多个线程,操作系统会将线程分配各不同的CPU去执行,如果只有一个线程,那么只会有一个CPU去工作,其他的CPU将会被浪费。
计算机CPU的运算速度相较于I/O等子系统的速度快太多,假设我们使用单线程做一个I/O操作,此时由于I/O操作缓慢导致线程被阻塞,即时有CPU处于空闲状态,I/O操作后面的指令也无法被执行。但是如果使用多线程的话,我们可以使用一个线程去做I/O操作,I/O操作后面的指令可以使用另一个线程交给空闲的CPU去执行。
1.1.2 降低系统响应时间
TPS:每秒事务处理数,它代表着一秒内服务段平均能响应的请求总数。
假设我们的WEB服务器同时有10个用户访问(每个响应大约需要1s),如果使用单线程,后面的9个用户则需要等待,如果一个用户很不幸是第10个,那么他将会需要等待10s以上,用户体验将会非常差。但是使用多线程则可以回避响应时间过长的问题,每个用户都能尽快得到响应。
当然目前WEB服务器除了使用多线程处理以外,还会部署多台,部署多台的好处除了可以提高我们网站的并发量以外,也可以做负载均衡,避免单点故障。
1.2 提升系统的容错能力
一个线程可以不受其他线程的干扰独立运行,如果此线程出现异常退出,其他线程可以不受干扰的继续执行,不至于整个系统崩溃。
2. CPU高速缓存
通过并发我们可以更加充分的利用CPU,CPU的计算通常需要与内存交互,如读取运算数据,存储运算结果,但是这种I/O操作的速度远远比CPU计算的速度要慢的多。为了解决这种硬件效率的不一致,现在的计算机系统都会增加一层高速缓存(读写速度接近于CPU运算速度)来作为内存和CPU之间的缓冲,CPU在工作时会将运算使用到的数据从主内存复制到高速缓存中,利用高速缓存来进行中间结果的保存读取,CPU在计算结束后再把高速缓存的结果同步回主内存,这样就可避免缓慢的主内存读写。
3. 缓存一致性
高速缓存虽然解决了硬件的速度一致性,但是同样也引入了缓存一致性问题。每个处理器都有自己的高速缓存,而这些处理器又共享同一主内存。当多个处理器任务都涉及同一块主内存区域时,将会导致各自的缓存数据不一致。
为了解决一致性问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议进行操作,这类协议有MSI、MESI、Synapse以及FireFly等。
4. 什么是内存模型
内存模型可以理解为在特定的操作协议下,对特定内存或高速缓存进行读写访问的过程抽象。不同架构的物理机器拥有不一样的内存模型,而Java虚拟机也有自己的内存模型。
5. Java内存模型
Java虚拟机在工作时也有自己的主内存,每个线程有自己的工作内存,这就好比CPU高速缓存和物理机主内存一样,工作内存也是为了获得更好的执行速率。
Java内存模型主要围绕以下三个特性展开:
5.1 主内存和工作内存交互协议
主内存和工作内存的交互协议规定了一个变量如何从主内存拷贝到工作内存,如何从工作内存同步回主内存。Java内存模型中规定了8种操作来完成以上过程,且虚拟机在实现这8种操作时必须要保证原子性,所谓的原子性指的是操作不可再分。这8种操作如下:
lock:作用于主内存的变量,把一个变量标识为一条线程独占的状态
unlock:作用于主内存的变量,把一个处于锁定状态的变量释放,以便后续被其他线程锁定
read:作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便后续的load使用
load:作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
use:作用于工作内存的变量,它把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时就会执行这个操作
assign:作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时就执行这个操作
store:作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便后续的write操作使用
write:作用于主内存的变量,它把store操作从工作内存中得到的变量的值写入主内存的变量中
上述指令有以下规定:
read a -> read b -> load b -> load a