同步与异步
同步(Synchronous):方法调用时,调用者必须等到方法调用并返回结果后,才能继续执行后面的操作。如果没有得到返回就不继续执行,有返回结果了就一个一个顺序的执行,需要等待,根据返回结果协调执行。
异步(Asynchronous):方法调用时,调用者不必等待方法返回结果便可继续执行后面的操作,当调用的方法执行后将通知调用者。通知的方式一般有三种:状态、通知、回调。
- 状态:监听被调用者的状态(比如通过轮询、长连接等),调用者需要每隔一段时间进行检查,效率低;
- 通知:当被调用者执行完以后,发通知告诉调用者,不是很消耗性能;
- 回调:与通知比较类似,当被调用者执行完以后,会调用调用者提供的函数继续执行。
区别:发出同步请求后,需要等待,发出异步请求后,无需等待。
并发与并行
并发(Concurrency):有处理多个任务的能力,但不一定同时,但一般是处理任务的间隔时间很短。比如通常讲的高并发,其实很可能是在一秒内处理了很多个请求,可能不是同一时间处理了这么多请求,还可能是多个任务交替进行,可能多个任务还是串行方式执行的,说明在一秒内处理多个任务的能力很强。有可能给人一种并行执行的错觉。
并行(Parallelism):有同时处理多个任务的能力,强调是同时处理。比如你的两只手可以同时干事情一样。
临界区、互斥量和信号量
临界区(Critical Section):表示一种公共资源或共享数据(如共用的存储器),可以被多个线程访问。但当有线程进入临界区时(临界区的资源已被此线程访问占用),其它线程必须等待,不可访问共享资源。直到此线程离开临界区,其它线程可才抢占对临界区的访问。目的是控制对共享资源的原子性访问。数据快,适用于控制数据访问。
互斥量(Mutex):与临界区看着相似,只有一个互斥对象,只有拥有互斥对象的线程才有对共享资源的访问权限。所以决定了在任何情况下共享资源都不会被多个线程访问。当前线程在完成对共享资源的访问后不再拥有互斥对象,然后其它对象拥有互斥对象后访问共享资源。互斥量比临界共复杂,使用互斥不仅能够在同一应用程序的不同线程中实现对共享资源的安全访问,还可在不同应用程序的不同线程中实现对资源的安全共享。
信号量(Semaphores):类似于交通中的信号灯(红、黄、绿灯),当绿灯亮起,某一(或两个)方向的车会有多辆车开出,其它方向的车辆不允许开出,为允许行驶方向的车辆让行。信号量也是允许多个线程共享资源,但控制了共享资源的最大线程数目,所以会对已经访问资源的线程进行计数。
阻塞与非阻塞
阻塞(Blocking):调用结果返回之前,其它线程被挂起。对于CPU是一直等待CPU处理完成,然后才会执行后面的操作(CPU跑去干别的事了,没空搭理我)。与同步不同,同步虽然也是要等到调用有结果返回,是因为它们有依赖关系,后面的执行需要根据返回结果再做决定,所以在没有返回结果之前,其实仍然是处于激活的,我还可以处理没有依赖关系的事情。而阻塞就像堵车了,同步是我就等你给我了一个结果。
非阻塞(Non-Blocking):在不能立即返回结果之前,不会阻塞当前线程,而是立即返回,这样不会影响其它线程的使用(可以通过主动轮询得知调用情况)。CPU处理一个任务时,不管当时来不来得及会不会处理完,都会返回,然后处理后面的任务。和异步看起来非常像。不过个人想从字面上去分析一点,阻塞强调了运行的顺畅,侧重于负重(像不堵车),异步强调了运行的流转,之间相互不影响。
死锁、饥饿与活锁
死锁(Deadlock):多个线程在执行过程中,由于竞争资源或彼此通信而形成的一种阻塞情况。结果导致都无法正常执行。好比是交通的十字路口,四个方向的车辆相互占用其它车辆可行驶的车道,又都没有释放占用的车道,结果就是谁都走不了。相互排斥、循环等待、部分分配、缺少优先权等情况下都可能导致死锁。可通过有充资源分配法和银行家算法等方法进行预防。
饥饿(Starvation):一个或多个线程长时间得不到需要的资源而不能执行的现象。比如某线程的优先级低,那优先级高的线程就不断抢占它需要的资源,它就一直得不到需要的资源而无法执行。也可能是某线程占用某资源未释放。它不同于死锁,有可能在未来一段时间内解决。避免饥饿可采用队列方式,以保证每个线程都有机会获得请求的资源,还有优先级、时间片等。
活锁(Livelock):活锁是一种比较有趣的现象,我们在生活中时不时会出现类似的情况。当A走在某个路口,此时B也走了过来,路口不算很宽,此时A想让路给B,于是向左,但B也想让A先行,于是也往左,这样谁也过不去。活锁就是在线程运行过程中类似的现象。活锁一般是由于对死锁的不正确处理引起的。由于处于死锁中的多个线程同时采取了行动(比如同时释放了资源),为了避免活锁应只对一个线程采取资源释放的操作。
注:以上内容参考《实战Java高并发程序设计(第2版)》。