Java之离弦之箭 - 高并发

一个系统如果足够稳定,而且客户对它也很满意,我们一般没有去优化它的必要,但如果随着用户量增加,系统变的缓慢,我们就有优化它的必要性了. 借用我前面博文《一种可以衡量事物的指标-快和好》,一个系统追求的目标之一是快,但快的前提的是
好,也就是稳,否则步伐太大了就容易扯着蛋了. 优化系统是大的话题,这里只讨论其中的一个小领域-高并发.
任何一个计算机的问题,都能从现实中找到雏形和模型,高并发也不例外。高并发的内容还是比较多的,所以不同类型的高并发问题需要不同的角度,就好像作战的时候,战斗机需要在高空1万米,也需要突然下降到高空1千米,也有可能是潜艇下沉到水下几千米配合作战. 这种思维模型我给它取了个名字叫“海陆空”模型.

  • 什么是并发?
  • 为什么需要并发?
  • 如何使用并发?
  • 并发的身影
  • 并发的未来?

什么是并发?

从现实的角度,并发像什么?

在现实世界里,我们做事是有顺序的,比如做完了事情1,再做事情2, 也有可能是没有顺序的, 事情1和事情2是可以同时进行的. 所以我们在这里看到两个特征

  • 事情是可以拆解的
  • 从人的观察者角度,在一个时间窗口(t0-t1)内,事情是同时进行的.
    在这里,我没有没有提到任何程序领域的术语,换句话说,上面的这两个特征在管理学是常见的,这两个特征可以认为是并发的雏形.
    本文仅仅讨论狭义上的并发:基于线程的多任务。

从编程的角度,并发要解决的核心问题是什么?

并发要解决的核心问题是

  • 分解 - 如果要对一件事情进行并发处理,那么这件事情最起码是可以分解的, 如果这件事情不能分解,那么并发无从谈起.
  • 协作 - 这个问题不具备必然性,仅仅发生在事情1和事件2有关系的前提下,比如事情2依赖事件1的结果
  • 互斥 - 处理事件1和事件2的时候需要访问同一个资源,如果保证访问资源的时候只能自己访问,而且要保证系统的整体的利用率?
    以上三个问题可以说是层层递进的, 有了分解才有可能协作和互斥.
    这就是任何编程领域,并发要解决的所有问题种类吗?答案是肯定的.
    但有人可能很疑惑,并发不是会牵涉到什么自旋锁,可重入锁,分片还有什么volatile么?
    哦,不急,因为现在战斗机还在天上1万米的高空巡逻,遇到特定的问题和特定的目标慢慢会下降的.
    也可以想象这是一部小说,这部小说有三条主线,这三条主线最终会汇聚到一起的.

在开发者眼里,并发的目的和愿景是:用一种符合开发者认知模型的方式来解决上述三个问题. 换句话说,就是认知成本少点。 为什么这件事情很重要?

  • 我们开发软件是属于工程领域而不是科学领域,所以安全可控是首要目标,要是谁写个高并发,写的很炫,但很难维护,很难测试,最终会让人抓狂.
  • 人是习惯的产物,就比如做西红柿炒蛋这个菜,我们知道放完鸡蛋会放西红柿,而不是黄瓜. 所以高并发的编写应该符合人的认知模型.

并发遇到的阻碍是什么?

并发要解决的问题都是有实际意义的事情,看起来很美好。就好比我要成为富豪一样,这件事情本身没有错,但容易吗?不容易,因为资源有限,能力有限.
并发要落地,还得需要计算机来帮忙,所以得看看计算机能不能让我很容易的达到并发这个目标,最好是我说一句话就能达到并发的目的.
一个基本事实:CPU, 内存, I/O的速度不匹配. 根据木桶原理,一个系统的瓶颈取决于最短的那块木板.

姚明在休斯敦的时候,战术基本上是围绕着姚明来打造的,后来麦迪加盟火箭,管理层对麦迪的要求是球队的大战略是必须等姚明落位之后才能开始进攻,麦迪只能同意这种战略。火箭的慢的原因是在于姚明。无奈啊,黄种人跑不动。

