您的位置:首页 > 其它

多线程并发艺术(一)原子性、可见性和有序性

2019-04-19 19:55 183 查看

原子性,可见性和有序性

一、原子性 Atomicity
  1. 原子性: 在化学上,原子就是构成一般物质最小的单位,在化学中是不可分割的。在java中,原子性就是操作不可中断的。即使是多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
    原子性是操作原子性,根本上来说只是某个变量的某个操作赋予原子性
《thinking in Java》的第21章的《并发》有写:
“除了long和double类型,Java基本数据类型都是的简单读写都是原子的,而简单读写就是赋值和return语句。”
因此而对于其他自增自减以及其他运算操作,是非原子操作。

基本数据类型的自增自减操不是原子操作,这是为什么呢

比如:i++;
1)读取i的值
2)进行 i 增加1 的操作
3)将i的值新写入内存

这已经是三个步骤了。原子操作可是不能拆分的,这一下子就拆分了三个子操作。所以很明显的不是原子操作。
虚拟机定义了8种原子操作,包括

lock 锁定主内存的变量,使其被某一线程独占
unlock 解锁主内存中的变量
read 把一个主内存的变量传递到工作内存中,以便load
load 将从主内存传递的值传递到工作内存的变量副本中
store 将工作内存中变量副本传递到主内存中去,以便write
write 将工作内存传递过来的值赋到主内存中变量
use 将工作内存的值传递给执行引擎
assign 将执行引擎的值传递到工作内存

这8种操作可以用来确定你的访问是否安全。

这八种操作涉及到了Java内存模型

二、Java内存模型 Java Memory Model
  1. 在jdk1.2之前,java的内存模型实现总是从主内存(共享内容)中读取变量。由于CPU执行速度很快,而从内存中读取数据和向内存中写入数据的过程和CPU执行速度相比慢的太多了。大大降低了指令执行的速度。所以现在的Java内存模型下,线程可以把变量保存在本地内存中,而不是直接在主内存中进行读写。
  2. JMM主要用于屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。如果扩展开来说,它也可以称为内存一致性模型。

如图就是多个线程中共享一块主内存,每个线程中有自己的一块工作内存。线程的工作内存中保存了该线程使用的变量的主内存的副本拷贝,线程对变量的所有操作(读取,赋值等)都在工作内存中进行,而不能直接读写主内存的变量。不同的线程之间无法直接访问对方工作内存中的变量,线程之间值的传递都间接通过主内存来完成

注意:Java内存模型本身是一种抽象的概念,并不真实存在,它描述的是一组规则和规范,通过这组规范定义了程序中各个变量的访问方式。
而Java内存区域的划分(方法区,堆,栈等)是实际的区域划分。
后续我们也会有相应的章节去详细说明Java内存区域的划分。
三、可见性 Visibility
  1. 可见性:当一个线程修改了主内存中的共享变量的时候,对于其他线程是可见的。(即修一个线程改了工作内存中的共享变量副本时,其他线程要知道你修改了这个共享变量副本)
  2. 普通变量很明显是不能实现可见性的。但是volatitle和synchorized,final是可以实现的。
  3. 后续会有章节专门说明这三个关键字。
四、有序性 Ordering
  1. 有序性:程序运行的顺序与代码的先后顺序一致。
  2. Java内存模型中的程序天热有序性可以总结为一句话:
如果在本线程内观察,所有操作都是有序的。如果在一个线程中观察另一个线程,所有操作都是无序的

前半句指的是线程内表现为串性语义,后半句指的是指令重排序和工作内存与主内存同步延迟

  1. 有序性的语意大致有几层
    1) 最常见的是 保证多线程的串性机制
    2)防止指令重排序
    3)程序运行的先后顺序,比如JMM定义的一些Happens-before原则。
    那么什么是指令重排序呢
五、指令重排序 Instroduction Reorder
int a = 1;
int b = 3;
a = 4;    //代码1
b = 5;    //代码2

如上述代码,在代码顺序中,代码1是在代码2之前的。那么JVM在调用这些代码的时候,真的会保证代码1在代码2之前运行么?不一定,这是为什么呢,因为这里发生了 指令重排序(Instroduction Reorder).

计算机在执行过程中,为了提高性能,编译器和处理器常常会对指令进行重排序

一般分为以下三种:

  1. 编译器优化重排序:编译器在不改变单线程线程语义的情况下,可以重新安排语句的执行顺序(遵循as-if-serial原则)。
as-if-serial语义:不管怎么重排序(编译器和处理器为了提高并发量),(单线程)的执行结果不会改变
  1. 指令级并行重排序:现代处理器采用了指令级并行技术,将多条指令重叠执行,如果不存在数据依赖关系(即后一个执行的语句无序依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的顺序
int a = 1; //代码1
int b = 3; //代码2
a = a + 3;  //代码3
b = a * a; //代码4

代码4是不可能在代码3之前运行的,因为它依赖了代码3的运行结果

  1. 内存系统重排序:由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去像是乱序执行,因为三级缓存的存在,导致内存和缓存的数据同步存在时间差。
    有兴趣的可以参考下述博文
    指令重排序的详细说明

理解了Java内存模型中的原子性,可见性和有序性,下一章我将再详细介绍一个有趣的修饰符 volatitle。

注意:它可以实现可见性和有序性,但是不可以原子性

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: