前言
本文由PPT整理而来。
Page1
目录
- 进程与线程
- Java内存模型
- Java线程
Page2
进程
- 一般一个应用程序对应一个进程
- 进程有自己的资源和内存地址空间
- 各个进程之间互不干扰
- 进程会保存程序的运行状态,支持切换及恢复
- 进程使操作系统看起来同一时刻有多个任务在执行
Page3
线程
- 一个进程可以有多个线程
- 多个线程共享同一进程的资源和内存空间
- 线程创建、调度、切换及线程间通信的开销小于进程
- UI线程编程模型
- 多线程并发执行可能会引发问题
Page4
小结
- 进程是操作系统进行资源分配的基本单位
- 线程是操作系统进行调度的基本单位
- 进程让操作系统的并发成为可能
- 线程让应用程序的并发成为可能
Page5
思考
- 思考1:为什么要发明线程?何不用多进程来取代多线程?
- 思考2:多线程性能是否一定优于单线程?
- 思考3:并发与并行有什么区别?
Page6
Java内存模型
- 概念理解
- 缓存一致性
- 内存间交互协议
- volatile语义
- happens-before原则
- 原子性
- 可见性
- 有序性
Page7
物理机层面的并发
Page8
应用程序层面的并发
Page9
概念理解
- 内存模型可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。不同的物理机可能有不同的内存模型,JVM也有自己的内存模型,基本原理是类似的。
- JVM是跨平台的,为了实现跨平台,Java虚拟机规范视图定义一种JMM(Java Memory Model)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台上都能达到一致的内存访问效果。对比C/C++等非跨平台语言,需针对不同平台写程序,同时体现了JVM这个抽象层的好处。
- Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。
- Java内存模型经过长时间的验证修补和发展,JDK1.5之后,基本成熟和完善起来了。
Page10
内存间交互协议
- lock:作用于主内存变量,将其标识为线程独占
- unlock:作用于主内存变量,将线程独占释放
- read:作用于主内存变量,把变量从主内存读到线程工作内存,之后是load
- load:作用于工作内存变量,把之前read的值存入工作内存副本中
- use:作用于工作内存变量,将其值传给执行引擎
- assign:作用于工作内存变量,将从执行引擎收到的值赋给工作内存变量
- store:作用于工作内存变量,将工作内存的值传送到主内存,之后是write
- wirte:作用于主内存变量,将store操作得到的值存入主内存中
Page11
附加规则
- 不允许read和load、store和write操作之一单独出现
- 不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变后必须把该变化同步回主内存
- 不允许一个线程无原因地(没有发生assign)把数据从工作内存同步回主内存
- 一个新变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assgin)的变量,即对一个变量实施use、store操作之前,必须先执行了load和assgin。
- 一个变量同一时刻只允许一条线程对其进行lock,执行unlock才能释放
- 如对一个变量执行lock操作,则会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign初始化变量值
- 如果一个变量没有被lock,则不能执行unlock,也不能unlock其他线程
- 对一个变量执行unlock之前,必须把此变量同步回主内存(执行store、write)
Page12
volatile关键字
- 保证变量对所有线程的可见性
- 禁止指令重排序优化
Page13
指令重排序
指令重排序(Instruction Reorder)——处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的
有数据依赖性则不能进行指令重排序,如下表所示:
名称 | 代码示例 | 说明 |
---|---|---|
写后读 | a = 1;b = a; | 写一个变量之后,再读这个位置。 |
写后写 | a = 1;a = 2; | 写一个变量之后,再写这个变量。 |
读后写 | a = b;b = 1; | 读一个变量之后,再写这个变量。 |
Page14
案例1:volatile自增操作
Page15
案例2:指令重排序优化
Page16
案例3:DCL单例模式
Page17
volatile规则小结
- 在工作内存中,每次使用V前必须从主内存刷新最新的值,用于保证能看见其他线程对变量V做的修改(即read-read-use联动)
- 在工作内存中,每次修改V之后必须立刻同步回主内存,用于保证其他线程可以看到自己对V做的修改(即assign-store-write联动)
- volatile修饰的变量不会被指令重排序优化,保证代码执行顺序与程序顺序相同
Page18
happens-before原则
- 程序次序规则:在一个线程内,按照程序逻辑执行
- 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
- 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始
- 传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,则A先行发生于操作C
Page19
先行发生原则判断示例
Page20
三大特征
- 原子性
- 可见性
- 有序性
Page21
原子性
- 在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作
- read load assign use store write操作具有原子性
- synchronized同步块也具有原子性
- volatile无法保证原子性
Page22
思考:哪个操作具有原子性
- x = 10
- y = x
- x++
- x = x + 1
Page23
可见性
- volatile可以保证可见性
- synchronized
- final
- this引用逃逸
Page24
有序性
- Java程序中天然的有序性可以总结为:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句指线程内表现为串行的语义——Within Thread As-If-Serial Semantics;后半句指“指令重排序”现象和“工作内存与主内存同步延迟”现象。
- volatile禁止指令重排序
- synchronized保证线程间操作的有序性
Page25
Java线程
- 线程实现
- 线程调度
- 线程状态
Page26
线程实现方式
- 使用内核线程实现
- 使用用户线程实现
- 使用用户线程加轻量级进程混合实现
Page27
1:1模型
图片摘自《深入理解Java虚拟机》
Page28
1:N模型
图片摘自《深入理解Java虚拟机》
Page29
N:M模型
Page30
Java线程实现方式
- JDK1.2之前,使用用户线程方式实现
- JDK1.2改为基于操作系统原生线程模型实现
- 目前JVM线程的映射方式取决于操作系统支持哪种线程模型,不同平台不一定一致。如Windows、Linux上是一对一实现,即一条Java线程映射到一条轻量级进程中;而Solaris中同时支持一对一及多对多。
- Java语言层是统一的,JVM封装了操作系统的不同
Page31
线程调度方式
协同式线程调度——Cooperative Threads-Scheduling (如Windows 3.x)
抢占式线程调度——Preemptive Threads-Scheduling (线程优先级)
Page32
Java线程状态
Page33
并发编程相关
- sleep
- join
- yield
- wait、notify、notifyAll
- synchronized
- volatile
- Lock
- ThreadLocal
- 同步容器,如Vector、Stack、HashTable
- 并发容器,如ConcurrentHashMap、CopyOnWriteArrayList
- 阻塞队列BlockingQueue
- 线程池
- Callable、Future、FutureTask
- ……
参考文献
- 《深入理解Java虚拟机》