1. 并发设计原则
要区分并发和并行两个概念。
- 并发
- 多个任务
- 调度到单个CPU单个内核
- 多个不同的任务
- 同步机制
- 访问共享资源
- 并行
- 多个任务
- 调度到不同的计算机
- 不同的CPU
- 不同的内核
- 多个相同的任务
-
同步
- 控制同步
- 数据访问同步
- 两个重要概念:
- 临界区
- 互斥
-
粒度
- 粗粒度
- 大任务
- 低通讯
- 细粒度
- 小任务
- 高通讯
- 可能影响吞吐
- 粗粒度
几个现在流行的理论机制:
- 信号量
- 控制某个单位的资源
- 对资源进行计数
- 提供两个原子操作
- mutex
- 一种特殊的信号量
- 只有两个值
- 资源忙碌
- 资源空闲
- release
- 只有占有资源的进程可以
- mutex
- Monitor
- 监视器
- 可以实现共享资源的互斥
- 内部拥有
- mutex 互斥对象
- 条件变量
- 两个操作
- 等待条件
- 触发条件信号
- 当触发条件信号后,在等待中的任务会被唤醒一个,让其继续执行。
- 线程安全
- 如果满足下列三个情况中的任意一个,则认为代码块是线程安全的:
- 所有的共享数据都在同步机制的保护之下
- 非阻塞的CAS操作
- 不可变数据
- 如果满足下列三个情况中的任意一个,则认为代码块是线程安全的:
- 不可变对象
- 线程安全
- 例如Java中的String
-
原子操作
- 避免对整个临界区进行同步
-
原子变量
- 可以通过同步机制
- 也可以使用lock-free的CAS操作
-
共享内存
- 同一台机器
- 需要在同步机制保护的临界区中操作
-
消息传递
- 一般都是多台机器
- 同步方式
- 发送者需要阻塞并等待回复
- 异步方式
- 发送者发送消息后继续处理其他事情
- 并发应用中,可能遇到的问题
- 数据争用(竞态条件)
- 共享数据不在临界区中操作
- 没有任何同步机制
- 死锁
- 四个条件(Coffman’s conditions)同时具备时
- 互斥
- 持有并等待条件
- 没有优先级(只能等着释放,不能抢夺)
- 循环等待
- 如何避免
- 直接无视
- 发生了大不了重启
- 检测
- 安排一个专用的线程去检测系统是否死锁
- 预防
- 关注上述的四个条件
- 避免
- 执行之前判断资源
- 直接无视
- 四个条件(Coffman’s conditions)同时具备时
- 活锁
- 如果对资源加锁失败,则释放已持有的锁,并重试
- 饥饿
- 资源饥饿
- 线程等不到需要的资源
- 公平性
- 解决饥饿
- 需要额外的性能开销
- 资源饥饿
- 优先级反转
- 低优先级的任务持有着高优先级任务所需的资源
- 数据争用(竞态条件)
并发设计的方法论
- 使用串行版本来跟踪设计的过程
- 利于测试
- 逐步演进,利于问题的分析
- 利于测量
- 吞吐量
- 利于测试
- 五步法
- 基于Intel的《Threading Methodology: Principles and Practices》
- 分析
- 首先要分析出哪些代码可以并行执行
- 用并发方式去改造
- 大的代码块(逻辑)
- 花费大多数时间
- 循环
- 内置逻辑是独立的
- 设计
- 改造将会带来两个方面的改变
- 代码的组织结构
- 数据结构
- 两种方式
- 任务分解
- 并行
- 依赖
- 等待点
- 同步点
- 整合点
- 数据分解
- 临界区
- 任务分解
- 注意设计的粒度(隔离的尺度)
- 粗细平衡
- 尽可能的充分的利用CPU
- 改造将会带来两个方面的改变
- 实现
- 测试
- 调校
- 预期中的要求
- 吞吐量
- 测量
- 三个度量;
- 加速比 Speedup
- 预期中的要求
T1 = T sequential
T2 = T concurrent
Speedup = T1 / T2
2. Amdahl法则
Speedup <= 1 / ((1-P) + P/N) `
P: 并行化代码占比
N:CPU内核数量
3. Gustafson-Barsis定律
Speedup=N-(1-P)*(N-1)
-
问题
- 并不是所有的算法都可以并行处理
- 几个关键点:
- 效率
- 简单
- 可移植性
- 可扩展性
-
Java concurrency API
- Basic
Thread
Runnable
ThreadLocal
ThreadFactory
- Synchronization mechanisms
synchronized
Lock
ReentrantLock
ReentrantReadWriteLock
StampedLock
Semaphore
CountDownLatch
CyclicBarrier
Phaser
- Executors
- 分离线程创建与任务管理
Executor
ExecutorService
ThreadPoolExecutor
ScheduledThreadPoolExecutor
Executors
Callable
Future
- Fork/Join framework
- 针对细粒度的任务优化
- 开销低
ForkJoinPool
ForkJoinTask
ForkJoinWorkerThread
- Parallel streams
Stream
Optional
Collectors
- Lambda expressions
- Concurrent data structures
- Blocking data structures
LinkedBlockingDeque
LinkedBlockingQueue
PriorityBlockingQueue
- Non-blocking data structures
ConcurrentLinkedDeque
ConcurrentLinkedQueue
ConcurrentSkipListMap
ConcurrentHashMap
- Atomic
AtomicBoolean
AtomicInteger
AtomicLong
AtomicReference
- Blocking data structures
- Basic
-
并发设计模式
- 信令模式(Signaling)
- 一个任务去通知其他任务
- 方案:
- 信号量(Semaphore)
- 互斥对象(mutex)
- 重入锁(ReentrantLock)
- 使用原生的
wait()
,notify()
方法
- 集合点模式(Rendezvous)
- 类似信令模式
- 使用两个互斥对象来控制两个任务同步
- 互斥模式(Mutex)
- 实现一个临界区
- 方案:
- synchronized
- ReentrantLock
- Semaphore
- 多路复用模式(Multiplex)
- 同时允许某个数量的任务进入临界区
- 方案:
- Semaphore
- 栅栏模式(Barrier)
- 控制一些任务在某个同步点等待
- 方案:
- CyclicBarrier
- 双重检查锁定模式(Double-checked locking)
- 在申请锁之前,检查条件(减少加锁操作)
- 申请锁之后,再次检查某个条件以确定数据是否符合预期
- 读写锁模式(Read-write lock)
- 读写分离,降低锁争用
- 内部两把锁
- 读锁
- 写锁
- 逻辑
- 读操作不互斥
- 已经持有读锁时,其他任务写操作会阻塞等待读锁释放
- 已经持有写锁时,其他任务的所有操作都会阻塞等待写锁释放
- 方案:
- ReentrantReadWriteLock
- 注意:
- 如果读操作非常多,那写操作可能会阻塞等待很久
- 需要考虑,读写的优先级
- 线程池模式
- 线程维护,与任务调度分离
- 资源(CPU)隔离
- 方案:
- ExecutorService
- 线程本地存储模式(Thread local storage)(TLA)
- 定义全局或者静态变量绑定到某个任务
- 方案:
- ThreadLocal
- 信令模式(Signaling)
-
Java内存模型
- 变量修改,实际是发生在CPU的cache中,而不是主存中
- 当其他任务读取这个变量时可能读取的时主存中的旧值
- 还可能因编译器/代码优化(重排序)引发一些不可预期的问题
- 1.5重新定义了内存模型
- 定义了:
- 定义了volatile,synchronized以及final等关键字
- 在所有的平台上,保证并发安全
- 定义了一个happens-before的规则,用于:
- volatile read
- volatile write
- lock
- unlock
- 当任务请求监控(monitor),那cache就会失效
- LOCK#开头的CPU指令,会导致cache line失效
- 当任务释放了监控(monitor),那cache会刷回主存
- CPU中的cache line与主存一致性机制
- 对java开发人员透明
-
扩展
- 任务数量的变化
- 资源变化
-
线程安全的API
- ConcurrentLinkedDeque
- CopyOnWriteArrayList
- LinkedBlockingDeque
永远不要去猜测执行顺序
-
操作系统调度
-
数据竞态
-
尽可能使用不可变对象
-
在顺序加锁的时候要注意死锁
-
不要带锁休眠
- sleep
- wait
-
使用原子变量替代同步锁
- volatile
- atomic
- AtomicInteger
- AtomicLong
- AtomicReference
- AtomicBoolean
- LongAdder
- DoubleAdder
- cas
- lock-free
-
持有锁的时间尽量的短
- 临界区尽可能小
- 不要在临界区中执行不可控(外部,回调等)的代码
- 避免在临界区使用阻塞逻辑
-
延迟初始化
- 如:单例问题