并行的世界
并发简史
早期计算机(不包含操作系统),只能从头到尾执行一个程序,并且执行的程序能访问计算机中的所有资源。
操作系统的出现使得计算机每次能运行多个程序,并且不同程序都在单独的进程中运行:操作系统为各个独立执行的进程分配各种资源,包括内存、文件句柄以及安全证书等。计算机中加入操作系统来实现多个程序的同时执行,主要是基于以下原因:
- 资源利用率(同步等待造成的资源浪费)
- 公平性(通过时间分片实现用户和程序共享计算机资源)
- 便利性(多程序开发可以降低单个程序的开发的复杂度)
串行与并行的区别
好处:可以缩短整个流程的时间
- 串行(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
并发编程的挑战之死锁
死锁示例代码
/**
* 死锁Demo
*/
public class DeadLockDemo {
private static final Object HAIR_A = new Object();
private static final Object HAIR_B = new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (HAIR_A) {
//休眠一段时间
try {
Thread.sleep(50L);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (HAIR_B) { //A获取不到B的锁,因为B一直不释放自己持有的锁
System.out.println("A成功的抓住B的头发");
}
}
}).start();
new Thread(()->{
synchronized (HAIR_B) {
synchronized (HAIR_A) { //B获取不到A的锁,因为A一直不释放自己持有的锁
System.out.println("B成功抓到A的头发");
}
}
}).start();
}
}
如何排查死锁方式一
C:\Users\soulboy>jps
18160 Launcher
18148 Jps
16108 DeadLockDemo
18108
C:\Users\soulboy>jstack 16108
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x000000001cd12168 (object 0x000000076b18a5f0, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x000000001cd13608 (object 0x000000076b18a600, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at com.xdclass.synopsis.DeadLockDemo.lambda$main$1(DeadLockDemo.java:28)
- waiting to lock <0x000000076b18a5f0> (a java.lang.Object)
- locked <0x000000076b18a600> (a java.lang.Object)
at com.xdclass.synopsis.DeadLockDemo$$Lambda$2/1831932724.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
"Thread-0":
at com.xdclass.synopsis.DeadLockDemo.lambda$main$0(DeadLockDemo.java:20)
- waiting to lock <0x000000076b18a600> (a java.lang.Object)
- locked <0x000000076b18a5f0> (a java.lang.Object)
at com.xdclass.synopsis.DeadLockDemo$$Lambda$1/990368553.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
Found 1 deadlock.
如何排查死锁方式二
C:\Users\soulboy>jconsole
并发编程的挑战之线程安全
不同于死锁,线程安全比较难以定位。线程安全问题通常都是因为多个线程在操作共享的数据产生的。
线程不安全代码示例
import java.util.concurrent.CountDownLatch;
/**
* 线程不安全操作代码实例
*/
public class UnSafeThread {
private static int num = 0;
private static CountDownLatch countDownLatch = new CountDownLatch(10);
/**
* 每次调用对num进行++操作
*/
public static void inCreate() {
num++;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{ //创建10个线程
for (int j = 0; j < 100; j++) { //每个线程调用inCreate方法100次
inCreate();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//每个线程执行完成之后,调用countDownLatch
countDownLatch.countDown();
}).start();
}
while (true) {
if (countDownLatch.getCount() == 0) { //10个线程都执行完毕才进行打印num
System.out.println(num);
break;
}
}
}
}
控制台输出不符合预期的值:1000
# 每次执行结果都不尽相同,都无法满足预期值
826
799