欢迎关注我的公众号:观知小阁。包含各种类的文章,内容更丰富,更新及时且不迷路。
并发编程是现代软件开发的基石,也是区分初级和高级程序员的重要标尺。
在现代软件开发中,并发编程是提高系统性能、利用多核处理器能力的关键技术。
无论是Web服务器、数据库系统还是分布式计算,都离不开并发编程。但并发编程也是一把双刃剑,在提升性能的同时,也带来了复杂性问题。
今天,我们将深入探讨并发编程中的相关概念,来构建并发相关的理论基础。
1. 同步(Synchronous)和异步(Asynchronous):理解调用方式的本质区别
同步和异步通常用来形容一次方法调用。
同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。这就好比你去商场买空调,向售货员下单后,在商店里等着,直到商家把空调配送安装完成,你们一起回家。这个过程是顺序执行的,你必须等待前一个步骤完成后才能进行下一个步骤。
异步方法调用则更像消息传递,一旦开始,方法调用就会立即返回,调用者可以继续后续的操作。异步方法通常会在另外一个线程中"真实"地执行。这就像网上购物:你完成在线支付后,购物过程对你来说就结束了,你可以去做其他事情,等快递送货上门即可。

在编程中,同步调用像是普通函数调用,而异步调用通常通过回调、Future/Promise等机制实现。.NET中的async/await关键字和Java中的Future/Callable接口都是异步编程的典型实现。
2. 并发(Concurrency)和并行(Parallelism):细微差别背后的重大区别
并发和并行是两个最易混淆的概念,但它们有本质区别。
并发指的是在一段时间内,多个任务交替执行,从宏观上看像是"同时"执行,但微观上还是分时交替执行。这好比大家排队在一个咖啡机上接咖啡:虽然只有一个咖啡机,但通过交替使用,多个人都能接到咖啡。

并行是真正意义上的"同时执行",需要多个执行单元。如同有两台咖啡机,两个人可以同时接咖啡,互不干扰。

并发关乎程序结构设计,是一种能力;而并行关乎程序执行,是一种状态。
在单核处理器系统中,只能实现并发;而在多核系统中,既可以并发也可以并行。并发编程的价值在于,它既能提高资源利用率,又能让程序响应更快,还能更自然地建模某些问题域。
3. 临界区:共享资源的保护屏障
临界区表示一种公共资源或共享数据,可以被多个线程使用,但每次只能由一个线程使用。
当临界区资源被占用时,其他线程必须等待。比如办公室里只有一台打印机,如果张三正在使用它打印文件,李四就必须等待张三完成后才能使用。
临界区的典型特点是:
-
互斥访问:一次只能有一个线程进入临界区
-
有限等待:任何线程等待进入临界区的时间必须是有限的
-
无优先权限制:不能假设线程调度的顺序和速度
实现临界区保护的技术包括锁、信号量、互斥量等。在Java中,可以使用synchronized关键字;在C++中,可以使用std::mutex;在C#中,可以使用lock关键字。
4. 阻塞(Blocking)和非阻塞(Non-Blocking):线程的等待策略
阻塞和非阻塞形容线程间的相互影响。
阻塞指一个线程占用了临界区资源,导致其他需要该资源的线程必须等待,这些等待的线程会被挂起。如果占用资源的线程长时间不释放资源,其他线程将一直无法工作。
非阻塞强调没有线程会妨碍其他线程执行,所有线程都会尝试不断向前执行。非阻塞算法通常使用原子操作(如CAS)实现,避免了线程挂起和唤醒的开销。
阻塞操作简单直观,但可能导致性能问题和死锁;非阻塞操作性能更好,但实现复杂,需要处理竞争条件和重试逻辑。
5. 死锁(Deadlock)、饥饿(Starvation)和活锁(Livelock):并发编程的三大陷阱
5.1 死锁
死锁指两个或多个线程互相持有对方所需的资源,导致所有线程都无法继续执行。就像两辆车在十字路口相遇,每辆车都被另一辆车挡住了去路,谁也不愿后退,形成僵局。

死锁发生的四个必要条件:
-
互斥条件:资源一次只能被一个线程使用
-
请求与保持条件:线程在等待其他资源时不释放已占有的资源
-
不剥夺条件:资源只能由占用它的线程主动释放
-
循环等待条件:多个线程形成头尾相接的资源等待环
5.2 饥饿
饥饿指某个或某些线程因无法获得所需资源而一直无法执行。常见于线程优先级设置不合理或某些线程长时间占用资源的情况。
与死锁不同,饥饿在理论上是有可能解决的,比如当高优先级线程完成任务后,低优先级线程可能有机会执行。
5.3 活锁
活锁指线程不断重复相同的操作,但就是无法取得进展。就像两个人在走廊相遇,同时向一侧让路,结果又挡住了对方,于是又同时向另一侧让路,如此反复。
活锁与死锁的不同在于:活锁中的线程并未阻塞,而是在不断活动,但活动没有产生进展。
6. 死锁代码示例与避免策略
以下是一个简单的Java死锁示例:
// 死锁示例代码
public class DeadlockExample {
private static final Object resource1 = new Object();
private static final Object resource2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("Thread 1: Holding resource 1…");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread 1: Waiting for resource 2…");
synchronized (resource2) {
System.out.println("Thread 1: Acquired resource 2!");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (resource2) {
System.out.println("Thread 2: Holding resource 2…");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread 2: Waiting for resource 1…");
synchronized (resource1) {
System.out.println("Thread 2: Acquired resource 1!");
}
}
});
thread1.start();
thread2.start();
}
}
避免死锁的实用策略:
-
按固定顺序获取资源:所有线程都按相同顺序请求资源
-
使用超时机制:为锁请求设置超时时间,超时后释放已占有的资源
-
避免嵌套锁:尽量减少同时持有多个锁的情况
-
使用高级并发工具:如Java并发包中的并发集合和同步器
7. 结语
并发编程是现代软件开发中不可或缺的技能。通过理解同步/异步、并发/并行、临界区、阻塞/非阻塞以及死锁等相关概念,我们能够设计出更高效、更健壮的并发系统。
并发编程的学习曲线可能较为陡峭,但掌握其核心概念后,你会发现它并没有想象中那么可怕。重要的是理解各种概念的本质区别,并在实际开发中遵循最佳实践。
希望本文能帮助你建立对并发编程核心概念的清晰理解,为后续深入学习打下坚实基础。
网硕互联帮助中心




评论前必须登录!
注册