Logo
Section 1 / 15

简介

本课详细介绍了线程存在的原因及其带来的优势,同时探讨了使用线程所带来的问题。

导入

简介

本课详细介绍了线程存在的原因及其带来的优势,同时探讨了使用线程所带来的问题。

并发有什么用?

理解线程的工作原理以及掌握并发编程的基本原则,能够体现一名开发人员的技术成熟度和深度,这在求职时可能成为获得更高级别职位的关键优势。首先,我们需要弄清楚为何存在线程模型,以及它们能带来哪些好处。

线程和大多数计算机科学概念一样,并非物理实体。我们最接近直观感受线程的方式是在调试器中观察它们。下图展示了程序在调试器中被暂停时的各个线程状态。

...
调试器中的暂停线程

一个简单的并发系统例子是:一台单处理器机器上运行着你常用的IDE。假设你编辑了一个代码文件并点击保存,这个操作会触发一个工作流程,导致数据写入底层物理磁盘。然而I/O操作是相对耗时的,在数据写入磁盘的过程中,CPU将处于空闲状态。

在此期间,空闲的CPU本可以执行其他有用的任务——这正是线程发挥作用的地方。此时,I/O线程会被切换出去,而UI线程会被调度到CPU上运行,这样当你再次点击界面其他位置时,IDE仍然响应迅速,不会显得卡顿或冻结。

线程能够在任意时刻仅由一个CPU执行的前提下,营造出多任务并行的假象。每个线程在CPU上获得一段执行时间片,随后被切换出去——要么是因为它启动了需要等待且不占用CPU的任务(如I/O),要么是因为其时间片已用完。线程调度的具体机制更为复杂精细,但我们上述描述构成了其基本原理。

随着硬件技术的发展,如今多核处理器已十分普遍。应用程序可以充分利用这种架构,让不同的CPU核心各自独立地运行一个线程。

线程的优势

  1. 更高的吞吐量。尽管在某些极端情况下,线程间上下文切换的开销可能会抵消甚至超过性能提升,导致表现不如单线程场景,但这类情况属于例外而非普遍现象。

  2. 使应用程序更具响应性,营造出多任务处理的体验。

  3. 资源利用更高效。需要注意的是,创建线程比启动全新的进程要轻量得多。使用线程而非为每个Web请求创建新进程的服务器,能显著减少系统资源消耗。

其他所有多线程的好处,本质上都是上述几点的延伸或间接结果。

通过多线程提升性能

以下是一个具体的示例。任务目标是 计算从0到Integer.MAX_VALUE之间所有整数的总和。第一种情况使用单个线程完成累加;第二种情况则将范围分为两部分,分别由两个线程独立求和,最后将两个部分和相加以得到最终结果。我们测量两种情况的执行时间并输出。

1class Demonstration { 2 public static void main( String args[] ) throws InterruptedException { 3 SumUpExample.runTest(); 4 } 5} 6 7class SumUpExample { 8 9 long startRange; 10 long endRange; 11 long counter = 0; 12 static long MAX_NUM = Integer.MAX_VALUE; 13 14 public SumUpExample(long startRange, long endRange) { 15 this.startRange = startRange; 16 this.endRange = endRange; 17 } 18 19 public void add() { 20 21 for (long i = startRange; i <= endRange; i++) { 22 counter += i; 23 } 24 } 25 26 static public void twoThreads() throws InterruptedException { 27 28 long start = System.currentTimeMillis(); 29 SumUpExample s1 = new SumUpExample(1, MAX_NUM / 2); 30 SumUpExample s2 = new SumUpExample(1 + (MAX_NUM / 2), MAX_NUM); 31 32 Thread t1 = new Thread(() -> { 33 s1.add(); 34 }); 35 36 Thread t2 = new Thread(() -> { 37 s2.add(); 38 }); 39 40 t1.start(); 41 t2.start(); 42 43 t1.join(); 44 t2.join(); 45 46 long finalCount = s1.counter + s2.counter; 47 long end = System.currentTimeMillis(); 48 System.out.println("双线程最终计数 = " + finalCount + " 耗时 " + (end - start)); 49 } 50 51 static public void oneThread() { 52 53 long start = System.currentTimeMillis(); 54 SumUpExample s = new SumUpExample(1, MAX_NUM ); 55 s.add(); 56 long end = System.currentTimeMillis(); 57 System.out.println("单线程最终计数 = " + s.counter + " 耗时 " + (end - start)); 58 } 59 60 61 public static void runTest() throws InterruptedException { 62 63 oneThread(); 64 twoThreads(); 65 66 } 67}

在我的测试中,双线程版本耗时约 652毫秒,而单线程版本耗时约 886毫秒。你可能会看到略有不同的数值,但通常双线程所用时间总是少于单线程。性能提升的幅度取决于可用CPU核心数量以及问题本身的特性。不过,也总会有一些问题不适合采用多线程方式解决,反而用单线程处理更加高效。

线程带来的问题

然而,正如那句老话所说:天下没有免费的午餐。使用线程的代价主要体现在以下几个方面:

  1. 通常很难发现的缺陷,有些错误可能只会在生产环境中偶然出现
  2. 更高的代码维护成本,因为代码逻辑本身变得更难理解和推理
  3. 系统资源消耗增加。每个线程的创建都会额外占用内存,CPU需花费周期进行管理,频繁的上下文切换也会浪费时间
  4. 程序可能出现性能下降,因为线程之间的协调是有代价的。获取和释放锁会增加执行时间,多个线程争抢锁还会引发锁竞争

了解了这些背景后,接下来我们将深入探讨并发编程的更多细节,这些内容很可能会在技术面试中被重点考察。

程序 vs 进程 vs 线程