一、并发编程的挑战

1 上下文切换

并发是单核处理器支持多线程执行代码。则 CPU 通过给每个线程分配时间片来实现,线程任务执行的切换,由于在切换 前要保存当前任务的状态,因此从保存再到加载的过程就是一次上下文切换。

1.1 多线程一定快吗?

以下为对比:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class ConcurrencyTest {
private static final long count = 100000001;
public static void main(String[] args) throws InterruptedException {
concurrenct();
serial();
}
private static void concurrenct() throws InterruptedException {
long start = System.currentTimeMillis();
Thread thread = new Thread(new Runnable() {
@Override public void run() {
int a = 0;
for (long i = 0; i < count; i++) {
a += 5;
}
}
});
thread.start();
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
long time = System.currentTimeMillis() - start;
thread.join();
System.out.println("concurrency: " + time + "ms, b = " + b);
}
private static void serial() {
long start = System.currentTimeMillis();
int a = 0;
for (long i = 0; i < count; i++) {
a += 5;
}
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
long time = System.currentTimeMillis() - start;
System.out.println("serial: " + time + "ms, b = " + b);
}
}
循环次数 serial/ms concurrent/ms
一亿 91 51
一千万 14 10
一百万 5 5
十万 2 3
一万 0 0

1.2 如何减少上下文切换

无锁并发编程:多线程竞争锁时,会引起上下文切换。因此 Hash 算法通过取模分段,不同的线程处理不同段的数据。
CAS 算法: Atomic 包使用 CAS 算法来更新数据,而不需要加锁。
协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

2. 死锁

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
26
27
28
29
30
31
32
33
34
public class DeadLockDemo {
private static String A = "A";
private static String B = "B";
public static void main(String[] args) {
new DeadLockDemo().deadLock();
}
private void deadLock() {
Thread t1 = new Thread(new Runnable() {
@Override public void run() {
synchronized (A) {
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B) {
System.out.println("1");
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override public void run() {
synchronized (B) {
synchronized (A) {
System.out.println("2");
}
}
}
});
t1.start();
t2.start();
}
}

这段代码的死锁在于,t1拿到锁之后,因为一些异常没有释放锁(或者死循环)。又或者是t1拿到一个数据库锁,释放锁的时候抛出了异常,没有释放掉。 导致t2一直等待t1释放锁。
而避免死锁常见方法有:

避免一个线程同时获取多个锁。
避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
尝试使用定时锁,使用 lock.tryLock(timeout) 来替代使用内部锁机制。
对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

3 资源限制的挑战

3.1 什么是资源限制

资源限制是指,程序的执行速度受限于计算机硬件资源或软件资源。例如,服务器的宽带只有 2MB/s ,某个资源的下载速度是 1MB/s , 系统启动 10 个线程下载资源,下载速度会不会变成 10MB/s ,所以在进行并发编程时,要考虑资源的限制。而硬件资源限制有带宽的上传/下载速度、 硬盘读写速度和 CPU 的处理速度,软件资源限制有数据库的连接数和 socket 连接数等。

3.2 资源限制引发的问题

在并发编程中,将代码执行速度加快的原则是将代码中串行执行的部分编程并发执行,但是受限于资源,仍然在串行执行,这时候执行的会更慢, 因为增加了上下文切换和资源调度的时间。例如,之前看到一段程序使用多线程在办公网并发的下载和处理数据,导致 CPU 使用率达到 100% , 几个小时都不能运行完成任务,后来修改成单线程,一个小时就执行完成了。

3.3 如何解决资源限制的问题

对于硬件资源限制,可以考虑使用集群并行执行程序。既然单机的资源有限制,那么就让程序在多机运行。不同的机器处理不同的数据。

对于软件资源限制,可以考虑使用资源池将资源复用。例如使用连接池数据库和 Socket 连接复用,或者在调用对方 webservice 接口获取数据时, 只建立一个连接。

4 本章小结

本章的并发程序不够严谨,但是够入门,笔者建议多实用 JDK 并发包提供的并发容器和工具类来解决并发问题,因为这些类都已经通过了充分的测试和优化。