2

0x01 什么是并发

要理解并发首选我们来区分下并发和并行的概念。

并发:表示在一段时间内有多个动作存在。
并行:表示在同一时间点有多个动作同时存在。

例如:
此刻我正在写博客,但是我写着写着停下来吃一下东西(菠萝片)再写、再吃。这两个动作在一段时间内都在发生着,这可以理解为并发。
另一方面我在写这个博客的同时我在听音乐。那么同时存在的两个动作(写博客、听音乐)是同时在发生的这就是所谓的并行。

从上面两个概念明显可以感受到并发是包含并行操作。所以我们通常说的并发编程对于cpu来说有可能是并发的在执行也有可能是交替的在执行。


说到这里你可能会问为什么我们需要并发编程?
在求解单个问题的时候凡是涉及多个执行流程的编程模式都叫并发编程。

0x02 为什么需要并发

  1. 硬件的发展推动软件的进度,多核时代的到来

  2. 应用系统对性能和吞吐量的苛刻要求

  3. 大数据时代的到来

  4. 移动互联网、云计算对计算体系的冲击

0x03 并发编程方式

Java:多进程/多线程的并发实现方式

Go:协程--用户态实现的多线程方式(goroutine)

Java并发模型

在介绍java并发模型前我们来介绍下系统对多线程的实现方式。系统支持用户态线程和内核态两种线程的实现方式,内核态线程是cpu去调度的最小单位,所以这牵涉到用户态线程和内核态线程之间的映射关系,用户态线程:内核态线程 = 1:1 、 N:1 、 M:N。

1:1 这种映射关系充分利用多核的优势,但是这种方式在用户态进行线程切换的过程中都会涉及到内核态线程之间的切换,切换开销大。(主要涉及内核线程运行时上下文的保存与恢复)
N:1 没法充分利用多核的优势,但是这种由于是用户态的内存切换不涉及内核态线程之间的切换所以这种映射关系在线程之间切换代价小。
M:N 这种是上面两种映射关系的结合体,集合了上面两种映射关系的优势,但是这也增加了线程之间这种映射关系的调度复杂度。

Java的并发编程模式是通过1:1这种映射关系来实现线程之间的并发调度。

Go并发模型

Go的并发模式是通过M:N这种方式来实现并发调度的。
Go调度器中有三种重要结构:M(posix thread)、P(调度上下文,一般数量设置为和机器内核数相同,这样能充分发挥机器的并发性能)、G(goroutine)。

clipboard.png

一个调度上下文可以包含多个Goroutine,多个上下文所以可以所有的Goroutine都能并发的运行在CPU的多核上面。
如果有Goroutine发现找不到调度上下文,就会被放到global runqueue中,等清闲的调度上下文来捞取它进行调度。
如果调度上下文上面挂载的所有Goroutine都已经执行完毕,此时他会去global runqueue中获取Goroutine,如果发现此时没有获取到,则会去别的调度上文中抢Goroutine,一般一次抢都是抢此时被抢调度上下文的一半Goroutine,确保充分利用M去被多核调度。

0x04 并发带来的问题

在享受并发编程带来的高性能、高吞吐量的同时,也会因为并发编程带来一些意想不到弊端。

  1. 资源的消耗,要管理这么多用户线程、内核线程、用户线程内核线程之间的切换调度,上下文等等这些都是由于引用了并发编程所带来的额外消耗。

  2. 并发过程中多线程之间的切换调度,上下文的保存恢复等都会带来额外的线程切换开销。

  3. 编码、测试的复杂性。和我们生活中的例子很相像,三五个人一起出去活动很容易把控,如果带着几十、上百人的团队出去活动这些都会带来额外的管理上的开销。

真的是有阳关的地方就有黑暗啊!

上面这些都是我们没法避免的一些问题,要引用并发编程必然会要付出点额外的代价才行。但是并发编程还带来了一个不能忽视的问题,线程之间对同一资源的竞争访问,造成内存对象状态和自己的想象千差万别。

Java线程和内存对象之间的交互

java线程对内存的理解分为两部分:线程工作内存(每个线程独有的)、共享内存也叫主内存(所有的线程所共有的),下面是java线程对内存中Count对象的一次修改操作。

clipboard.png

从主线程中读取Count对象放入线程工作内存,后面的读取修改都在线程工作内存中,最后(更新到主内存的时间不是确定的,可能会插入别的操作在store、write之间)更新到主内存中。所有的上述操作都是顺序执行的,但是不保证连续执行。

volatile变量、synchronized同步块

volatile变量、synchronized块执行结束后能保证每次去更新的值都会立即写入到主内存中。
volatile变量很多人会认为这样就是线程安全的,但是通过上面我们可以看到如果两个线程同时去读了一个volatile变量,最后一前一后更新到主内存中,这样也会出现写丢失的情况,所以volatile不能保证线程安全。

0x05 并发实战

1) 定义线程池

private static final ExecutorService executor = Executors.newFixedThreadPool(20);

2)定义并发服务

CompletionService<Result> completionService = new 
      ExecutorCompletionService<Result>(executor);

3)提交并发任务

completionService.submit(new Callable<void>() {
    @Override
    public void call() throws Exception {
        return ;
    }
});

4)等待并发结果

for (int i = 0; i < taskSize; ++i) {
    Future<ModelScoreResult> future = completionService.poll(TIME_OUT,
                    TimeUnit.SECONDS);
    Result result = future.get();
}

博予liutxer
266 声望14 粉丝

专业写代码的代码仔。