基本概念
同步和异步
同步和异步通常是用来形容一次方法调用。
同步方法调用一旦开始,调用者必须等到方法返回才能继续执行后续操作。
异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。异步方法通常会在另外一个线程中"真实"执行,整个过程不会影响调用者的工作。
举例:到了饭点,我们点外卖,下单成功之后等待外卖小哥送餐。这就是同步调用。 不过,为了学习,我们在下单成功之后看了会《Java 从入门到放弃》书籍,边等待外卖小哥送餐。这就是异步调用。
并发和并行
并发和并行两个概念很容易被混淆。他们都可以表示两个或者多个任务一起执行,但是偏重点有些不同。并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的。而并行是真正意义上的"同时执行"。
严格来说,并行的多个任务是真实的同时执行,而对于并发来说,这个过程值是交替执行。一会儿执行A,一会儿执行B,系统会不停地在两者间切换。但是对于外部观察者来说,即使多个任务之间是串行并发的,也会造成多任务间是并行执行的错觉。
举例:三个人同时吃三个苹果,这句是并行。而一个吃三个苹果,这就是并发,他需要来回切换。
临界区
临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程想要使用这个资源,则必须等待。
举例:会议室开会,当有人正在使用会议室,而这个时候你又想要使用这个会议室,就必须等他们会议结束才能使用。
阻塞和非阻塞
阻塞和非阻塞通常是用来形容多线程间的相互影响。比如当临界区资源被占用时,那么其他需要需要使用这个临界区资源的线程就必须等待,等待会导致线程挂起,这种情况就是阻塞。非阻塞的意思与之相反,它强调没有一个线程可以妨碍其他线程执行。
死锁、饥饿、和活锁
死锁、饥饿和活锁都属于多线程的活跃性问题。
死锁:多个进程对公共资源的相互竞争,导致循环等待的现象。比如:A->B->C->A,互相竞争,循环等待。
饥饿:指某一个线程或者多个线程因为某种原因无法获得所需要的资源,导致一直无法执行。比如:线程优先级低,导致高优先级的线程不断抢占它需要的资源,导致低优先级的线程无法工作。举例:在自然界中,母鸟喂食。由于雏鸟很多,而食物有限,雏鸟之间的事务竞争可能非常厉害,小雏鸟因为经常抢不到食物,有可能会被饿死。
活锁:活锁恰恰与死锁相反,死锁是大家都拿不到资源都占用着对方的资源,而活锁是拿到资源却又相互释放不执行。当多线程中出现了相互谦让,都主动将资源释放给别的线程使用,这样这个资源在多个线程之间跳动而又得不到执行,这就是活锁。举例:两个人迎面走来时,有时候因为有事比较急,而为了快速通过会避让对方先行。恰好对方也为了能快速通过礼让你先行,结果撞上了。于是乎,都意识到了这个问题,希望尽快避让对方,你向右他向左,结果又撞上了。
并发级别
由于临界区的存在,多线程之间的并发必须收到控制。根据控制并发的策略对并发的级别进行分类,大致为阻塞,无饥饿,无障碍、无锁、无等待。
阻塞
一个线程是阻塞的,那么在其他线程释放资源之前,当前线程无法继续执行。当使用 Synchronized 关键字或者重入锁时,我们得到的就是阻塞线程。
无饥饿
如果线程之间是有优先级的,那么线程调度的时候总是会倾向于满足高优先级的线程。也就是说,对于同一个资源的分配,是不公平的。那就出现了公平和非公平两种情况。对于非公平锁来说,系统允许优先级高的线程插队。这样有可能导致优先级低的线程产生饥饿。但如果锁是公平的,满足先来后到,那么接就不会产生,不论新来的线程优先级高低,想要获得资源就必须乖乖排队,那么所有的线程都有机会执行。
无障碍
无障碍是一种最弱的非阻塞调度。两个线程如果是无障碍的执行,那么他们不会因为临界区的问题导致一方被挂起。换言之,大家都可以大摇大摆地进入临界区了。对于无障碍的线程来说,一旦检测到数据被修改坏了,它会立即对自己所做的修改进行回滚,确保数据的安全。
如果说阻塞的控制方式是悲观策略,那么非阻塞的调度就是一种乐观的策略。它任务多个线程之间很有可能不会发生冲突,或者说这种概率不大。因此大家都应该无障碍的执行,但是一旦检测到冲突,就应该进行回滚。但是无障碍的多线程程序并不一定能够顺畅的运行,因为当临界区中存在严重的冲突时,所有的线程可能都会不断地回滚自己的操作,而没有一个线程可以走出临界区。这种情况会影响系统的正常执行。所有,我们希望在这一堆线程中,至少有一个线程能够在有效的时间内完成自己的操作,从而退出临界区。至少这样可以保证系统不会在临界区中进行无线的等待。
一种可行的无障碍实现可以依赖一个"一致性标记"来实现。线程在操作之前,线程读取并保存这个标记,在操作完成后,在此读取,检查这个标记是否被更改过,如果两者是一致的,则的说明资源访问没有冲突。如果不一致,则说明资源可能在操作过程中与其他线程冲突,需要重试操作。
无锁
无锁的并行都是无障碍的。在无锁的情况下,所有的线程都能尝试对临界区进行访问,但是不同的是,无锁的并发保证必然有一个线程能够在有限步内完成操作离开临界区。
无等待
无锁只要求有一个线程可以在有限步内完成操作,而无等待则是在无锁的基础上更进一步进行扩展。它要求所有的线程都必须在有限步内完成,这样就不会引起饥饿问题。如果限制这个步骤上限,还可以进一步分解为有界无等待和线程数无关的无等待几种,他们之间的区别只是对循环次数的限制不同。