1 常用概念
随着不断提出的新的应用需求,计算机体系结构的不断发展,操作系统也在不断地发展,从最初的单道批处理系统到多道批处理系统、分时系统和实时系统等等,不同的操作系统有着各自不同的特征,但是它们也都有着几个基本特征,其中之一就是并发。进程和并发是现代操作系统中最重要的基本概念,由于多核多线程CPU的诞生,为了充分利用CPU的资源,多线程、高并发的编程越来越受重视和关注。
- 程序与进程
程序是一组有序指令的集合,是一种静态的概念。进程是程序的一次执行,属于一种动态的概念。在多道程序环境下,程序的执行属于并发执行,此时它们将失去封闭性,并具有间断性,运行结果也将不可再现,为了能使多个程序可以并发执行,提高资源利用率和系统吞吐量,并且可以对并发执行的程序加以描述和控制,引入进程的概念。
- 进程和线程
线程的引入主要是为了减少程序在并发执行时所付出的时空开销。我们知道,为了能使程序能够并发执行,系统必须进行创建进程、撤销进程以及进程切换等操作,而进程作为一个资源的拥有者,在进行这些操作时必须为之付出较大的时空开销。 线程和进程的区别主要如下:(1) 进程是系统中拥有资源的一个基本单位,线程本身并不拥有系统资源,同一进程内的线程共享进程拥有的资源。(2) 进程仅是资源分配的基本单位,线程是调度和分派的基本单位。(3) 进程之间相对比较独立,彼此不会互相影响,而线程共享同一个进程下面的资源,可以互相通信影响。(4) 线程的并发性更高,可以启动多个线程执行同程序的不同部分。
- 并行和并发
并行是指两个或多个线程在同一时刻执行,并发是指两个或多个线程在同一时间间隔内发生。如果程序同时开启的线程数小于CPU的核数,那么不同进程的线程就可以分配给不同的CPU来运行,这就是并行,如果线程数多于CPU的核数,那就需要并发技术。【解惑并行和并发】
2 常见问题
2.1 为什么需要并发
并发其实是一种解耦合的策略,它帮助我们把做什么(目标)和什么时候做(时机)分开。这样做可以明显改进应用程序的吞吐量(获得更多的CPU调度时间)和结构(程序有多个部分在协同工作)。
- 资源利用率:从整个程序的执行角度来看,程序执行时可以看作是对输入的数据进行计算处理然后输出到特定的设备中。如果这条流程线完全是串行执行的话,当其中的一个环节正在执行的时候其他环节就不能工作。这就意味着一旦输入阻塞,即IO等待读入数据那么已读入的数据也不能得到处理,已处理的数据也不能输出,这就造成了CPU的闲置。而如果这三个步骤可以并发执行的话即使IO在等待输入CPU仍然可以对已在内存中的数据做计算处理,结果也可以正常输出。这就提高了CPU的利用率,不会因为输入输出的阻塞导致CPU的计算能力被浪费。
- 时间:很多任务彼此之间并没有什么关联,当有充分的资源可以使用时,它们可以同时被执行,与串行地执行任务相比,这样既可以充分利用现有资源,提高资源的利用率,同时又可以减少任务完成的总时间,可以节省出更多的时间处理接下来的任务。
- 公平性:对于同优先级的任务来说,它们应该能够受到计算机资源的同等待遇。如果是串行执行的话就意味着有先有后,这就造成了任务处理的不公平性,并发就可以很好地解决这个问题,它们或是同时在不同的CPU上执行,或是在单个CPU上交替执行,保证了任务应该享有的公平性。
- 简便性:当有多种类型的任务执行时,为每种任务单独编写程序比编写混杂在一起的所有任务的处理程序要简单的多。试想当我们在处理多种事情时,把每种任务都分配给单独的人员比起把每种任务都平均分配然后让相应人员都处理所有种类的任务相比,效率肯定要高的多。一方面是因为一直做一件事会做的越来越熟,更重要的是可以专心做一件事而不用受到其他事情的干扰,这一点想必已经工作的朋友一定深有体会。
2.2 误解和正解
- 并发总能改进性能?(真相:并发在CPU有很多空闲时间时能明显改进程序的性能,但当线程数量较多的时候,线程间频繁的调度切换反而会让系统的性能下降)
- 编写并发程序无需修改原有的设计?(真相:目的与时机的解耦往往会对系统结构产生巨大的影响)
并发编程比较客观的认识:
- 编写并发程序会在代码上增加额外的开销。
- 正确的并发是非常复杂的,即使对于很简单的问题。
- 并发中的缺陷因为不易重现也不容易被发现。
- 并发往往需要对设计策略从根本上进行修改。
3 并发编程的原则和技巧
-
单一职责原则:分离并发相关代码和其他代码(并发相关代码有自己的开发、修改和调优生命周期)。
-
限制数据作用域:两个线程修改共享对象的同一字段时可能会相互干扰,导致不可预期的行为,解决方案之一是构造临界区,但是必须限制临界区的数量。
-
使用数据副本:数据副本是避免共享数据的好方法,复制出来的对象只是以只读的方式对待。Java 5的java.util.concurrent包中增加一个名为CopyOnWriteArrayList的类,它是List接口的子类型,所以你可以认为它是ArrayList的线程安全的版本,它使用了写时复制的方式创建数据副本进行操作来避免对共享数据并发访问而引发的问题。
-
线程应尽可能独立:让线程存在于自己的世界中,不与其他线程共享数据。