本篇文章,首先了解虚拟机Java 内存模型的结构及操作,然后讲解原子性,可见性,有序性在 Java 内存模型中的体现,最后介绍先行发生原则的规则和使用.
在多数情况下让计算机同时去做几件事情,不仅是因为计算机的运算能力强大,还有一个重要的原因是计算机的运算速度与它的存储和通信子系统速度的差距太大,大量的时间都花费在磁盘 I/O, 网络通信或者数据库访问上.

1.1 硬件的效率与一致性

计算机的存储设备与处理器运算速度有很大的差距,所以加入了一层高效缓存来作为内存与处理器之间的缓冲:将运算需要的数据复制到缓冲区中,让运算快读进行,当计算完毕之后再将结果从缓冲区同步到内存.但这样做也引入一个新的问题,缓存一致性.

缓存一致性

在多处理系统中,每个处理器都有各自的高效缓存,而它们共享同一内存,当涉及到同一块主内存区域时,将可能导致缓存数据不一致.为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议.在读写时根据协议来操作.Java 虚拟机有自己的内存模型,内存模型可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写访问的抽象过程.

为了使处理器内部单元充分利用,处理器对输入代码进行乱序执行优化,在计算之后将乱序执行的结果重组.保证结果一致.但不保证各个语句计算的先后顺序与输入代码一致.

1.2 Java 内存模型

Java 虚拟机试图定义一种 Java 内存模型来屏蔽各种硬件和操作系统的内存访问差异,实现在各个平台内存访问的一致性.

1.2.1 主内存与工作内存

Java 内存模型主要目标是定义程序中各个变量的访问规则,即虚拟机中从内存中存储和取出变量的底层细节.此处的变量与 Java 编程中的变量有所区别,包括实例字段,静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被分享.自然就不会存在竞争问题.

Java 内存模型规定所有的变量都存储在主内存中.每条线程还有自己的工作内存,工作内存中保存被该线程使用变量的主内存副本拷贝.线程中所有操作必须在工作内存进行,不能直接读写主内存中的变量,不同线程不能直接访问其它工作内存的变量,线程间的传递需要通过主内存完成.

1.2.2 内存间交互操作

  • lock(锁定):作用于主内存变量,将一个变量标识为一条线程独占的状态
  • unlock(解锁):作用于主内存变量,将处于锁定状态的变量释放出来,释放的变量才可以被其它线程锁定.
  • read(读取):用作于主内存变量,将一个变量的值从主内存传输到线程的工作内存,以便随后的 load 动作使用
  • load(载入):用作与工作内存的变量,把 read 操作从主内存中得到的变量值放入工作内存的变量副本中.
  • use(使用):作用于工作内存的变量,把工作内存中一个变量的值传递给执行引擎.当虚拟机的字节码指令使用到变量的值时将会执行这个操作.
  • assign(赋值):用作与工作内存的变量,将一个从执行引擎接收到的值赋值给工作内存的变量,当虚拟机的字节码执行遇到一个给变量赋值时执行这个操作.
  • sore(存储):作用于工作内存变量,将工作内存一个变量值传宋到主内存中,以便随后的 write 操作的使用.
  • write(写入):用作与主内存的变量,将 store 操作从工作内存中得到的值放入主内存的变量中.

如果要把一个变量从主内存复制到工作内存,就要顺序的执行 read和load 操作.如果要把变量从工作内存同步回主内存,就要顺序执行 store 和 write 操作.

1.2.3 对于 volatile 型变量的特殊规则

关键字 volatile 是 java 虚拟机提供的最轻量级的同步机制,但它并不容易完全被正确,完成地理解.

当一个变量定义为 volatile 之后,将具备两种特性,第一是保证变量对所有线程的可见性,当一个线程修改了这个变量的值,其他线程可以立即得到新值.第二是禁止指令重排序优化,普通的变量仅仅保证方法的执行过程中所有依赖赋值结果的地方都能获取到正确结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致.

1.2.4 原子性,可见性与有序性

原子性

基本数据类型的访问读写是具备原子性的,如果应该场景需要一个更大范围的原子性操作,在 Java 中就是使用 synchronized同步代码块操作也具备原子性.

可见性

可见性指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改.

有序性

Java 内存模型中程序天然有序性可以总结为:在本线程内观察,所有操作都是有序的.如果在一个线程观察另一个线程,所有操作都是无序的.

1.2.4 先行发生原则

先行发生是Java 内存模型中定义的两项操作之间的偏序关系,如果说操作 A 先发生于操作 B ,也就是发生操作B 之前,操作 A 产生的影响能被操作 B 观察到,”影响”包括修改了内存中共享变量的值,发送了消息,调用方法等.

1.3 Java 与线程

1.3.1 线程的实现

线程是比进程更轻量级的调度单位,线程可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源,又可以独立调度.

实现线程主要有3种方式

  1. 使用内核线程实现

内核线程是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上.

但是它有局限性,首先由于是基于内核线程实现,所以各种线程操作都需要进行线程调用.而系统调用的代价相对较高,需要在用户态和内核态中来回切换.其次每个轻量级进程都需要一个内核线程支持,因此轻量级进程要消耗一定内核资源,因此一个系统支持轻量级进程的数量是有限的.

  1. 使用用户线程实现

广义上讲,一个线程只要不是内核线程就可以认为是用户线程

狭义上的用户线程指完全建立在用户空间的线程库上.系统内核不会感知线程存在,用户线程的操作完全在用户态中完成,不需要内核的帮助.

使用用户线程的优势在于不需要系统内核支援,劣势也在于没有系统内核支援所有操作都需要用户进程自己处理.Java,Ruby 等语言曾经使用过用户进程,最终又都放弃.

  1. 使用用户线程加轻量级进程混合实现

在这种混合实现下,即存在用户线程,也存在轻量级进程.用户线程还是完全建立在用户空间中,而操作系统提供的轻量级进程则作为用户线程与讷河线程之间的桥梁,这样可以使用内核提供的线程调度功能以及处理器映射.

Java 线程的实现

在 JDK1.2中线程模型替换为基于操作系统原生线程模型来实现.因此操作系统支持怎样的线程模型,很大程度上决定了 Java 虚拟机的线程是怎样映射的.

1.3.2 Java 线程调度

线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种:协同式线程调度和抢占式线程调度.

协同式多线程系统,线程的执行时间是由线程本身来控制.当线程工作执行完毕之后,要主动通知系统切换到另一个线程上.缺点是线程时间不可控制,容易造成程序阻塞.

抢占式调度多线程系统,每个线程由系统来分配执行时间,线程的切换不由线程本身来决定,Java 使用抢占式调度.

1.3.3 状态转换

Java 语言定义了5种线程状态,在一个线程点,一个线程只能有且只有一种状态

  • 新建,运行,等待, 阻塞, 结束