并发模型

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

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

Thanks

Q&A

java concurrency

By Xiangqian Lee

java concurrency

  • 731