Java并发编程(一)——不得不说的几个概念

1.同步(Synchronous)和异步(Asyncronous)

同步和异步通常用来形容一次方法调用。同步调用一旦开始,调用者必须等到方法返回后才能进行后续操作;而异步调用更像是一次消息传递,调用的方法会立刻返回,调用者就可以进行后续操作。如图所示,同步调用会阻碍当前线程的后续步骤,而异步调用则会将操作放到另一个线程执行,不会阻碍调用者的后续工作。从调用者的角度来看,异步操作似乎是瞬间完成的。

打比方说,同步就像我们去超市购物,我们(当前线程)需要进行一系列诸如坐电梯、乘公交/开车、挑选商品、结账等操作,我们需要每个步骤完成之后才能进行下一步骤;
但是如果我们觉得亲自去购物过于浪费时间,那么就可以在家里打开购物app,手指点两下完成下单支付操作,我们的任务就完成了,后续的发货配送等步骤就交由商家和物流处理,而我们则可以想做什么做什么,当货物送到时会有配送人员联系我们。整个购买过程我们只负责下单,其他交由别人处理,这就是异步处理。

2.并发(Concurrency)和并行(Parallelism)

并发和并行是两个比较容易混淆的概念,两者的字面意思都可以理解为多个任务同时执行。但并发一般是指多个任务交替运行(任务之间可能是串行),而并行则是真正意义上的同时执行。

举例说明,我们在打电话时可能会需要记录一些东西。电话那头说一句,我们记一句,过程中“听”和“写”是交替进行的,但从结果上来看的话,那边说完了,我们也记完了,这就是并发;而当我在打电话的同时,我同事在旁边敲代码,我们互不干扰,各做各的,这就是并行。
实际上,当只有一个CPU时使用多进程/多线程,那么多个任务只能交替运行,不可能是真正并行的(一个CPU一次只能执行一个指令)。真正的并行需要由多个CPU来完成。

3.临界区

临界区用来表示一种公共资源(也可以称为共享数据),可以供多个线程使用,但同一时间只能有一个线程在访问它,当公共资源被某个线程占有时,其它需要使用这个资源的线程必须等待。
就好像公共电话,同一时间只能有一个人在使用。当甲先进入电话亭打电话后,后边的乙丙丁就只能在电话亭外等待(实际情况要更复杂,比如等待者不一定会按先来后到的顺序占有资源,后续会说明),当甲通话结束后后边的人才能使用电话亭。 在并发程序中,临界区资源是需要受保护的,如果乙对甲的通话对象说了要传达的事情,怕是会出不少的乱子。

4.阻塞(Blocking)和非阻塞(Non-Blocking)

阻塞和非阻塞通常用来形容多线程间的相互影响。
比如一个线程进入了临界区占有了公共资源,那么其他需要这个资源的线程就必须在临界区外等待,这将导致等待的线程挂起,线程后续操作无法进行,这就出现了阻塞。此时如果占有资源的线程一直不释放资源,那么这些临界区外的线程将无法进行后续工作。
非阻塞则与之相反,线程不会在临界区上进行等待,而是尝试不断向前执行。

5.死锁(Deadlock)、饥饿(Starvation)和活锁(Livelock)

死锁、饥饿和活锁都会导致相关线程很难继续执行后续操作。其中死锁最为糟糕,发生死锁的情况下,最严重时会导致整个程序无法正常执行。下图显示了一个死锁的发生。

线程A和线程B都无法继续往下执行:A在等待B的操作完成,而B又在等待A的操作完成,这种互相等待对方导致永远无法向下执行的情况称为死锁。
饥饿是指一个或多个线程由于种种原因而无法获得所需资源,导致一直无法执行。比如当一个优先级很低的线程,它所需要的资源会被高优先级的线程优先抢占,导致低优先级线程无法工作。与死锁相比,饥饿是有可能在未来一段时间内解决的(比如所有高优先级线程执行完毕,低优先级线程就可以获取到资源了)。
活锁是一个比较有趣的情况。生活中我们可能会遇到这种情况:迎面走来一个人,一般我们的本能反应都会是避让对方,但很凑巧,对方也往同一方向避让我们,通常这个动作重复2-3次后,也就可以顺利解决问题。因为这时大家会进行交流,避免这种情况继续下去。
但这种情况发生在两个线程间的时候,就没那么容易解决了。如果两个线程都主动将资源释放给对方使用,那么资源将在两个线程间不断跳动,而没有一个线程可以拿到资源从而正常执行,这就是活锁。

猜你喜欢

转载自juejin.im/post/5dc028bdf265da4d2e1225bf