如果一个线程因为CPU时间全部被其他线程抢走而得不到CPU运行时间,这种状态被称之为“饥饿”。线程被“饥饿致死”正是因为它得不到CPU运行时间的机会,解决饥饿的方案被称之为“公平性” – 即所有线程均能公平地获得运行机会。

本文在饥饿与公平 这篇文章的基础上,加入了自己的理解和对代码的分析。

Java中导致饥饿的原因

  1. 高优先级线程吞噬所有的低优先级线程的CPU时间。
  2. 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。
  3. 线程在等待一个本身(在其上调用wait())也处于永久等待完成的对象,因为其他线程总是被持续地获得唤醒。

1.高优先级线程吞噬所有的低优先级线程的CPU时间
你能为每个线程设置独自的线程优先级,优先级越高的线程获得的CPU时间越多,线程优先级值设置在1到10之间,而这些优先级值所表示行为的准确解释则依赖于你的应用运行平台。对大多数应用来说,你最好是不要改变其优先级值。

2.线程被永久堵塞在一个等待进入同步块的状态
Java的同步代码区也是一个导致饥饿的因素。Java的同步代码区对哪个线程允许进入的次序没有任何保障。这就意味着理论上存在一个试图进入该同步区的线程处于被永久堵塞的风险,因为其他线程总是能持续地先于它获得访问,这即是“饥饿”问题,而一个线程被“饥饿致死”正是因为它得不到CPU运行时间的机会。

3.线程在等待一个本身(在其上调用wait())也处于永久等待完成的对象
如果多个线程处在wait()方法执行上,而对其调用notify()不会保证哪一个线程会获得唤醒,任何线程都有可能处于继续等待的状态。因此存在这样一个风险:一个等待线程从来得不到唤醒,因为其他等待线程总是能被获得唤醒。

Java中实现公平性

虽Java不可能实现100%的公平性,我们依然可以通过同步结构在线程间实现公平性的提高。
首先来学习一段简单的同步态代码:

多个线程调用doSynchronized()方法,在第一个获得访问的线程未完成前,其他线程将一直处于阻塞状态,在多线程被阻塞的场景下,接下来将是哪个线程获得访问权是无法保证的。

使用锁方式替代同步块

此时doSynchronized()不再声明为synchronized,而是使用了Lock类的lock和unlock方法来代替。

下面是Lock类的实现:

我们先来测试一下:

测试结果为:

简述一下t1线程和t2线程的执行流程:t1率先进入lock方法,获的了Lock对象锁(后面均用锁代替)。由于lock方法被synchronized修饰,t2被阻塞在lock方法之外,t1进入lock方法后将isLocked置为true,然后t1跳出lock方法,并释放了锁,t2顺势获取到锁。t2进入lock后发现isLocked等于true,执行了wait方法,t2释放刚刚获取到的锁并处于等待状态(PS:如果没有其他线程唤醒将一直等待下去)。 t1执行完耗时的操作后,进入了unlock方法并获取到锁,并将isLocked置为false,然后执行notify方法唤醒等待的线程(PS:notify虽然唤醒了等待线程,但是当前线程并不会释放锁),当t1跳出unlock方法后,才将锁释放。然后被唤醒的t2此时又获取到了锁,继续往下执行…….

有人会质疑这么写会不会引起死锁?其实是不会的,因为lock和unlock方式的执行顺序是单线程下完成的,并不具备死锁的条件。

对比

同步代码块会将线程阻塞在试图进入lock方法的外部(阻塞态),而Lock锁则将时间用在了wait()的等待中(等待态)。

同步块不会对等待进入的多个线程谁能获得访问做任何保障,同样当调用notify()时,wait()也不会做保障一定能唤醒某个线程,因此这个版本的Lock类和同步块那个版本就公平性而言,没有任何区别。

公平锁

上一个版本的Lock锁对象都是相同的,如果每个线程在不同的对象上调用wait(),这样我们就可以决定哪个对象能对其调用notify(),因此可以做到有效的选择唤醒哪个线程(PS : 每个线程对应自己独有的对象锁)。我们可以对上面的Lock类进一步设计成FairLock类。

FairLock类主要思想:每一个调用lock()的线程都会进入一个队列,当解锁后,只有队列里的第一个线程被允许锁住FairLock实例,所有其他线程都将处于等待状态,知道他们处于队列的头部,遵循先到先执行的原则。

FairLock类代码如下:

测试代码:

测试结果:

我来简述一下线程的执行流程:

  • t1,t2,t3进入lock方法,创建了一个属于自己的QueueObject锁对象,然后将其加入到队列中(打印结果看,t1先进入队列,t2其次,t3最后进入)。
  • t1进入while循环中的同步代码块,并将isLock置为true,并将自己的锁对象从队列中移除,然后return跳出lock方法并释放同步代码块中的锁。然后t2,t3竞争该锁,t2和t3无论谁先进入同步代码块,都会调用dowait()方法进入等待(因为isLock等于true),并释放对象锁。如果线程出错则从队列移除并抛出异常。
  • t1工作完成,调用unlock方法,将isLock置为false(此时的isLockedForThisThread等于true),将处于队列头的对象锁取出并调用doNotify方法,唤醒可以拥有此对象锁的线程即t2。t2继续在while中运行,队列移除t2的锁对象,然后t2工作完成之后,调用unLock方法,唤醒t3……..
  • 可以看出先进队列的线程,先被唤醒,优先执行,一定程度上防止了某些线程无法被唤醒而处于饥饿状态的情况。(比如:开始线程为3个,执行过程中又进来2个,即使后面两个竞争到CPU的几率比较大,也得在队列中排队,防止前面3个线程中某个线程处于饥饿状态)

性能考虑

不公平锁意味着后请求锁的线程可能在其前面排列的休眠线程恢复前拿到锁,这样就有可能提高并发的性能。这是因为通常情况下挂起的线程重新开始与它真正开始运行,二者之间会产生严重的延时。因此非公平锁就可以利用这段时间完成操作。这是非公平锁在某些时候比公平锁性能要好的原因之一。