趣谈并发 2:认识并发编程的利与弊

2,013 阅读7分钟

读完本文你将了解:

从上篇文章 趣谈并发(1):全面认识 Thread 我们了解了 Java 中线程的基本概念和关键方法。

在开始使用线程之前,我觉得我们有必要先了解下多线程给我们带来的好处与可能造成的损失,这样才能在合适的地方选用合适的并发策略。

多线程的优点

shixinzhang

1:提高资源利用率

“一口多用”其实就是一种多线程。

想象一下,我们左手拿着海鲜大狂欢披萨,右手拿着意式面包佐芦笋腊肉肠,桌子上还放着青柠香茅饮,左边吃一口,右边咬一块,再使劲地喝一口,啊!此生无憾!

看到了吧,多线程最大的优点就是:提高资源利用率

在 PC 或者手机中,我们的资源主要说的就是 CPU。

我们知道,通常情况下,网络和磁盘的 I/O 比 CPU 和内存的 IO 慢的多。

在执行频繁 I/O 的任务时,CPU 很多时候都处于闲置状态。这时如果我们开启多个线程,在 A 线程 I/O 的同时让 CPU 执行 B,在 B 线程 I/O 的同时再执行 A。这样就比 A B 串行执行时 CPU 的利用率更高。

2:响应更快

这一点想必小肉深有感悟:

  • 家里快递来了,小肉会说:shixin,去取一下。我下去愚公移山的时候,她可以继续 shopping;
  • 窗外有人吼卖樱桃喽,小肉会说:shixin,去买一点。我去夸父逐日的时候,她可以继续吃吃吃。

我们在主线程接受用户请求后,将耗时操作交给子线程,然后告诉用户在等待的同时还可以干点别的。

此外将一些可以拆分的任务分给多个线程执行,执行完毕后再合并结果,也会让任务处理更高效。

多线程的缺点

俗话说:有阳光的地方就有黑暗;
俗话说:世界上没有免费的午餐。

线程能够给我们带来以上好处,是需要一定代价的。

1:增加资源消耗

每个线程都拥有各自的计数器、堆栈、局部变量等资源,同时管理这些线程也需要额外的资源。

2:上下文切换的开销

当 CPU 调度不同线程时,它需要更新当前执行线程的数据,程序指针,以及下一个线程的相关信息。

这种切换会有额外的时间、空间消耗,我们在开发中应该避免频繁的线程切换。

3:设计、编码、测试的复杂度增加

其实第三点才是关键,我们知道公司人数越多问题越多,线程也一样,线程之间的交互非常复杂。

不正确的线程同步只有运行时才能发现问题,而且非常难以重现,发现并修复复杂度大大增加。

Java 内存模型与 CPU 内存简介

在了解多个线程同时访问数据可能出现的问题之前,我们需要先了解 Java 内存模型

Java 内存模型规范了 Java 虚拟机与计算机内存是如何协同工作的。

shixinzhang

Java 内存模型中将 JVM 分为堆和栈:

  • 堆为同一个 JVM 中所有线程共享,存放运行时创建的对象和数组数据;
  • 栈为每个线程独有,栈中存放了当前方法的调用信息以及基本数据类型和引用类型的数据。

shixinzhang

Java 中的堆

堆在虚拟机启动时创建,堆占用的内存由垃圾回收器管理,不需要我们手动回收。

JVM 没有规定死必须使用哪一种内存回收机制,不同的虚拟机实现可以使用不同的回收算法

堆中包含在 Java 程序中创建的所有对象,无论是哪一个线程创建的。

一个对象的成员变量随着这个对象自身存放在堆上。不管这个成员变量是基本类型还是引用类型。

静态成员变量跟随着类定义一起也存放在堆上。

Java 中的栈

栈在线程创建时创建,它和 C 语言中的栈相似,在一个方法中,你创建的局部变量和部分结果都会保存在栈中,并在方法调用和返回中起作用。

当前栈只对当前线程可见。即使两个线程执行同样的代码,这两个线程仍然会在自己的线程栈中创建一份本地副本。

因此,每个线程拥有每个本地变量的独有版本。

栈中保存方法调用栈、基本类型的数据、以及对象的引用。

计算机中的内存、寄存器、缓存

这部分摘自:ifeve.com/java-memory…

一个现代计算机通常由两个或者多个 CPU,每个 CPU 都包含一系列的寄存器,CPU 在寄存器上执行操作的速度远大于在主存上执行的速度。

每个 CPU 可能还有一个 CPU 缓存层。CPU 访问缓存层的速度快于访问主存的速度,但通常比访问内部寄存器的速度还要慢一点。

  • 通常情况下,当一个 CPU 需要读取主存时,它会将主存的部分读到 CPU 缓存中。它甚至可能将缓存中的部分内容读到它的内部寄存器中,然后在寄存器中执行操作。

  • 当 CPU 需要将结果写回到主存中去时,它会将内部寄存器的值刷新到缓存中,然后在某个时间点将值刷新回主存。

这里先简单地对“Java 内存模型”进行介绍,后序介绍完常见并发类后再详细总结。

多线程可能出现的问题

通过上述介绍,我们可以知道,如果多个线程共享一个对象,每个线程在自己的栈中会有对象的副本。

如果线程 A 对对象中的某个变量进行修改后还没来得及写回主存,线程 B 也对该变量进行了修改,那最后刷新回主内存后的值一定和期望的值不一致。

就好比拭心和小翔同时开发同一模块代码,拭心下笔如有神不一会儿搞定了注册登录并且提交,小翔没有从服务器拉代码就蒙头狂写,最后一 pull 代码,就会发现自己写的好多都跟服务器上的冲突了!

竞态条件与临界区

当多个线程操作同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。

在临界区中使用适当的同步就可以避免竞态条件,比如 synchronized, 显式锁和原子操作类等。

内存可见性

拭心写的代码小翔无法立即看到,这就是所谓的“内存可见性”问题。

为了让线程 A 对变量做的修改线程 B 立即可以看到,我们可以使用 volatile 修饰变量或者对修改操作使用同步。

总结

本篇文章结合 Java 内存模型简单介绍了多线程开发的优点与可能导致的问题,犹豫了一下我还是觉得有必要在开始学习 Java 各种并发 API 之前了解它们出现的背景,这样更容易明白它们解决了什么问题。

知道了多线程的开销与可能带来的问题后,我们在开发中不要为了使用多线程而使用多线程。应该在确认多线程给项目带来的好处比隐含的开销更多时,再使用多线程。

Thanks

en.wikipedia.org/wiki/Java_m…
en.wikipedia.org/wiki/Java_c…
tutorials.jenkov.com/java-concur…
ifeve.com/java-concur…
www.zhihu.com/question/29…