Java内存模型
2020-06-11 13:46:48 0 举报
AI智能生成
Java并发编程原理、Java并发包包含哪些内容、Java并发包到底是怎么来的、Java内存模型、并发的微观和宏观
作者其他创作
大纲/内容
Java内存模型
按需禁用缓存按需禁用编译优化
JVM
方法
volatile
synchronized
final
final 修饰变量时,初衷是告诉编译器:这个变量生而不变,可以可劲儿优化
六项 Happens-Before 规则
前面一个操作的结果对后续操作是可见的
顺序性
传递性
volatile 变量规则
valatile变量的写Happens-Before后续的读
锁的规则
对一个锁的解锁 Happens-Before 于后续对这个锁的加锁
线程 start() 规则
主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作
线程 join() 规则
在线程 A 中,调用线程 B 的 join() 并成功返回,那么线程 B 中的任意操作 Happens-Before 于该 join() 操作的返回
...
线程中断规则
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
可以通过Thread.interrupted()方法检测到是否有中断发生
对象终结规则
一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始
Happens-Before 规则
约束了编译器的优化行为
虽允许编译器优化
但是要求编译器优化后一定遵守 HappensBefore 规则
关键字
规则
在 Java 语言里面
Happens-Before 的语义本质上
是一种可见性
在现实世界里
如果 A 事件是导致 B 事件的起因
那么 A 事件一定是先于(Happens-Before)B 事件发生的
A Happens-Before B
意味着 A 事件对 B 事件来说是可见的
无论 A 事件和 B 事件是否发生在同一个线程里
A 事件发生在线程 1 上,B 事件发生在线程 2 上
Happens-Before 规则保证
证线程 2上 也能看到 A 事件的发生
这里所谓的发生/看见
指的都是:共享变量
Java内存模型 两部分
面向开发人员的
即上述Happens-Before 规则
面向JVM开发人员的
涉及到具体的实现了
volatile保证可见性功能的实现
synchronized——管程在Java中的实现:隐式锁
final 不变性的实现
了解(仅仅为了装逼)、忽略即可
隐式规则
只要我们按照这些 默许的规则 编写并发程序
就能够达到
的目的
并发的博弈
可见性
按需禁用缓存
有序性
按需禁用编译优化
原子性
按需禁用CPU中断
原子性问题的源头是 线程切换
操作系统 做线程切换 是依赖 CPU中断的
单核时代
禁止CPU发生中断 就能够 禁止线程切换
多核时代
同一时刻,有可能有N个线程同时在执行
N个核(CPU)在工作 = 同时N个线程在工作
一个线程执行在 CPU-1 上一个线程执行在 CPU-2 上
此时禁止 CPU中断
只能保证 CPU上的线程 连续执行
并不能保证 同一时刻只有一个线程执行
同一时刻只有一个线程执行
互斥
杀手级解决方案:锁
保证 对共享变量的修改是互斥的
无论是 单核CPU 还是多核CPU
都能保证 原子性
互斥锁
谨防:锁自家门来保护他家资产
性能:细粒度锁
“原子性”的本质
其实不是不可分割
不可分割只是外在表现
其本质是:多个资源间有一致性的要求
操作的中间状态对外不可见
在 32 位的机器上写 long 型变量有中间状态(只写了 64 位中的 32 位)
在银行转账的操作中也有中间状态(账户A 减少了100,账户B 还没来得及发生变化)
解决原子性问题
是要保证 中间状态对外不可见
预防 死锁
死锁发生的条件:四个条件 缺一不可
共享资源 X 和 Y 只能被一个线程占用
互斥这个条件我们没有办法破坏
因为我们用锁为的就是互斥
占有且等待
线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X
破坏:可以一次性申请所有资源
等待-通知
synchronized + 等待:wait() + 通知/唤醒:notify()、notifyAll()
不可抢占
其他线程不能强行抢占线程 T1 占有的资源
破坏: Lock 是可以轻松解决这个问题的
循环等待
线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源
破坏:对资源进行排序,然后按序申请资源
反向分析
只要我们破坏其中一个,就可以成功避免死锁的发生
性能
Java SDK 并发包里之所以有那么多东西
有很大一部分原因 就是要提升在某个特定领域的性能
并发问题
微观
原子性问题
可见性问题
有序性问题
宏观
安全性问题
安全性方面 要注意 数据竞争 和 竞态条件
活跃性问题
活跃性方面 需要注意 死锁、活锁、饥饿等问题
性能问题
最好的方案 自然就是使用 无锁的算法 和 数据结构 了
写入时复制 (Copyon-write)
乐观锁
Java 并发包里面的原子类也是一种 无锁的数据结构
Disruptor 则是一个 无锁的内存队列
减少锁持有的时间
使用 细粒度的锁:
ConcurrentHashMap
还可以使用读写锁
也就是读是无锁的,只有写的时候才会互斥
Java 语言里的线程
本质上就是操作系统的线程,它们是一一对应的
为什么要区分 堆和栈
局部变量 是和 方法(栈)同生共死的
一个变量如果想跨越方法的边界,就必须创建在堆里
每个线程都有自己独立的调用栈
每个线程都有自己的调用栈,局部变量保存在线程各自的调用栈里面,不会共享
没有共享,就没有伤害
线程封闭
方法里的局部变量,因为不会和其他线程共享,所以没有并发问题
这个思路很好,已经成为解决并发问题的一个重要技术:线程封闭
当你看到代码里出现 if 语句的时候
就应该立刻意识到 可能存在 竞态条件
Lock 和 Semaphore 的区别
Lock --> 允许 1 个 线程进入临界区Semaphore --> 允许 N个 线程进入临界区
管程和信号量 这两个 同步原语 在 Java 语言中的实现,理论上用这两个同步原语中任何一个 都可以 解决所有的 并发问题 。
Java SDK 并发包里 为什么还有很多其他的工具类呢
分场景优化性能,提升易用性
ReadWriteLock
StampedLock
CountDownLatch
CountDownLatch 主要用来解决 一个线程等待多个线程 的场景
CountDownLatch 的计数器是 不能循环利用 的
CyclicBarrier
一组线程之间互相等待
但CyclicBarrier 的 计数器 是可以 循环利用 的,而且具备自动重置的功能,一旦 计数器减到0 会 自动重置 到你设置的 初始值
所有的阻塞操作,都需要设置超时时间,这是个很好的习惯
线程安全容器
同步容器
Collections
synchronized(this)
this --> 包装容器
并发容器
Java容器中的快速失败机制(fail-fast)
ConcurrentModificationException
它会探查容器上的任何除了你的进程所进行的操作以外的所有变化,一旦它发现其它进程修改了容器,就会立刻抛出ConcurrentModificationException异常
“快速报错”的意思
即:不是使用复杂的算法在事后来检查问题
原子类
基本 不会 出现 死锁
但可能出现 饥饿 和 活锁 问题,因为 自旋 会 反复重试
reference ABA 问题
version版本号
能够解决一些 简单的 原子性 问题
所有原子类的方法 都是针对 一个 共享变量 的
解决 多个变量 的 原子性 问题
还是使用 互斥锁 方案
concurrent包的实现
线程
是一个 重量级 的 对象,应该 避免 频繁 创建 和 销毁
线程池
是一种 生产者 - 消费者 模式
线程池 的 使用方 是生产者,线程池 本身 是消费者
强烈建议 你要根据 不同的业务类型 创建 不同的线程池 ,以 避免 互相干扰
FutureTask
实现了 Runnable 和 Future
如果任务之间有 依赖 关系,比如当前任务依赖前一个任务的执行结果,这种问题基本上都可以用 Future 来解决
工具类
主要是 会用,知道 什么场景 用什么
重点关注: 细节问题 与 最佳实践
产生背景 、应用场景 以及 实现原理
异步编程
1.8
CompletableFuture
串行、并行、AND聚合、OR聚合
1.9
Flow API
并发工具
简单的并行任务
线程池 +Future
任务之间有聚合关系
CompletableFuture
批量的并行任务
CompletionService
ThreadPoolExecutor + Future
Executor
ThreadPoolExecutor
共享 一个队列
ForkJoinPool
有 多个任务队列
任务窃取 机制
最佳实践
我们写程序,不是做数学题,而是在 搞工程
工程中会有 很多 不稳定的因素,更有很多你预料不到的情况发生
所以不要让你的代码铤而走险,尽量使用 更稳妥 的 方案和设计
范例
while(true) 总不让人省心
死循环
break
活锁
随机数
signalAll() 总让人省心
用 signalAll() 会 更安全
写代码是 搞工程
尽量使用 更稳妥 的方案
Semaphore 需要锁中锁
Semaphore 允许多个线程访问一个临界区
这也是一把 双刃剑
当 多个线程 进入临界区 时
如果需要 访问共享变量 就会 存在并发 问题
所以 必须加锁
也就是说 Semaphore 需要 锁中锁
锁的申请和释放要成对出现
StampedLock
锁的升级
会生成新的 stamp ,而 finally 中释放锁用的是 锁升级前的 stamp
要对 stamp 重新赋值
回调总要关心执行线程是谁
当看到回调函数的时候,一定问一问执行回调函数的线程是谁
共享线程池:有福同享就要有难同当
所有的 CompletableFuture 默认共享一个 ForkJoinPool
当有阻塞式 I/O 时
可能导致所有的 ForkJoinPool 线程都阻塞
进而影响整个系统的性能
更倾向于使用 隔离 的方案
每个任务类型创建 单独的 线程池
线上问题定位的利器:线程栈 dump
定位线上并发问题
通过查看 线程栈 来 定位问题
重点是查看 线程状态
分析线程 进入该状态 的原因 是否合理
给线程赋予一个 有意义的 名字
......
并发设计模式
Immutability模式:利用不变性解决并发问题
1. 不可变类的特点:类、属性都是final的,方法是只读的1. 不可变类的特点:类、属性都是final的,方法是只读的
2. 为了解决有些不可变类 每次创建一个新对象 导致内存浪费的问题:享元模式/对象池
3. 注意事项:区别引用不可变和实际内容不可变
4. 更简单的不可变对象:无状态对象
Copy-on-Write模式:不是延时策略的COW
线程本地存储模式:没有共享,就没有伤害
Guarded Suspension模式:等待唤醒机制的规范实现
Balking模式:再谈线程安全的单例模式
Thread-Per-Message模式:最简单实用的分工方法
对应到现实世界,其实就是 委托代办
Worker Thread模式:如何避免重复创建线程?
类似于 车间里工人 的工作模式
提交到相同线程池中的任务一定是 相互独立 的,否则就一定要慎重
两阶段终止模式:如何优雅地终止线程?
第一个阶段主要是 线程T1 向 线程T2 发送终止指令
第二阶段则是 线程T2 响应终止指令
“毒丸”对象
“毒丸”对象是 生产者 生产的 一条 特殊任务
然后当 消费者线程 读到 “毒丸”对象 时
会 立即终止 自身的 执行
生产者-消费者模式:用流水线思想提高效率
避免共享的设计模式
Immutability 模式
需要注意对象属性的不可变性
Copy-on-Write 模式
需要注意性能问题
线程本地存储 模式
需要注意异步执行问题
多线程版本 IF 的设计模式
Guarded Suspension 模式
会等待 if 条件 变为 真
Balking 模式
则 不需要 等待
三种最简单的分工模式
Thread-Per-Message 模式
Worker Thread 模式
需要注意潜在的线程 死锁问题
任务之间 不要有 依赖关系
生产者 - 消费者模式
并发设计模式是前人在做并发编程时已经归纳好的,在不同场景下具有可行性的设计模式,我们在设计并发程序时应当优先考虑这些设计模式(以及这些设计模式的组合)
Immutability模式
充分利用了面向对象的封装特性,将类的mutator的入口全部取消,自身的状态仅允许创建时设置。状态的改变通常通过新建一个对象来达成,为了避免频繁创建新对象,通常通过享元模式或对象池来解决该问题。因此,其适用于对象状态较少改变或不变的场景,需要对一定的内存overhead可容忍。
COW模式
通过写时拷贝的方式,保证读取时候的无阻塞及多线程读写时的无共享,由于其写入时的拷贝机制和加锁机制(JAVA中),因此仅适合于读多写非常少的场景。
相比于Immutability模式,COW将引用指向新对象的操作封装在了内部(JAVA中)来实现一定的可变性。
线程本地存储模式
利用线程本地存储空间(TLAB)来存储线程级别的对象,以保证各线程操作对象的隔离性,一定程度上可以等同于能够携带上下文信息的局部变量。
JAVA中是在用户空间实现的ThreadLocal控制的,目前的实现可以保证map的生命周期与各Thread绑定,但Value需要我们手动remove来避免内存泄漏。
对各类并发设计模式,考量其 核心思想、核心技术、trade-off、适用场景、与其他设计模式的对比等。
首先,应当考虑没有共享的模式,这类方式用一些技术手段来避免并发编程中需要考虑的同步、互斥等问题,有些模式的实现也被称为无锁机制,其简单且不易出错。
其次,从分工、同步、互斥三个角度来看几个设计模式
从 分工 的角度看
以下三种模式在对线程 工作粒度 的划分上 逐渐变细
Thread-per-message 模式
Thread-per-message模式通过一消息/请求一线程的方式处理消息/请求,这种模式要求线程创建/销毁overhead低且线程占用内存的overhead也低,因此在overhead高时需要保证线程的数量不多,或者采用更轻量级的线程(如协程)来保证。
Worker Thread模式相当于在Thread-per-message模式的基础上,让消息/请求与threads的工厂打交道,在JAVA中可以理解为线程池,通过将同类消息/请求聚类到某类工厂(也有工厂模式的意思在)来为这类消息/请求提供统一的服务(定量的线程数、统一的创建方法、统一的出错处理等),当然,它依然有Thread-per-message中需要控制线程占用内存的问题。
生产者-消费者 模式
生产者-消费者模式在Woker Thread模式的基础上,加入了对消息/请求的控制(大部分使用队列来控制),并划定了生产者线程和消费者线程,其中它也包含了同步和互斥的设计,在JAVA中的线程池中也可见一斑。这类设计常见于MQ中。
从 同步 和 互斥 的角度看
多线程版本的if被划分为了两种模式(Guarded Suspension模式和Balking模式)
是 传统的 等待-通知 机制 的实现,非常标准化,JAVA中则依赖管程实现了各种工具类来保证多线程版本if的正确性
依赖于 互斥 保证 多线程版本 if的正确性
两阶段终止模式
在线程粒度的管理中通过中断操作和置位标记来保证正常终止,JAVA中在线程池粒度的管理中可以通过SHUNDOWN方法来对线程池进行操作,源码中可以看到,其实质也是通过第一种方式来达成目的的。
0 条评论
回复 删除
下一页