因为各个语言之间的内存模型并不相同,所以本篇博文不讨论多个线程之间资源同步的问题


单核单线程

  小秦同学在自动化路开了一家餐厅,自动化路的管理员贴心的为小秦准备了一个机器人厨师(当然是收费的)帮他做饭。小秦非常高兴的就把机器人安置在了厨房中,并且让他开始工作。

  但是这个机器人显然不是很聪明,它在工作的时候只会严格按照预先指定好的程序进行,无论中间出现了什么问题。

  假如一个客户下了订单:

  1. 米饭
  2. 番茄炒鸡蛋

  机器人就会严格按照菜单顺序执行,先把米饭放进电饭煲中,等待米饭蒸好后把米饭送给服务员,然后继续做番茄炒鸡蛋。

点击查看大图

  在刚开始,这个问题还不明显,但是小秦餐厅的菜单越来越繁杂,出现了一些比较耗时的菜品。此时机器人仍然按照先前的规则进行工作,这时候就出现了很严重的问题:

  1. 可能菜还没做好米饭就凉了
  2. 上饭和上菜的时间间隔容易让客户不满

原理

  单核单线程的原理十分的简单易懂,就是CPU严格按照指令顺序(可能会和程序的指令顺序不一样)执行代码。

单核多线程

  这时候小秦找到了街道的管理员,想让他帮助自己解决问题。管理员非常爽快的帮小秦升级了机器人的程序,现在机器人变聪明了,它会在做一个任务的间隙去做其它任务。

  说白了就是在把米蒸上后,它不会一直等待到米蒸好,而是在蒸米的过程中把菜做好。

点击查看大图

  这样子就节省了很多时间。

原理

  单核多线程是如何实现的呢?其实就是系统给每一个进程分配一定的执行时间,按照一定顺序执行各个进程的指令,时间到了就保存需要的信息然后切换到下一个线程,以此往复。

多核多线程

  经过一段时间的苦心经营,小秦的顾客越来越多。现在,聪明的机器人也无法应对如此庞大的客户量了。于是乎,小秦再次找到了管理员帮忙。

  管理员原本想继续给小秦的机器人升级,提升它的效率,但是成本太高了,小秦无法承受。两人协商后选择了另一种方案:租用更多的机器人。

  现在,假如有如下订单:

  1. 米饭
  2. 番茄炒鸡蛋
  3. 千叶豆腐
  4. 糖醋鱼

  那么小秦的厨房会按下图所示的顺序工作:

点击查看大图

  显而易见,多核多线程相比于单核多线程速度快了很多。因为单核多线程是一个人在干活而多核多线程是多个人在干活,一个人肯定是没办法同时做两件事,但是多个人却可以。

原理

  多核多线程就是多个CPU核心(可以把一个核心看为一个独立的CPU)同时工作,同时一个核心还会处理多个进程。

  可以发现,单核多线程是无法实现真正的并行执行的,只是让人看起来好像是几个代码同时在执行,多核多线程才能做到真正的并行。

资源竞争

  多线程一定比单线程快吗?答案是否定的,在某些情况下,单线程会比多线程更快。抛开任务类型不谈,影响多线程性能的最大因素便是资源竞争。

  那么什么是资源竞争呢?我们同样用厨房举例子:

  现在厨师在做两个饭:米饭和粥。这两个饭理论上是可以同时进行的,但是这两个饭都需要用到同一种锅,并且厨房只有一口锅。那么这两个任务就变得无法并行,其中一个要等待另一个执行完毕后才可以执行。

  上面这个例子中是两个线程在“争夺一口锅”,实际上两个线程可以争夺的资源有很多,锅碗瓢盆没有一个是可以不竞争的。

线程锁

  不过机器人可没有那么聪明,不管有没有其它机器人在使用它要用的器械,它都会直接拿起来用。这就不可避免地会造成混乱,用代码的例子来讲,最常见的就是如下情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int value = 0;

public static main(String[] args) throws Exception {
List<Thread> list = new ArrayList<>(5);
for (int i = 0; i != 5; ++i) {
Thread thread = new Thread() {
@Override
public void run() {
for (int i = 0; i != 10000; ++i) {
++value;
}
}
};
thread.start();
list.add(thread);
}
for (Thread thread : list) thread.join();
System.out.println(value);
}

  这个代码最终会输出什么是无法预知的,因为当其中一个线程对value进行操作时,value可能已经被其它线程修改了。用厨房举例子就是厨师A正在用锅做番茄炒鸡蛋,这时候厨师B突然把锅拿走去做糖醋鱼,然后厨师C又把锅抢走去做千叶豆腐,就这样争来抢去,最终做出来的菜肯定是一塌糊涂。

  为了解决这个问题,线程锁的作用就凸显出来了。顾名思义,线程锁就是将资源锁住,这样自己使用资源的时候其他人就拿不走了。也就是说,厨师A在做番茄炒鸡蛋的时候厨师B和C没办法拿走锅。

  上面的代码加上锁就变成了下面这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Main {

static int value = 0;

public static void main(String[] args) throws Exception {
List<Thread> list = new ArrayList<>(5);
for (int i = 0; i != 5; ++i) {
Thread thread = new Thread() {
@Override
public void run() {
for (int i = 0; i != 10000; ++i) {
synchronized (Main.class) {
++value;
}
}
}
};
thread.start();
list.add(thread);
}
for (Thread thread : list) thread.join();
System.out.println(value);
}

}

  上面这个代码的运行结果就恒为50000了,不过这样子的多线程显然是没有单线程速度更快的。

小结

  上面所说的只是多线程的表面,写这些只是为了让读者明白多线程是什么东西,运行起来大概长什么样子。真正想要了解并熟练运用多线程,不可避免地要去了解对应语言的内存模型、API接口等内容。

  总而言之,如果不怎么会用多线程,写代码的时候就不要乱用。很多时候,错误的使用多线程非但不能提高代码执行效率,反而会让速度更加拉跨。