现在基本事实已经确定了,可以说这些基本事实是原罪,是障碍的起点。可能有人要问,为什么不能把 CPU, 内存, I/O的速度弄成一样呢?这个从硬件上来说真不好弄.
既然从硬件层面上解决不了这个问题,那就从设计和策略的角度上来解决. 计算机系统的参与方都开始大展拳脚.
一个重要的规律: 任何的方案有好处,也有代价,这个代价可以理解为成本或者副作用,应了那句天下没有免费的午餐.

  • 计算机体系结构的策略 - 增加 CPU 缓存,平衡CPU和内存之间的速度差异
    • 在单核时代,只有一个CPU缓存,CPU缓存和内存之间的一致性很容易解决。一个线程更改了CPU的缓存,另外一个线程是可以知道这个CPU缓存被更改了的. 这个知道就叫做“可见性”.
    • 在多核时代, 有多个CPU缓存,线程1操作 CPU1缓存,线程2有可能操作的是CPU2缓存。这个时候线程2就不知道线程1的操作结果了.
  • 操作系统的策略 - 增加进程,线程,平衡 CPU和 I/O之间的速度差异
    • 我们之所以能一边听音乐,一边发微信,是因为操作系统多进程的功劳,换句话说操作系统定义了时间片这个概念,比如在最开始的10毫秒把CPU的资源分给进程A,接下来的10毫秒把CPU的资源分给进程B, 这样能充分利用CPU资源(充分利用资源这是一种政治正确,谁会鼓励说浪费资源呢?),给用户的感觉就是同时做两件事情.
    • 正如去医院看病的时候,每个人都拿了一些号,叫到张三,张三就进去会诊,在会诊的时候,医生要让张三去抽血,才能得出完整的会诊结论,在张三出去抽血的时候,医生在干嘛呢?在等张三吗?显然不是. 可继续为李四会诊,等张三抽血完毕,再为张三会诊. 在这里可以看到, CPU和医生的角色有点类似.
    • 但这样的策略会带来的一些问题,从操作的角度来说,会有任务切换这个概念. 举个例子, count = count + 1 是语言层面的代码,这个代码在CPU的角度看来是三个指令, 所谓的任务切换是CPU指令级别的切换,而不是语言层面的切换. 所以我们希望能有一个策略保证任务切换的效果是基于语言层面.
  • 编译器的策略 - 优化指令的执行顺序
    • 编译器会对一些代码的执行顺序进行优化, “a=6;b=7”优化之后可能变为”b=7;a=6;”, 这个代码看上去正常, 但在某些例子上却有意想不到的情况,比如双重加锁的单例模式,会引发空指针异常.

为什么需要并发?

系统是要追求快,在快的基础上追求好. 换一种说法,如何让系统在单位时间内执行更多的任务?

如何使用并发?

整体设计原则

大体有四个原则,层层递进, 不局限于Java, 是跨语言的, 这里的原则也可以类比一个国家的宪法或者党的纲领.

  1. 不要使用并发,如果要使用高并发, 必须要有很强的理由.
  2. 如果确实要使用并发,尽可能不要有共享变量
  3. 如果确实需要共享变量,那么尽可能保证变量是只读的
  4. 如果不能保证变量是只读的,那么要确保变量访问的时候要同步。基本上就是讨论各种各样的锁.

整体设计策略

这里的设计策略存在的前提是使用了高并发,也是应用了上面的原则2, 原则3, 原则4.

  • 分解
  • 同步
  • 互斥
    这里的整体设计策略和前面提到的并发要解决的核心问题是一致的, 这也是跨语言的

    整体设计模式

  • Immutability
  • Copy-on-Write
  • ThreadLocal
  • Guarded Suspension
  • Balking
  • Thread-Per-Message
  • Worker Thread
  • 两阶段终止
  • 生产者消费者
  • Actor
  • 软件事务内存
  • 协程
  • CSP

方法

  • Java是如何解决并发路上的障碍的?
    • 可见性和顺序问题
      • final
        • 表明这个变量是不可变的
      • volatile
        • volatile int x = 0, 告诉编译器对x的读写不要用CPU缓存
        • 在JDK1.5以前,忽略了多核的情况,在JDK1.5对volatile进行了增强.
      • synchronized
      • 8个Happens-Before原则
        • 程序的顺序性规则
        • Volatile变量规则
        • 传递性
        • 管程中锁的规则 - 对一个锁的解锁 Happens-Before 于后续对这个锁的加锁
        • 线程Start规则
        • 线程Join规则
        • 线程中断规则
        • 对象终结规则
      • 小结
        • 可见性和顺序性问题主要由Java内存模型的规范来解决
        • A Happens-Before B 意味这 A做了某件事情B是知道的。
    • 原子性问题
      • 原子性问题是由CPU线程切换引起的,所以禁止CPU线程切换是一个很自然的思路,要禁止CPU线程切换就要禁止中断,但禁止中断这种做法只在单核CPU上有效
      • 互斥就是要保证同一时刻只有一个线程执行
        • synchronized是Java对管程的一种实现
          • 一把锁可以锁住多个资源
  • 如何实现等待通知机制?
    • synchronized 配合 wait()、notify()、notifyAll()
  • Lock和Condition
  • Semaphore
  • ReadWriteLock
  • StampedLock
  • CountDownLatch和CyclicBarrier

其他

    • 一把锁可以锁住多个资源
      • 资源和资源之间有协作关系
    • 用不同的锁对受保护资源进行精细化管理,能够提升性能,可能的代价就是死锁。
      • 资源和资源之间没有协作关系
    • 多把锁不可以锁住一个资源
    • 用锁两大要素:锁定的对象和锁定的资源.

并发的身影

并发的未来