并行的世界
并发简史
早期计算机(不包含操作系统),只能从头到尾执行一个程序,并且执行的程序能访问计算机中的所有资源。
操作系统的出现使得计算机每次能运行多个程序,并且不同程序都在单独的进程中运行:操作系统为各个独立执行的进程分配各种资源,包括内存、文件句柄以及安全证书等。计算机中加入操作系统来实现多个程序的同时执行,主要是基于以下原因:
- 资源利用率(同步等待造成的资源浪费)
- 公平性(通过时间分片实现用户和程序共享计算机资源)
- 便利性(多程序开发可以降低单个程序的开发的复杂度)
串行与并行的区别
好处:可以缩短整个流程的时间
- 串行(5):洗茶具 => 打水 => 烧水(同步等待) => 等水开 => 冲茶
- 并行(4):打水 => 烧水同时洗茶具 => 水开 => 冲茶
可怕的现实:摩尔定律的失效
摩尔定律:当价格不变时,集成电路上可容纳的元器件的数目,约每隔 18-24 个月便会增加一倍,性能也将提升一倍。这一定律揭示了信息技术进步的速度。
然而,在 2004 年,Intel 的 4GHz 芯片宣布推迟到 2005 年,然后再 2004 年的秋季,又再次宣布彻底取消 4GHz 计划。
究其根本,硅电路很有可能已经走到的尽头,制造工艺已经精确到了纳米,1 纳米是 10 的负 9 次方米,也就是十亿分之一米。就目前科技水平而言,如果无法在物质分子层面以下进行工作,那么 4GHz 已经很接近理论的极限值了。因为即使一个水分子,他的直径也只有 0.4 纳米。
顶级计算机科学家唐纳德·尔文·克努斯(Donald Ervin Knuth)说过:在我看来,这种现象(并发)或多或少是由于硬件设计者已经无计可施了导致的,他们将摩尔定律失效的责任推给软件开发者。
光明或黑暗
根据唐纳德的观点,摩尔定律本应该由硬件开发人员维护。但是,很不幸,硬件工程受限于当代科学水平,似乎已经无计可施。然而为了继续保持性能的高速发展,硬件工程师破天荒地想出了将多个 CPU 内核塞进一个 CPU 里的奇妙想法。由此,并行计算就被非常自然地推广开来,随之而来的问题也层出不穷,软件开发人员的黑暗时期也随之到来。简化的硬件设计方案必然带来软件设计的复杂性。软件开发人员正在为硬件工程师无法完成的工作买单。
并发编程的适用场景
- 任务会阻塞线程,导致之后的代码不能执行:比如一边从文件中读取,一边进行大量计算的情况。
- 任务执行时间过长,可以划分为分工明确的子任务:比如分段下载。
- 任务间断性执行:日志打印。
- 任务本身需要协作执行:比如生产者消费者问题。
并行计算相关概念
同步(Synchronous)和异步(Asynchronous)
同步和异步通常用来形容一次方法的调用。
- 同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。
- 异步调用则立刻返回,调用者就可以继续后续的操作。
并发(Concurrency)和并行(Parallelism)
他们都可以表示两个或者多个任务一起执行,但是侧重点有所不同。
- 并发侧重多任务交替执行,而多个任务之间有可能还是串行的。
- 并行是真正意义上的“同步执行”。
临界区
临界区用来表示一种共享资源,可以被多个线程使用。但是每一次只能有一个线程使用它,一旦临界区资源被占用,其他线程想使用临界区就必须等待。
阻塞(Blocking)和非阻塞(Non-Blocking)
阻塞和非阻塞通常用来形容多线程间的相互影响。
- 阻塞:比如一个线程占用了临界区资源,那么其他所有需要这个资源的线程就必须在这个临界区中等待。等待会导致线程挂起,这种情况就是阻塞。此时,如果占用资源的线程一直不愿意释放资源,那么其他所有阻塞在这个临界区上的线程都不能工作,一直处于等待、线程挂起的状态。
- 非阻塞:非阻塞的意思与之相反,它强调没有一个线程可以妨碍其他线程执行,所有的线程都会尝试不断前向执行。
并发编程的挑战之频繁的上下文切换
CPU 为线程分配时间片,时间片非常短(毫秒级别),CPU 不停的切换线程执行,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态,让我们感觉是多个程序同时运行的。
但是上下文的频繁切换,会带来一定的性能开销。并且上下文切换本身是没有意义的(零生产力),如何减少上下文切换的开销?
- 无锁并发编程 :多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的 ID 按照 Hash 算法取模分段,不同的线程处理不同段的数据。
- CAS:Java 的 Atomic 包使用 CAS 算法来更新数据,而不需要加锁。
- 使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态(频繁的上下文切换只会操作 CPU 时间片的浪费)。
- 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。--GO
并发编程的挑战之死锁
死锁示例代码
1/**
2 * 死锁Demo
3 */
4public class DeadLockDemo {
5
6 private static final Object HAIR_A = new Object();
7
8 private static final Object HAIR_B = new Object();
9
10 public static void main(String[] args) {
11
12 new Thread(()->{
13 synchronized (HAIR_A) {
14 //休眠一段时间
15 try {
16 Thread.sleep(50L);
17 } catch (InterruptedException e) {
18 e.printStackTrace();
19 }
20 synchronized (HAIR_B) { //A获取不到B的锁,因为B一直不释放自己持有的锁
21 System.out.println("A成功的抓住B的头发");
22 }
23 }
24 }).start();
25
26 new Thread(()->{
27 synchronized (HAIR_B) {
28 synchronized (HAIR_A) { //B获取不到A的锁,因为A一直不释放自己持有的锁
29 System.out.println("B成功抓到A的头发");
30 }
31 }
32 }).start();
33 }
34}
如何排查死锁方式一
1C:\Users\soulboy>jps
218160 Launcher
318148 Jps
416108 DeadLockDemo
518108
6
7C:\Users\soulboy>jstack 16108
8Found one Java-level deadlock:
9
10=============================
11"Thread-1":
12 waiting to lock monitor 0x000000001cd12168 (object 0x000000076b18a5f0, a java.lang.Object),
13 which is held by "Thread-0"
14"Thread-0":
15 waiting to lock monitor 0x000000001cd13608 (object 0x000000076b18a600, a java.lang.Object),
16 which is held by "Thread-1"
17
18Java stack information for the threads listed above:
19===================================================
20"Thread-1":
21 at com.xdclass.synopsis.DeadLockDemo.lambda$main$1(DeadLockDemo.java:28)
22 - waiting to lock <0x000000076b18a5f0> (a java.lang.Object)
23 - locked <0x000000076b18a600> (a java.lang.Object)
24 at com.xdclass.synopsis.DeadLockDemo$$Lambda$2/1831932724.run(Unknown Source)
25 at java.lang.Thread.run(Thread.java:745)
26"Thread-0":
27 at com.xdclass.synopsis.DeadLockDemo.lambda$main$0(DeadLockDemo.java:20)
28 - waiting to lock <0x000000076b18a600> (a java.lang.Object)
29 - locked <0x000000076b18a5f0> (a java.lang.Object)
30 at com.xdclass.synopsis.DeadLockDemo$$Lambda$1/990368553.run(Unknown Source)
31 at java.lang.Thread.run(Thread.java:745)
32Found 1 deadlock.
如何排查死锁方式二
1C:\Users\soulboy>jconsole
并发编程的挑战之线程安全
不同于死锁,线程安全比较难以定位。线程安全问题通常都是因为多个线程在操作共享的数据产生的。
线程不安全代码示例
1import java.util.concurrent.CountDownLatch;
2
3/**
4 * 线程不安全操作代码实例
5 */
6public class UnSafeThread {
7
8 private static int num = 0;
9
10 private static CountDownLatch countDownLatch = new CountDownLatch(10);
11
12 /**
13 * 每次调用对num进行++操作
14 */
15 public static void inCreate() {
16 num++;
17 }
18
19 public static void main(String[] args) {
20 for (int i = 0; i < 10; i++) {
21 new Thread(()->{ //创建10个线程
22 for (int j = 0; j < 100; j++) { //每个线程调用inCreate方法100次
23 inCreate();
24 try {
25 Thread.sleep(10);
26 } catch (InterruptedException e) {
27 e.printStackTrace();
28 }
29 }
30 //每个线程执行完成之后,调用countDownLatch
31 countDownLatch.countDown();
32 }).start();
33 }
34
35 while (true) {
36 if (countDownLatch.getCount() == 0) { //10个线程都执行完毕才进行打印num
37 System.out.println(num);
38 break;
39 }
40 }
41 }
42}
控制台输出不符合预期的值:1000
1# 每次执行结果都不尽相同,都无法满足预期值
2826
3799