深入理解Java虚拟机-内存模型及多线程

系列阅读
1.深入理解Java虚拟机-GC&运行时数据区
2.深入理解Java虚拟机-类文件结构及加载
3.深入理解Java虚拟机-内存模型及多线程

1. Java内存模型

主内存(Main Memory)是各个线程共享的内存区域,所有的变量都存储在主内存中。线程间变量值的传递需要通过主内存来完成。

工作内存(Working Memory)是每条线程都有属于自己的区域,工作内存保存了被该线程所使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)等都必须在工作内存中进行,而不能直接读写主内存中的变量。

勉强来说,主内存对应于物理硬件的内存,工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问读写的是工作内存。

处理器、高速缓存、主内存间的交互关系

主内存与工作内存之间的交互协议,即读写同步的操作是原子的,不可再分的,包括以下8中操作:lock/unlock/read/write作用于主内存变量,use/assign/store/load作用于工作内存。

2. 线程同步

valatile同步
可以说是JVM中最轻量级的同步机制。
保证变量对所有线程的可见性,而普通变量不能保证这一点。
禁止指令重排序优化,保证变量赋值操作的顺序与程序代码的执行顺序一致。
优点:volatile变量读操作与普通变量几无差别,写操作时由于在本地代码中插入需要内存屏障质量来保证处理器不发生乱序执行,所以会慢一点。
volatile与锁之间选择的唯一依据是volatile能否满足使用场景的需求。

Java内存模型3大特性

  • 原子性
    可大致认为基本数据类型的访问读写是具备原子性的。synchronized块之间具备原子性。
  • 可见性
    指当一个线程改变了此值,新值对其他线程立即可见。Java内存模型通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的。volatile/普通变量/synchronized/final。
  • 有序性
    如果在本线程内观察,所有的操作都是有序的。如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义”,后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”的现象。valatile及synchronized可保证线程之间操作的有序性。synchronized规定了“一个变量在同一时刻只允许一条线程对其进行lock操作”。

线程的实现
线程的引入可以把一个进程的资源分配和执行调度分开,线程既可共享进程资源(内存地址、文件I/O等),也可独立调度(线程是CPU调度的基本单位)。
实现线程主要有3种方式:

  1. 使用内核线程实现
    轻量级进程(Light Weight Process, LWP)就是通常意义上的线程,每个LWP都由一个内核线程(Kernel-Level Thread,KTL)支持。


    轻量级进程与内核线程之间1:1的关系
  2. 使用用户线程实现
    广义上来说,一个线程只要不是内核线程,就可以认为是用户线程(User Thread,UT)。用户进程的建立、同步、销毁和调度完全在用户态中进行,不需要内核的帮助,所以,所有线程都需用户程序自己处理的话会异常困难。


    进程与用户线程之间1:N的关系
  3. 使用用户线程加轻量级进程混合实现
    这种混合实现下既存在用户线程也存在轻量级进程。用户线程完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。而操作系统提供支持的轻量级进程则作为用户线程和内核线程之间桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级进程来完成,大大降低了整个进程被完全阻塞的风险。


    用户线程与轻量级进程之间N:M的关系

线程调度
多线程系统的线程调度是指系统为线程分配处理器使用权的过程,主要调度方式为以下两种:
协同式调度:线程的执行时间由线程本身来控制;
抢占式调度:每个线程将有系统来分配执行时间。

线程的状态转换可参见Java并发编程学习笔记

3. 线程安全

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的操作,单次调用都可以获得正确的结果,那这个对象就是线程安全的。

线程安全的实现方法

  1. 互斥同步
    同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或者是一些,使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区、互斥量、信号量都是主要的互斥实现方式。Java中可使用synchronized关键字和RetrantLock(重入锁)来实现同步,具体参见JAVA锁机制
  2. 非阻塞同步
    互斥同步主要问题是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也叫阻塞同步。
    非阻塞同步是基于冲突检测的乐观并发策略,先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果有争用,产生了冲突,那就再采取其他的补偿措施。这种实现大都不需要把线程挂起。为了让操作和冲突检测这两个步骤具备原子性,需要硬件指令集的发展和支持。
  3. 无同步方案
    同步只是保证共享数据争用时的正确性的手段。如果一个方法不涉及共享数据则无需任何同步措施去保证正确性。比如可重入代码和县城本地存储。

操作共享的数据类型

  1. 不可变
    不可变(Immutable)对象一定是线程安全的。如果共享数据是基本数据类型,只要定义用final修饰则是不可变;如果是一个对象,需要保证对象的行为不会对其状态产生任何影响,比如String/Number部分子类/Long/Double/BigInteger/DigDecimal等。

  2. 绝对线程安全
    一个类不管运行时环境如何,调用者都不需要任何额外的同步措施。

  3. 相对线程安全
    需要保证对这个对象单独的操作是线程安全的,在调用的时候不需要做额外的保障措施。Java中大部分线程安全类都属于这种,例如Vector/HashTable/Collections的synchronizedCollection()方法包装的集合等。

  4. 线程兼容
    对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用。比如Vector/ArrayList/HashMap等。

  5. 线程对立
    无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。Java中很少出现。

注:主要内容摘录自书籍 深入理解Java虚拟机,周志明 著