并发模型
与
java并发编程
李祥乾
技术工程部
并发模型
并发编程不是新的概念
-
OS支持以一种调度算法对多个任务一段时间内同时进行处理。
- 时间片轮转
- 优先级调度
- ...
- CPU还很弱的时候,对于某些应用来说并发也是重要的。
- 图形OS的UI界面。
并发编程又火了
- 摩尔定律
- 多核化
- 多核危机
并发与并行不是一个概念
- 并发是面向问题域的概念:
- 程序需要被设计成能够处理多个(几乎)同时发生的事件。
- e.g. UI界面 在处理事件对应的action的同时也要能够继续响应新的用户事件
- 并行是面向方法域的概念:
- 程序的多个部分可以同时进行来加速解决问题。
- e.g. 把互相无依赖关系的参数获取过程调度到多个cpu核心去处理
并行不一定多核
- 现代计算机在不同层次上使用了并行技术。
- 位级并行 32位计算机比8位快.
- 指令级并行 CPU中的流水线,乱序执行,猜测执行等。
- 数据级并行 SIMD单指令多数据架构,并行在大量数据上施加同一操作
- 以上是硬件层面的
- 任务级并行 ->内存模型
- 单核CPU完全可能并行计算。
并发模型
- 线程与锁
- 所有模型的基础
- actor模型
- 发迹于Erlang, JVM有Akka
- CSP模型
- Golang的goroutine
- 单线程的事件驱动异步模型
- 源于浏览器前端js开发,运用于node.js出名.Java中有RxJava
- 协程
- 可看做语法糖,将异步+callback的方式用同步的人类的方式编写
线程与锁 - The WELL-KNOWN
- 线程与锁是一种比较原始的方式
- 接近OS的简单抽象
- 充斥与底层相关的细节,以java为例:
-
资源竞争 <- 锁与其它同步工具
- 死锁
- 数据的一致性<->吞吐量的矛盾
-
java内存模型
- 代码可能不按照你所认为的顺序所执行。
-
资源竞争 <- 锁与其它同步工具
- 需要成熟的抽象与工具
- java.concurrent包
- 并发工具 e.g. Hystrix
- 更高级的抽象- Actor Model -Akka
Java并发编程
contents
- Under the Hood
- 数据一致性
- Java内存模型
- The Hood
- 线程池ExecutorService
- ForkJoinPool, with a little functional
Under the hood
COnSISTENCY
为什么要考虑数据一致性
- java线程
- 最基本的程序流程执行单元,有自己的程序计数器、寄存器、栈和帧等。
- 与同一个jvm进程中的所有线程共享内存。
- 为提高速度,常读取的数据存放在CPU寄存器中。
- 线程可能会读到失效的数据。 -> 不一致
- Java内存模型会对指令做重排序。 -> 不一致
如何保证数据一致性
- 基本数据类型
- volatile关键字 加volatile关键字的基本数据类型对它的原子读写时会保证内存可见性。
- java.concurrent.AtomicXXX类
- AtomicInteger(或其他基本数据类型)包含大部分对数据的更新操作,保证每个操作的原子性。
- java.concurrent.AtomicXXXFieldUpdater类
- 保证数据域是原子类型,使用FieldUpdater通过反射读写。
如何保证数据一致性
- 对象引用 - 复杂的数据
- 使用synchronized关键字与对象内置锁
- 简洁,尽量缩小作用域
- 使用ReentrantLock
- 可以非阻塞地tryLock,可以设置lock的timeout。
- 使用ReadWriteLock或java8的StampedLock
- 使用synchronized关键字与对象内置锁
UNDER THE HOOD
java memory model
重排序
- 无论编译期间还是runtime,无论JVM还是CPU,都会对指令进行乱序以提高并行度。
- as-if-serial语义: 不管怎么重排序,(单线程)程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守as-if-serial语义。
- How?
- 有数据依赖的操作不会被重排序;否则可能。
double pi = 3.1415926D; //1
double r = 2.3D; //2
double area = pi * r * r; //3
1
2
3
数据依赖
- 可能的执行顺序:
- 1 -> 2 -> 3
- 2 -> 1 -> 3
并发下的重排序
- 多线程并发下,程序有了多个执行流,执行顺序依赖于程序、线程的调度以及许多未知因素,数据的依赖关系难以确定。
- 此时,重排序可能会导致意外的结果。as-if-serial不再能够保证程序执行结果一致。
class ReorderExample {
int a = 0;
boolean flag = false;
public void writer() {
a = 3; //1
flag = true; //2
}
Public void reader() {
if (flag) { //3
int i = a * a; //4
……
}
}
}
flag = true; //2
if (flag) //3
int i = a * a; //4
a = 3; //1
重排序
thread 1
thread 2
i = 0;
设置为volatile
volatile
- 为人熟知的volatile关键字的用处:
- 在多线程并发程序中保持变量的内存可见性。
- 对volatile的写对于任何线程在其后的读是可见的。
-
从JSR-133开始(java5),对volatile变量的写-读操作可以实现线程间的通信。具有原子性的特性。
- 以『内存屏障』CPU指令的方式禁止特定的重排序
volatile语义---一致性
假设每个线程都拥有一个虚拟的CPU
local memory
a |
---|
flag |
CPU1
a |
---|
flag |
shared MEM
bus
local memory
a |
---|
flag |
CPU2
bus
volatile写(1)-写register
volatile写(2)-写回共享内存
volatile读(1)-register置为无效
volatile读(2)-从共享内存读
普通写-写register
普通读-读register
volatile语义---禁止重排序
volatile写
普通写
内存屏障
普通读
volatile读
volatile写
volatile读
class ReorderExample {
int a = 0;
volatile boolean flag = false;
public void writer() {
a = 3; //1
flag = true; //2
}
Public void reader() {
if (flag) { //3
int i = a * a; //4
……
}
}
}
flag = true; //2
if (flag) //3
int i = a * a; //4
thread 1
thread 2
i = 9;
a = 3; //1
volatile与lock的区别
- volatile只能保证单个volatie变量的简单读写的原子性。
- i++,i--是不能保证原子性的。
- lock确保整个临界区code block的原子操作。
final关键字
- 对final域的读和写,编译器,JVM与处理器遵循以下原则:
- 在构造器方法中对一个final域的写入,与随后把这个被构造对象的引用赋给一个引用变量不可被重排序。
- 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作不能被重排序。
public FinalExample {
int i;
final int j;
static FinalExample INSTANCE;
public FinalExample(int i, int j) {
this.i = i; //2 写普通域
this.j = j; //3 写final域
}
public static void init() {
INSTANCE = new FinalExample(2, 3); //1
}
public static void read() {
if (INSTANCE != null) {
int readI = INSTANCE.i; //4 读普通域
int readJ = INSTANCE.j; //5 读final域
}
}
}
进入FinalExample构造方法
thread 1
thread 2
this.j = j;//3 写final域
this.i = i;//2 写普通域
构造完成返回给INSTANCE引用
if (INSTANCE != null)
int readI = INSTANCE.i;
int readJ = INSTANCE.j;
重排序,逃逸
readI=0;readJ=3;
THE HOOD
世界这么乱
这还怎么玩
编程分两种
- 应用层面的开发
- 基础工具/库/框架开发
一个应用开发者
- 大部分时候你都不需要关注这些细节。
- 大部分时候不造轮子。
- 应用容器jetty, 依赖注入框架Spring Framework,java.concurrent包,并发框架Hystrix等等帮我们屏蔽了细节。
- 但是仍然需要能发现坑,找到坑,能填坑。
- 学会挑选合适的抽象和工具。
THE HOOD
EXECUTOR Service
Executor Service
- submit异步执行的task.
- 控制线程资源。
- 在shut down前可以提交新的task。
- 可以强行shutDownNow()终止所有task。
- Best Practice: 使用工厂类Executors初始化一个ExecutorService实例
- 所有的Executors工厂构造的线程池背后都是同一个实现——ThreadPoolExecutor。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
...
}
- corePoolSize: 保持长期alive的核心线程数量
- maximumPoolSize: 池的最大线程数
- keepAliveTime & timeUnit: 大于corePoolSize小于maximumPoolSize的线程最长保留时间,超时会被销毁。
- workQueue: 线程安全的BlockingQueue实现,用于缓存计划运行的RunnableTask。
- threadFactory: 可以自定义生成线程的工厂类。
- handler: 无法handle时自定义任务的abort策略。
- workerNum < core:即使有闲置的worker,也会新建新的worker去handle这个task。
- core < workerNum < max:只有在workQueue满了才会新建worker去handle这个task。否则add到workQueue.
-
workerNum == max且workQueue full:选择预设的RejectExecution策略。预设了三种策略:
- CallerRunsPolicy:使用调用execute方法提交task的调用线程直接运行这个被reject的task。如果executor已经被shut down了,就抛弃这个task。
- AbortPolicy: 直接抛RejectExecutionException。
- DiscardPolicy: 悄悄地就丢弃了task。
- DiscardOldestPolicy: 抛弃在workQueue没有处理的最旧的task然后再次尝试execute。如果线程池已经被shut down就直接抛弃。
public class Executors {
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(
1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
}
Executors工厂类的工厂方法
SynchronousQueue - 大小恒定为1的BlockingQueue,放一个必须取一个
线程池的好处
- 显而易见的:
- 节约线程初始化开销。
- 控制线程资源。
- 具备缓存task的能力,动态调节生产消费速度。
- 对于使用原生的线程,一个task遭遇到重大的Error会导致线程挂掉。而线程池会在需要的时候新建线程去处理缓存的tasks。
THE HOOD
FORKJOIN POOL - a little functional
Another POOL
- 用于运行ForkJoinTask的固定线程池大小的ExecutorService
- 最本质的不同:Task与worker的调度算法使用了work-stealing算法。
- ForkJoinTask应该是functional的、immutable的、存在递归调用的Task。能够递归分解成子任务的任务适合使用。ForkJoinTask是一个轻量级的Future。
merge-sort
class MergeSort<T> {
private List<T> merge(List<T> list0, List<T> list1) {
//...
}
private Entry<List<T>, List<T>> split(List<T> list) {
//...
}
public mergeSort(List<T> list) {
Preconditions.checkArgument(list != null && list.size() > 0);
if (list.size() == 1) {
return list;
}
Entry<List<T>, List<T>> splitted = split(list);
List<T> merge0 = mergeSort(splitted.getKey());
List<T> merge1 = mergeSort(splitted.getValue());
return merge(merge0, merge1);
}
}
单线程version
-
典型的递归调用
- 分解问题
- 递归解决子问题
- 合并子问题结果
class MergeSortTask<T> extends RecursiveTask<List<T>> {
private List<T> merge(List<T> list0, List<T> list1) {
//...functional implementation
}
private Entry<List<T>, List<T>> split(List<T> list) {
//...functional implementation
}
protected final List<T> list;
public MergeSortTask(List<T> list) { this.list = list; }
@Override
protected List<T> compute() {
if (list.size() == 1) { return list; }
Entry<List<T>, List<T>> splits = split(list);
ForkJoinTask<List<T>> task0 = new MergeSortTask<T>(splits.getKey()).fork();
ForkJoinTask<List<T>> task1 = new MergeSortTask<T>(splits.getValue()).fork();
List<T> list0 = task0.join();
List<T> list1 = task1.join();
return merge(list0, list1);
}
}
ForkJoinTask parrallel version
- fork: 创建一个新task加到待执行的队列里。 <-async
- join: 阻塞等待join被调用task的结果。<- synchronization
- 构造方法传递消息,而非共享内存。
- immutable的function实现。
Get it running
把它丢进ForkJoinPool就得了
public class SortUtils {
public <T> Future<List<T>> mergeSort(List<T> list) {
ForkJoinPool pool = new ForkJoinPool(Runtime.getRuntime()
.availableProcessors());
return pool.submit(new MergeSortTask<T>(list));
}
}
Work-Stealing
Worker
Worker
Worker
...
ForkJoinTask 0 |
---|
ForkJoinTask 1 |
ForkJoinTask 2 |
ForkJoinTask 0 |
---|
ForkJoinTask 1 |
ForkJoinTask 2 |
ForkJoinTask 3 |
ForkJoinTask 4 |
ForkJoinTask stealed |
---|
run out of tasks
steals
forks
- fork出的子task默认被加到当前运行worker的workQueue里。
- 自己queue里没有task的worker会以一种算法去其他worker的queue里偷没有run的task。
Work-Stealing的好处
- 能不调度的时候就不调度,降低调度成本 <- fork出的task在运行它的worker队列里
- 调度算法运行在run out of tasks的worker中 -> distributed
- 无中心的work queue -> 减少频繁竞争的资源,提高吞吐
- Java 8里的stream API中, parrallelSort()新接口,并行排序的方法,就使用了ForkJoinPool实现的。
More
优先使用更加抽象的并发工具,而非用自己的轮子
- 稳定、高性能
- 通过抽象隐藏细节,避免无法预测的『坑』,让并发程序更加可读、可测试、可维护、可debug、可监控。
- 如Hystrix Commands..提供了dashboard和metrics。
- 更加抽象的模型:
- Actor model: Akka
- asynchronous: RxJava
- 如果要阅读源代码,java.concurrent包是很好的学习来源。
- concurrent包里还有很多好用的工具:
- Condition
- BlockingQueue
- CopyOnWriteList
- AbstractQueuedSynchronizer
- ReentrantLock
- StampedLock(java8)
- CyclicBarrier
- ...
References
- InfoQ 深入理解java内存模型 系列文章 http://www.infoq.com/cn/articles/java-memory-model-1
- Java8 Api Documentation
- Java8 concurrent包源代码
- 《七周七并发模型》
Thanks
Q&A
java concurrency
By Xiangqian Lee
java concurrency
- 753