并发编程历史观

并发编程(Concurrency Programming),从字面意思来看,自然让人联想到两个或多个的意思。并发编程的出现是计算机硬件,操作系统进步的结果,也是计算机科学追求更快速度,更高效率解决问题的结果。

并发出现之前

在并发出现之前,程序的执行自然就都是串行了。那时计算机CPU都是单核的,内存也非常小。需要利用计算机解决的问题也远没有现在这样复杂。因此将程序串行化自然能够解决很多问题。

随着计算机软件和硬件的发展,不仅一台计算机可以有多个CPU,一个CPU还可以有多核。内存变得越来越大,速度也变得越来越快了。为了充分利用这些资源,同时为了提高计算效率和应对更大的计算量,自然而然出现了并发编程技术。

并发的基本概念

并发和并行

并发和并行,虽然只有一字之差,事实上这两个概念有很大的不同。

  • 并发是多个线程做同一件事情:

    • 这些进程可能一点关系也没有,举个例子,求N个数之和,我们可以将这N个数分成M分,用M个线程去分别求和,最后再由一个线程合并求和,这就是并发。
  • 并行是多个进程做不同的事情:

    • 一个程序做的事情是求N个数之和,不管这个程序是一个线程完成还是多个线程完成。
    • 另一个程序做的事情是求N个数的乘积,也不管这个程序是一个线程完成还是多个线程完成。
    • 这两个线程在各自的内存区域共同受一个CPU调度,而且这两个程序还是同时进行。那这就是并行。

总结来说,并发是彼此之间有联系,可以共享数据,并行是彼此之间没有多少联系,做着不同的事情不会共享数据。

并发编程

由并发牵扯到的一个最重要的概念就是线程(Thread),并发的手段就是多线程,因此并发编程也可以叫做多线程编程,如果没有多线程的编程绝对不能叫做并发编程。

并发编程的执行单元

并发编程的执行单元,那线程又是什么?线程可以理解为小粒度的进程,和进程的区别在于线程之间共享内存,进程之间独立内存。

串行和并行

串行

串行的反义词是并行,串行可以理解为一次只做一个事情,从微观层面来看,CPU的指令执行必定都是串行的,一个时钟周期内,单个CPU的单个核内不可能同时执行两条指令。

并行

并行的话上面说过,是多个进程做不同的事情,从微观上来看,一个多核CPU或者多个CPU在同一个时钟周期内,执行不同的指令可以认为是并行。

单核和多核

随着计算机硬件的发展,逐渐出现了多核甚至多个CPU(宏观上可以把多核和多CPU等同),这样并发编程就自然而然出现了。

事实上,这种说明严格来说,是不符合历史的。不能说多核就一定是并发,单核就一定只能是串行。单核也可以并发,多核自然绝对可以串行。

多CPU(或者说多核)出现之前,是有并发的。只不过这时候的并发从指令的微观层面来看,它是串行化的,一个CPU时钟周期内,一定只有一个线程在运行。

因此,这个时候,多线程编程的性能和效率不一定比串行编程要高。当然,并行比串行自然效率要高。

举个例子,某个程序是CPU密集计算,某个时刻正在CPU计算,另一个
程序是IO密集的,同一时刻可以调度它去进行IO计算而不影响CPU密集计算的程序,这样自然就提高了效率。

单核可以并发,也可以并行,多核的场景下进行并发编程比单核效率要高很多。这里有一个阿姆达尔定律来说明这个事情。关于阿姆达尔定律。

并发编程的影响

很显然,并发编程带来的最大影响是提高了计算效率,充分利用了计算资源(并行也是为了充分利用计算资源)。但同时并发编程也带来了一个巨大的影响,那就是使得编写并发程序变得困难起来,编写正确的程序就已经比较难了,编写正确的并行程序就是难上加难。

编写并发程序为什么更加困难?原因在于并发程序需要处理线程调度,数据共享问题。因为数据共享带来的线程安全问题。

同时,多线程就意味着需要额外的调度逻辑,这多出来的调度会带来线程的上下文切换等损耗。因此多线程不是高性能的良方。恰当地使用多线程,小心翼翼地编写并发程序才是对策。

有时候,我们甚至不能忽略线程切换带来的性能下降,更加不能容忍多线程带来的线程安全导致错误的计算结果问题。

提高计算效率吗

多线程不是高性能的最好办法。可以这样说,多线程的目的在于充分利用计算机硬件资源和软件资源,使其不断处于忙碌状态从而提高生产效率。而高性能的解决办法有时候需要从更加宏观的地方入手。

分布式架构

目前,互联网里分布式架构应该比较流行。因为解决高并发,大流量问题很多时候就是分布式。现在比较流行的微服务也可以理解为更小粒度的SOA。计算机科学里还是现实生活中,我们解决大问题的思路就是拆分。将大问题拆分成小问题,将大模块拆分成小模块。然后利用多实例,多服务来解决。因>此,从宏观层面来看,分布式和并发的思想上有某种契合。都是化大为小,各个击破。

协程

多线程必然带来线程切换的问题。线程切换就会消耗额外的计算资源。因此计算机科学里又有了一种协程的说法。可以这样认为,线程是小粒度的进程,而协程可以理解为小粒度的线程。协程不需要上下文切换,在方法调用里调用其他的方法不需要切换资源。这里的关键在于利用了CPU的中断。因此避>免了线程切换带来的资源消耗。未来,Java必将支持协程。而目前Python已经支持协程了。

Java并发编程的基本手段

实现Java多线程的手段以及基本用法(Thread,Runnable,Callable),static,final等对线程安全的意义

线程安全和线程通信问题

引出临界区,数据竞争以及线程通信模型,深入讲解线程通信的方法(分析ThreadLocal类)以及意义

原子性与可见性

因为多线程通信问题,可能引发原子性和可见性等问题,从Java语言层面讲解原子性和可见性

Java内存模型(JMM)

继续深入,理解JMM,从JMM角度深入理解synchronized和volatile

J.U.C概述

JDK并发工具包介绍,从该包的结构以及功能上做一个概述,为后面的分解铺垫思路

深入J.U.C中的锁

深入分析Java语言层面提供的锁机制以及实现原理,对比分析synchronized的异同和优劣

深入J.U.C中Executor框架

深入分析Java中线程池,比较分析4中线程池的异同以及用法

深入J.U.C中并发容器类

简单分析各个容器类的作用和用法,重点分析ConcurrentHashMap

深入J.U.C中原子类

简单分析各种原子类的用法以及作用,重点分析CAS操作的实现以及底层机制

深入J.U.C中并发工具类

简单分析各个工具类的用法以及作用,重点分析CountDownLatch和CyclicBarrier

并发编程总结

这个思路从简单到复杂,基本是按照并发编程学习的思路来的。当然,各个章节也很独立。可以作为单独的复习或学习参考。希望这个专题能给自己和他人带来知识上的梳理与巩固。