Java 内存模型与线程

一、概述

TPS:Transactions Per Second 每秒处理事务,是衡量一个服务性能高低的重要指标

二、硬件的效率与一致性

绝大多数运算任务需要处理器和内存进行 IO 交互。

计算机的存储设备与处理器的运算速度有几个数量级的差距,因此高速缓存被用来做为缓冲。

高速缓存:将运算所需要的数据复制到缓存中,让运算能够快速进行,结束后再从缓存同步回主内存之中。

每个处理器都有自己的高速缓存,这样会带来缓存一致性的问题。所以需要遵循一些协议来避免这个问题。

不同的物理机有不同的内存模型,Java 虚拟机也有自己的内存模型。

三、Java 内存模型

Java 虚拟机用 Java 内存模型来屏蔽各种硬件和操作系统的内存访问差异,以实现一致的内存访问效果。

1、主内存与工作内存

  • 变量包括实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为他们是线程私有的

  • 所有的变量都存储在主内存中,每条线程有自己的工作内存

  • 线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量
  • 不同线程也无法访问对方工作内存中的变量

2、内存间相互操作

  • lock:将主内存的变量标识为一条线程独占的状态
  • unlock:将主内存的变量,从锁定状态释放出来
  • read:将主内存的变量传输到工作线程的内存中,方便 load 使用
  • load:将 read 获取的主内存变量的值放入工作内存的变量副本中
  • use:将工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时,就会执行这个操作
  • assign:将从执行引擎接受到的值赋值给工作内存的变量,每当虚拟机遇到一个赋值的指令就执行这个操作
  • store:将工作内存中一个变量的值传送到主内存中,方便 write 使用
  • write:将 store 操作获取的值放入主内存的变量中

操作规则:

  • 不允许 read/load 、store/write 之一单独出现
  • 不允许一个线程丢弃她最近的 assign 操作,即变量在工作内存中修改之后必须同步回主内存
  • 不允许一个线程无原因的(没有发生 assign 操作)把数据同步回主内存
  • 一个新的变量只能在主内存中产生,不允许工作内存中直接使用一个未被 load 或assign 的变量
  • 对一个变量 unlock 之前,必须先把此变量同步回主内存

3、volatile 语义

当一个变量用 volatile 修饰之后,具有两种特性:

  • 保证此变量对于所有线程的可见性,所有修改会立即更新到主存
    • 但是 Java 里的运算并非原子操作,导致 volatile 变量的运算在并发下一样是不安全的
  • 禁止指令重排序优化

除了以下两种场景,我们都要加锁(synchronized 或者原子类)来保证原子性:

  • 运算结果并不依赖变量的当前值,或者可以确保只有单一的线程修改变量的值
  • 变量不需要与其他的状态变量共同参与不变约束

4、对于 long 和 double类型变量的特殊规则

允许虚拟机将没有被 volatile 修饰的 64 位数据的读写操作,划分为两次 32 位的操作来进行,这就是 long 和 double 的非原子性协定。

不过虚拟机还是选择将这些操作实现为原子性的操作。

5、原子性、可见性和有序性

1)原子性:基本数据类型的访问读写是具备原子性的。Synchronized 块之间的操作也是原子性的

2)可见性:当一个线程修改了变量的值,其他线程可以立即看到。volatile、synchronized、final 都可以保证可见性

3)有序性:如果在本线程内观察,所有的操作都是有序的,如果观察其他线程,所有的操作都是无序的。volatile 和 synchronized 可以保证线程之间的有序性。

6、先行发生原则

先行发生是 Java 内存模型中定义的两项操作之间的偏序关系,如果说 A 先行发生于 B,那么就是 B 发生之前,A 的影响能被 B 观察到。

Java 内存模型中天然的先行发生关系,这些关系无需任何同步器协助就已经存在:

  • 程序次序规则:在一个线程内,前面代码的操作先于后面的
  • 管程锁定规则:一个 unlock 操作先发生于后面对于同一个锁的 lock 操作
  • volatile 变量规则:对于一个 volatile 变量的写操作先行发生于后面对于这个变量的读操作
  • 线程启动规则:thread 的 start 方法先行发生于此线程的每一个动作
  • 线程终止规则:线程的所有操作都先行发生于终止检测
  • 线程中断规则:对 interrupt 方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 对象终结规则:对象的构造函数先行于 finalize
  • 传递性

四、Java 与线程

1、线程的实现

线程是比进程更轻量级的调度单位,各个线程既可以共享进程资源,又可以独立调度。

1)使用内核线程实现

  • 内核线程 KTL(Kernel-Level Thread)是直接由操作系统内核支持的线程。
  • 由内核来负责完成线程切换。
  • 内核通过操纵调度器来对线程进行调度
  • 程序一般通过内核线程的高级接口——轻量级进程LWP(Light Weight Process),就是通常意义上的线程 ,和内核是 1:1的关系

缺点:

  • 各种线程的创建同步操作,都需要进行系统调用
  • 系统调用需要在用户态和内核态中来回切换,所以代价很高
  • 每个轻量级进程都需要一个内核支持,所以一个系统支持的轻量级进程是有限的

2)使用用户线程实现

  • 户线程是完全建立在用户空间的线程库上,系统内核不能感知线程存在的实现。
  • 用户线程的建立同步销毁和调度全部在用户态中完成,不需要内核的帮助。操作快速而且低消耗,可以支持大规模的线程
  • 进程与用户线程是1:N

缺点:

  • 没有内核的支援,所有线程操作都要自己处理,很复杂
  • 处理阻塞等问题会无法解决

3)混合实现

内核线程与用户线程一起使用。

  • 用户线程的创建切换依然廉价,并且支持大规模并发
  • 内核提供线程调度和处理器映射,大大降低了整个进程被阻塞的风险
  • 进程与用户线程是 N:M 的关系


4)Java 线程的实现

Sun JDK 的 Windows 和 Linux 版本使用 1 对 1 的线程模型

Solaris 平台,可以同时支持 1 对 1 和 多对多的线程模型

2、Java 线程调度

分为协同式线程调度和抢占式线程调度。

协同式线程调度:

  • 线程的执行时间由线程本身来控制,当自己的事情做完之后才会通知下一个线程
  • 实现简单,但是容易阻塞

抢占式线程调度:

  • 每个线程由系统来分配时间,线程的切换不由线程本身来确定
  • 不会有一个线程导致整个进程阻塞的问题

3、状态转换