Java内存模型与线程
-
Java内存模型
- Java内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节。此处的变量(Variables)与Java编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,因为后者是线程私有的
- 所有的变量都存储在主内存中,每条线程有自己的工作内存(类似缓存)
- 线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据
-
- 主内存大致对应Java堆中的对象实例数据部分,工作内存基本对应虚拟机栈中的部分区域
-
- 工作内存与主内存之间的交互
- 不允许一个变量从主内存读取了但工作内存不接受,或者工作内存发起回写了但主内存不接受的情况出现
- 变量在工作内存中改变了之后必须把该变化同步回主内存。
- 不允许一个线程无原因地把数据从线程的工作内存同步回主内存中。
- 一个新的变量只能在主内存中“诞生”
- 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁(可重入)
- 如果对一个变量执行lock操作,会清空原来这个变量在工作内存的值,也就是要求重新从主内存read、load(lock操作保证了内存可见性)
- 如果变量没有被lock,就不能被unlock
- 对变量进行unlock之前,需要先把变量store、write进主内存
- 对于volatile型变量的特殊规则
- 保证此变量对所有线程的可见性
- 可以理解为volatile变量是“写直达”到主内存
- 内存可见性无法保证并发安全
- 读-判断-改 类型问题
- 禁止指令重排序
- 每次使用V前都必须先从主内存刷新最新的值,用于保证能看见其他线程对变量V所做的修改
- 每次修改V后都必须立刻同步回主内存中,用于保证其他线程可以看到自己对变量V所做的修改。
- 保证此变量对所有线程的可见性
- 针对long和double型变量的特殊规则
- 允许虚拟机实现自行选择是否要保证64位数据类型的load、store、read和write这四个操作的原子性
- 实际环境中,基本没有出现
- 原子性、可见性、有序性
- 原子性
- sychronized等
- 可见性
- volatile、synchronized、final
- 有序性
- 如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序
- 指令重排、工作内存与主内存之间的缓冲
- 先行发生规则
- 程序次序规则
- 控制流顺序
- 管程锁定规则
- 一个unlock操作先行发生于后面对同一个锁的lock操作
- volatile变量规则
- volatile变量的写操作先行发生于后面对这个变量的读操作
- 线程启动规则
- Thread对象的start()方法先行发生于此线程的每一个动作
- 线程终止规则
- 线程中的所有操作都先行发生于对此线程的终止检测
- 对象中断规则
- 对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发
- 对象终结规则
- 一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
- 传递性
- 如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论
- 程序次序规则
- 原子性
- Java内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节。此处的变量(Variables)与Java编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,因为后者是线程私有的
-
Java与线程
-
线程的实现
- 内核线程实现(1:1)
- 运行在内核态下的线程
- 由内核线程支持的轻量级进程
- 相关调度由内核线程控制,所以有些切入内核态的花费
- 用户线程实现(1:N)
- 完全建立在用户空间上的
- 线程的调度在用户态上完成
- 一个用户进程对应多个用户线程
- 完全建立在用户空间上的
- 用户线程加轻量级进程混合实现(N:M)
- 内核线程实现(1:1)
-
Java线程调度
- 协同式
- 线程自己执行完了,才回去通知其他线程来
- 调用时间由线程自己决定
- 抢占式
- 线程执行时间由系统来分配
- 协同式
-
状态转换
-
新建
- 创建后未启动
-
运行
- 可能正在执行、可能正在等待执行
-
无限期等待
- 只能等待其他线程显示唤醒
- 没有设置时间参数的Object::wait()方法
- 没有设置Timeout参数的Thread:join()方法
- LockSupport::park()方法
- 只能等待其他线程显示唤醒
-
限期等待
- 可以自身被唤醒
- Thread::sleep()
- 设置了Timeout参数的Object::wait()
- 设置了Timeout参数的Thread::join()
- LockSupport::parkNanos()方法
- LockSupport::parkUntil()方法
- 可以自身被唤醒
-
阻塞
- 等待锁的释放
-
结束
-
-
-
Java与协程
- 协同的调度的轻量级用户线程?
- 狭隘,存在不是协同调度的轻量级用户线程
- 协同的调度的轻量级用户线程?
线程安全与锁优化
-
什么是线程安全
- 当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的
-
并发安全分级
-
不可变、绝对线程安全、相对线程安全、线程兼容和线程对立
-
不可变的对象是线程安全的
-
绝对线程安全
- 方法级别的很容易做到
- 对象级别的基本没有,强如vector,它的遍历也不是线程安全的。
- 相对线程安全
- 只要求方法/操作级别的线程安全
- 线程兼容
- 可以通过在调用端使用同步手段(锁、原子操作)来保证在并发环境中安全使用
- 线程对立
- 略
-
线程安全的实现方法
- 互斥同步(悲观策略)
- 互斥量、信号量、临界区(操作系统概念)
- synchronized
- 非公平、不能超时退出、不能被中断、锁绑定一个条件
- ReetrantLock
- 公平、等待可中断、锁绑定多个条件
- 两者性能差不多
- 非阻塞同步(乐观策略)
- 硬件支持,必须要求冲突与修改操作可以合并为“一个原子操作”
- Test-and-Set
- Compare-and-Swap
- Load-Linked/Store-Conditional
- Swap
- CAS
- CAS指令需要有三个操作数,分别是内存位置(在Java中可以简单地理解为变量的内存地址,用V表示)、旧的预期值(用A表示)和准备设置的新值(用B表示)。CAS指令执行时,当且仅当V符合A时,处理器才会用B更新V的值,否则它就不执行更新。但是,不管是否更新了V的值,都会返回V的旧值,上述的处理过程是一个原子操作,执行期间不会被其他线程中断。
- ABA问题
- 无同步方案
- 线程本地化存储
- ThreadLocal
- ThreadLocalMap
- 弱引用与内存泄露
- ThreadLocal
- 线程本地化存储
- 硬件支持,必须要求冲突与修改操作可以合并为“一个原子操作”
- 互斥同步(悲观策略)
-
-
锁优化
-
自旋锁与自适应锁
-
自旋锁
- 如果锁被其他线程占用,就先spin一下
- 竞争激烈的情况下,效果不好
-
自适应锁
- 由虚拟机来判断是否要自旋,一般来说是根据自旋时间与阻塞的比值来的。
-
锁消除
- 虚拟机会帮助判断优化掉没有意义的锁
-
锁粗化
- 适当放宽锁范围,有时候频繁加锁、释放锁
-
轻量级锁
- “对于绝大部分的锁,在整个同步周期内都是不存在竞争的”
- 但如果确实存在锁竞争,除了互斥量的本身开销外,还额外发生了CAS操作的开销。因此在有竞争的情况下,轻量级锁反而会比传统的重量级锁更慢。
- 就是每个线程在尝试获得锁的时候,都会先进行CAS判断,如果判断成功(还没人获得锁),那就拿锁,修改状态。否则,阻塞,并将轻量锁膨胀为重量锁
-
偏向锁
-
偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不去做了
-
某个线程获得了偏向锁,就通过CAS把这个线程进行记录,之后使用都不需要进行同步操作
直到其他线程尝试获取这个锁,那么偏向模式解除,要么转成未锁定,要么转为轻量锁
-
-