原文地址: http://tutorials.jenkov.com/j...html
若是一个线程没有被分配到 CPU 执行时间,该线程就处于“饥饿”状态。若是老是分配不到 CPU 执行时间(由于老是被分配到其余线程去了),那么该线程可能会被“饿死”。有一种策略用于避免出现该问题,称做“公平策略”,即保证全部的线程都能公平地获得被执行的机会。java
在 Java 中,有三种最广泛的情形会致使饥饿的发生:安全
synchronized
块,以至某些线程老是得不到机会;wait()
方法)时,彻底得不到唤醒的机会,由于被唤醒的老是别的线程。每一个线程均可以单独设置优先级。优先级越高,该线程就能得到更多的 CPU 执行时间。优先级的值最低为 1 最高为 10。至于如何根据优先级来分配 CPU 执行时间,则依赖于操做系统的具体实现。在大多数应用中,咱们最好不要去擅自修改它。this
Java 当中的 synchronized 代码块也是致使饥饿的一个因素。它不保证线程进入的顺序,因此理论上某个线程可能永远没法进入 synchronized 块,这种状况下能够说这个线程就被“饿死”了。操作系统
当多个线程同时调用的某个对象的 wait() 方法并等待时,notify() 方法不保证必定能唤醒哪一个指定的线程。因此若是它老是不去唤醒某个线程的话,这个线程就处于永久性地等待当中了。线程
固然咱们没办法实现 100% 的绝对公平,但仍是能够经过一些结构上的设计来增长线程之间的公平性。设计
首先咱们来看一个简单的 synchronized 代码块:code
public class Synchronizer{ public synchronized void doSynchronized(){ //do a lot of work which takes a long time } }
当多个线程调用 doSynchronized()
方法时,只有一个线程可以进入该方法并执行,并且该线程退出该方法后,正在等待的线程中没法保证哪个才是接下来能够进入的。htm
为了加强公平性,第一步咱们先把 synchronized 块改成锁对象:对象
public class Synchronizer{ Lock lock = new Lock(); public void doSynchronized() throws InterruptedException{ this.lock.lock(); // critical section, do a lot of work which takes a long time // 须要同步执行的代码 this.lock.unlock(); } }
请注意 doSynchronized()
方法自己如今再也不是同步的了,须要同步执行的代码如今由lock.lock()
和 lock.unlock()
保护起来。
那么 Lock 类简单的实现是下面这个样子:
public class Lock{ private boolean isLocked = false; private Thread lockingThread = null; public synchronized void lock() throws InterruptedException{ while(isLocked){ wait(); } isLocked = true; lockingThread = Thread.currentThread(); } public synchronized void unlock(){ if(this.lockingThread != Thread.currentThread()){ throw new IllegalMonitorStateException( "Calling thread has not locked this lock"); } isLocked = false; lockingThread = null; notify(); } }
结合上面 Synchronizer 类和这里的 Lock 实现,你会看到:首先,当多个线程调用 lock() 方法时,它们会被阻塞;其次,当 Lock 对象处于锁住状态时,进入 lock() 方法的线程会在 wait() 语句处阻塞。这里要注意:当线程成功调用 wait() 方法时,会自动释放 Lock 对象的锁,因而其余的线程可以得以进入 lock() 方法,最终会有多个线程都阻塞在 wait() 语句处。
咱们回头看 doSynchronized() 方法中 lock() 和 unlock() 之间的部分,假设这部分代码须要很长时间来执行,甚至比线程在 wait() 语句处等待所花的时间都长的多。那么线程得到锁所需的时间主要也是耗在 wait() 语句处,而不是进入 lock() 方法的时候。
在目前这个版本的代码中,不论线程是在 synchronized 块阻塞,仍是在 wait() 处阻塞,都不能保证哪一个线程能必定被唤醒,因此目前的代码还没有提供公平策略。
(译注:之因此改为这样,目的是令线程在进入 lock() 方法时的阻塞时间尽量短,也就是全部的线程都在 wait() 处阻塞,以便实施接下来的改动。)
目前版本的 Lock 对象是在调用自身的 wait() 方法。咱们改掉这点,让每一个线程调用不一样对象的 wait() 方法的话,那么就能够自行挑选调用哪一个对象的 notify() 方法,以此实现自行挑选唤醒哪一个线程。
下面的代码展现了将 Lock 类转化为 FairLock 类的结果。请注意同步方式和 wait()
/notify()
的调用方式有了哪些的变化。
整个改动的实现是阶段性的,这个过程当中须要依次解决内部锁对象死锁、同步条件丢失以及解锁信号丢失等问题。因为篇幅长度所限这里就不详述了(请参考上面的连接)。这里最重要的改动点,就是对 lock()
方法的调用如今是放在队列中,全部的线程以队列中的顺序来依次得到 FairLock 对象的锁。
public class FairLock { private boolean isLocked = false; private Thread lockingThread = null; private List<QueueObject> waitingThreads = new ArrayList<QueueObject>(); public void lock() throws InterruptedException{ QueueObject queueObject = new QueueObject(); boolean isLockedForThisThread = true; synchronized(this){ waitingThreads.add(queueObject); } while(isLockedForThisThread){ synchronized(this){ isLockedForThisThread = isLocked || waitingThreads.get(0) != queueObject; if(!isLockedForThisThread){ isLocked = true; waitingThreads.remove(queueObject); lockingThread = Thread.currentThread(); return; } } try{ queueObject.doWait(); }catch(InterruptedException e){ synchronized(this) { waitingThreads.remove(queueObject); } throw e; } } } public synchronized void unlock(){ if(this.lockingThread != Thread.currentThread()){ throw new IllegalMonitorStateException( "Calling thread has not locked this lock"); } isLocked = false; lockingThread = null; if(waitingThreads.size() > 0){ waitingThreads.get(0).doNotify(); } } }
public class QueueObject { private boolean isNotified = false; public synchronized void doWait() throws InterruptedException { while(!isNotified){ this.wait(); } this.isNotified = false; } public synchronized void doNotify() { this.isNotified = true; this.notify(); } public boolean equals(Object o) { return this == o; } }
首先你可能注意到 lock()
方法再也不是 synchronized
。由于只有这个方法里面的部分代码才须要同步。
FairLock 会为每一个线程建立一个新的 QueueObject
对象并将其加入队列。调用 unlock()
方法的线程会从队列中取第一个元素对象并调用它的 doNotify()
方法,这样唤醒的就只有一个线程,而不是一堆线程。这个就是 FairLock 的公平机制所在。
注意接下来就是在同步块中从新检查条件并更新锁状态,这是为了不同步条件丢失。
此外 QueueObject
其实是一个信号量,doWait()
和 doNotify()
方法的目的是存取锁的状态信号,以免解锁信号丢失,即在一个线程调用 queueObject.doWait()
以前,另外一个线程已经在 unlock()
方法中调用了该对象的 queueObject.doNotify()
方法。至于将 queueObject.doWait()
方法的调用放在同步块外面,是为了不内部对象死锁的状况发生,这样另外一个线程就能够持有 FairLock 对象的锁,并安全的调用 unlock()
方法了。
最后就是对 queueObject.doWait()
这条语句进行异常捕获。若是这条语句执行时发生了 InterruptedException 异常,那么就须要在离开这个方法前将 queueObject 对象从队列中去掉。
咱们把 Lock
和 FairLock
对比一下就会看到后者的 lock()
和 unlock()
增长了不少代码,它们会致使其执行效率比前者略有降低。这个影响的程度如何,取决于 lock()
和 unlock()
之间的同步代码的执行时间,该时间越长,则影响就越小。固然同时也取决于锁自己的使用频繁程度。