在计算机诞生初期,由于其高昂的计算成本,只能允许一个用户运行一个任务或者一批任务(Batch processing)。随着技术的发展,人们开始思考一个问题,如何在计算机上实现多个任务“同时”运行?一开始,采用的是分时策略(Time sharing),就是允许多个用户共享一台计算机,但每个用户占用计算机的一个时间片,在这个时间后,由另一个用户接着运行使用。分时系统在一定程度上提高了计算效率,实现了资源的共享。但其实际上并没有真正实现任务的“同时”(并发,concurrency)运行。
现代并发编程(Concurrent Pragramming)概念的出现一方面是受到操作系统中如进程、中断以及抢占的影响;一方面由于计算机硬件技术的发展而出现的多核处理器。
- 进程,是程序执行的一个实体,是操作系统对CPU,寄存器,堆栈,内存,文件系统等计算资源的一种执行时的抽象;有了进程,计算机就可以通过调度系统来实现多个任务“同时”运行了,这里所谓的“同时”并不是多个程序真的在一个CPU中同时运行,而是说调度程序快速的在多个进程之间切换,交替执行不同程序,从而更有效的利用了计算机资源;
- 多核CPU的出现,为并发、并行计算提供了另一种可能。以前程序只能在一个CPU上运行,现在单个程序可以同时在多个CPU上同时运行了(Parallel Computing);或者多个进程同时在多个CPU上运行。
进程虽然提高了计算机系统的利用效率,实现了多个任务的并发执行,但是由于进程上下文切换Context Switch需要耗费CPU时间,频繁的进程切换势必导致计算机性能的下降。还有一个问题是,单个进程的程序在进行I/O操作时会阻塞,这样一方面会导致程序无响应,一方面也会浪费宝贵的CPU资源。于是,人们提出了线程(Thread)。一个进程中的线程共享进程的内存空间、全局变量、文件等资源,但会有独立的程序计数器(Program Counter),寄存器,栈空间(调用参数,本地变量等)以及线程状态。相比较而言,由于资源共享,线程创建比进程创建要快很多,上下文切换的时间更短,因而常被称为轻量级进程(Light-weight Process,LWP)。
那么,多线程并发到底有哪些好处?
- 充分利用处理器资源: 多线程可以让程序的线程同时在多个CPU上同时执行,这样能充分利用CPU,提高系统的性能,让程序本身运行更快;
- 更好的程序响应能力:程序在由I/O事件时,无需等待,可以让一个线程等待I/O,另一个线程处理其他事情,从而提供程序运行的效率;
- 更好的用户体验: 对有UI的程序来说,在多线程执行情况下,可以让一个线程来专门处理与UI的交互,其他线程则处理后台任务,这样既保证了良好的UI响应,也能保证程序的正常运行。
多线程并发执行虽然好处多多,但是在线程模型下,由于有资源的共享,多个线程同时操作共享数据/内存时,会出现数据一致性问题,导致程序运行出错。因此引来了一个难以忽视的线程安全问题:数据竞争(data race)与死锁(dead lock)。如果两个线程之间并没有共享数据(或者共享数据是不可变的,immutable),那么根本就不用考虑同步问题,因为线程之间执行时独立的,互不干涉。一旦有了共享数据,如果其中有一个线程修改,而其他线程需要读取该数据,则两个线程同时操作的情况下,可能出现竞争条件(race condition )或者变质(stale)数据。另一个可能的问题就是死锁。比如一个线程A试图获取某个资源X,而该资源被线程B持有,因此A需要等待B释放资源X;线程B需要获取某个资源Y,但是该资源被A所持有,B需要等待A释放资源Y,这样就可能出现死锁(Dead Lock)。另一方面,线程同步需要额外的计算来实现加锁与释放锁,在一定程度上降低了程序运行的效率,而频繁的上下文切换也会带来性能的损耗。为了减少线程上下文切换,人们又提出了协程,coroutine。
存在数据/内存共享的代码区域, 通常被称为临界区(critical section
), 为了避免上述可能存在的数据竞争与死锁问题的出现,需要确保线程的访问是原子(atomic)的, 不可中断的, 这种确保临界区数据的序列化访问方式即我们常说的同步(synchronization
).Java在语言层面实现了同步机制。这些同步机制主要考虑两个关键问题:
- 共享数据的互斥访问(mutual exclusion):对于共享数据,任何时刻都只有一个线程在操作(竞争);
- 共享数据的可见性(visibility):一个线程修改数据后对其他线程可见(协作);
在接下来的几篇文章里,我将围绕上述两个为题重点介绍Java并发编程的基本概念以及具体的实现机制,争取在这几篇文章里把Java并发编程的基本概念与核心要点都讲明白:
对于并发编程,本人也处于学习总结阶段,有不正确的地方,欢迎指正。