弄浪的鱼

我们知道多个线程同时读写同一共享变量会导致并发问题。

一种解决方案是使用 Immutability 模式,如果共享变量在初始化之后就不会改变,只能读取,那么无论多少个线程同时读这个共享变量都不会出现并发问题。比如说 Java 中的 Long、Integer、Short、Byte 等基本数据类型的包装类的实现。

另一种解决方案是突破共享变量,没有共享变量就不会有并发问题。那么如何避免共享呢?思路其实很简单,就是每个线程拥有自己的变量,彼此不共享,就不会有共享问题。

具体来说有两种方法:线程封闭和线程本地存储(ThreadLocal)。

在并发编程中有两个核心问题:一个是互斥,即同一时刻只允许一个线程访问共享资源;另一个是同步,即线程之间如何通信和协作。解决这两个问题的方法前人已经替我们总结出来理论模型,分别是管程模型信号量模型.

MESA管程模型

Java中实现管程有两种方式

  1. 一是使用synchronized关键字给代码块添加隐式锁实现互斥,同时使用notify()notifyAll()实现同步
  2. 二是使用 Java SDK 并发包下的 Lock 和 Condition 两个接口,来实现管程.
    1. Lock 的特性包括:能够响应中断、支持超时和非阻塞地获取锁
    2. Condition 实现了管程模型里面的条件变量。

注:Java 参考了 MESA 模型,语言内置的管程(synchronized)对 MESA 模型进行了精简。MESA 模型中,条件变量可以有多个,Java 语言内置的管程里只有一个条件变量。Java 并发包下的 Lock&Condition则则支持多个条件变量。

在 Java 领域,实现并发程序的主要手段是多线程。线程是操作系统的概念,虽然不同语言对线程操作进行了不同的封装,但是万变不离其综。

通用的线程模型可以用「五态模型」来描述,它们分别是:初始状态,可运行状态,运行状态,休眠状态和终止状态。

通用线程转换图——五态模型

这五种状态在不同编程语言里会有简化合并。例如,C 语言的 POSIX Threads 规范,就把初始状态和可运行状态合并了;Java 语言里则把可运行状态和运行状态合并了,这两个状态在操作系统调度层面有用,而 JVM 层面不关心这两个状态,因为 JVM 把线程调度交给操作系统处理了。

除了简化合并,这五种状态也有可能被细化,比如,Java 语言里就细化了休眠状态。

题图:https://unsplash.com/photos/FjRNGCLnmV0

我们来想象一个场景。现在你是清朝末年账房里的一个先生,张三要给李四转4两银子,你从柜台取过张三的账本,发现李四的账本不在了,于是你想先留着张三的账本,等会儿再去柜台看看李四的账本别人用完没。

没成想,李四想着给张三转5两银子,另一个账房先生和你同时取了李四的账本。你俩想法一样,他留着李四的账本,等着一会儿去拿张三的账本。

现在的局面正是俩人互相「死等」对方的账本,在代码的世界,这种局面就叫做「死锁」。死锁用书面点的方法来说就是:一组互相竞争的线程互相等待,导致「永久」阻塞的现象。

我们知道 CPU 与内存、IO 设备之间的读写速度存在巨大差距,短板理论也告诉我们计算机的性能取决于其性能最差的部分。

为了使内存和 IO 设备能够充分利用 CPU 的性能,同时也要兼顾设备的成本,操作系统和编译器为了做了许多优化,主要有 3 个方面:

  1. CPU 增加缓存来平衡与内存之间速度的差异
  2. 操作系统使用进程与线程,通过分时复用 CPU 来平衡 CPU 和 IO 设备速度的差异
  3. 编译器通过优化指令执行次序,来合理利用 CPU 的缓存

当然,万事万物有利有弊,正式由于这些优化手段,也引入了并发编程的核心问题,即:

  1. 缓存引起的可见性问题
  2. 线程切换引起的原子性问题
  3. 编译优化引起的有序性问题

以上就是并发编程的 3 大核心问题。对于Java并发编程这个话题,我们主要研究的就是怎么解决这 3 个问题。

解决的思路说来也不难,就是按需禁用缓存和编译优化。在我们需要的时候,程序员指定程序禁用缓存和编译优化就可以提升程序的性能。

这就是我们本文的重点Java内存模型。

Java 内存模型

Java 内存模型规范了 JVM 实现按需禁用缓存和编译优化的方法。其主要包括volatilesynchronized final 三个关键字,以及 6 项 Happens-Before 规则。

并发编程的优缺点

Java 程序从 main() 函数开始开始执行,就是启动一个名称为 main 的线程。在程序顺序执行的过程中,看似没有其他线程参与,实际上有许多线程和 main 线程一起执行着。

Java 天生就是多线程的,为什么 Java 会设计成多线程的呢?并发编程是不是有它优点?然而事物都是有两面性的,并发编程的缺点是什么?应该怎么避免?

计算机发明之初,单个 CPU 在同一时间点只能执行单个任务,也就是单任务阶段。紧接着发展为多任务阶段,在单个 CPU 上能执行多个进程,但是此处的并行执行并不是指在同一时间执行多个任务,而是指由系统对进程进行调度轮流使用 CPU。

接着发展到现在的多线程阶段,一个程序内部能够运行多个线程,每个线程都可以被看做运行在一个 CPU 上,此时计算机真正做到了在同一时间点能够执行多个任务。