若是一个线程由于其余线程占满了而没法获取CPU运行时间,这种状况咱们称之为“饥饿现象”.线程将一直饥饿下去,由于其余线程总能替代它获取CPU运行时间.解决这种状况的措施咱们称之为“公平措施”.即让全部线程都能得到一次执行的机会.html
在Java中,如下三种状况可以产生饥饿现象:java
你能够分别对每个线程设置优秀级.高优先级的线程可以获取到更多的CPU运行时间.你能够为线程设置1~10的优先级,但这彻底依赖于应用运行在哪一个操做系统之上.对于大多数应用采用默认优秀级就好.post
Java的同步代码块是引发饥饿现象的另外一个缘由.Java同步代码块没办法保证在等待中的线程可以按照某种序列进入同步代码块.这意味着理论上会有一个线程无限期的等待进入同步代码块,由于其余线程老是可以在它以前进入同步代码块.这种问题也称之为"饥饿现象",该线程将会一直饥饿下去,由于其余线程总能替代它获取CPU运行时间.学习
在多个线程同时调用同一个对象的wait()
方法时,notify()
方法没法保证唤醒哪一个线程.这会致使某些线程一直在等待.这会产生一个线程一直在等待唤醒的风险,由于其余线程总能在它以前被唤醒.this
虽然在Java中不可能实现百分百的公平措施,但咱们仍然能够实现本身的同步器结构来增长线程间的公平性.spa
咱们先来学习一个简单的同步器代码块:操作系统
public class Synchronizer{
public synchronized void doSynchronized(){
// 花费至关长的时间来执行工做
}
}
复制代码
若是有多于一个的线程调用doSynchronized方法,那么其余线程都须要等待第一个进入同步代码块的线程退出方法.只要有多于一个线程在等待状态,就不能保证哪一个线程先被容许进入同步代码块.线程
为了增长等待线程的公平性,首先咱们须要使用锁来代替同步代码块来实现同步机制.code
public class Synchronizer{
Lock lock = new Lock();
public void doSynchronized() throws InterruptedException{
this.lock.lock();
// 临界区代码, 花费至关长的时间来执行工做
this.lock.unLock();
}
}
复制代码
咱们注意到doSynchronized再也不使用synchronized
来声明.取而代之的是将临界区代码放置在lock.lock()和lock.unLock()方法调用之间.htm
一个简单Lock类实现:
public class Lock{
public class Lock {
private boolean isLocked = false;
private Thread lockingThread;
public synchronized void lock() throws InterruptedException {
while (isLocked){
wait();
}
isLocked = true;
lockingThread = Thread.currentThread();
}
public synchronized void unLock(){
if(lockingThread != null && lockingThread == Thread.currentThread()){
throw new IllegalMonitorStateException("Calling thread is not locked this lock");
}
isLocked = false;
lockingThread = null;
notify();
}
}
复制代码
若是你看了上文的Synchronizer和如今的Lock实现,你会发现若是有多于一个线程同时访问lock()方法时,会发生阻塞.其次,若是当前的锁为锁住状态的话,线程会在lock方法中的while循环内部调用wait()方法从而进入等待状态.记住,一个线程一旦调用wait()完毕,则会释放当前对象锁,让其余线程能够进入lock()方法.最后的结果是多个线程进入lock()方法的while循环中调用wait()方法进入等待状态.
若是你回头看doSynchronized()方法,你会发现lock()和unlock()状态切换中间的注释,这将花费至关长的时间来执行两个方法调用间的代码.须要咱们确认的是执行这些代码须要花费至关长的时间来比较进入lock()方法和在锁被锁住的状况下调用wait()方法.这意味着大部分时间都用来等待锁的锁住和在lock()方法内部调用的wait()方法完毕后等待退出wait()方法.而不是等待进入lock()方法.
在以前的同步代码块的状态下,若是有多个线程等待进入同步代码块,没法保证哪一个线程先进入同步代码块.一样的在调用wait()方法进入等待状态后,没法保证调用notify()后哪一个线程会先被唤醒.因此当前Lock的实现版本并不比以前的synchronized版本的doSynchronized()公平到哪去.但咱们能够稍做更改.
当前Lock版本调用的是它本身的wait()方法.若是将每一个线程调用的wait()方法替换成不一样对象的.即每一个线程对应调用一个对象的wait()方法,即Lock可以决定在日后的时间里到底调用哪一个对象的notify()方法,这样就能具体有效的选择唤醒哪一个线程.
如下内容是将上文说起的Lock.class转变为一个公平锁即FairLock.class.你会发现与以前版本对比,这个实现仅是调整了同步代码块和wait()/notify()的调用方式而已.
实际上在获得当前这个版本的公平锁以前遇到了许许多多的问题,而每解决这其中的一个问题都须要长篇概论来阐述,解决这些问题的每个步骤都会在日后的主题中说起.这包含Nested Monitor Lockout, 滑动条件和信号丢失问题. 如今重点要知道的是线程以队列的方式来调用lock()方法中的wait()方法,且每次在公平锁未锁住时仅能让队列头部的线程来获取和锁住公平锁实例.其余线程则处在等待状态直到进入队列头部.
public class FairLock {
private boolean isLocked = false;
private Thread lockingThread;
private List<QueueObject> waitingThreads = new ArrayList<>();
public void lock() throws InterruptedException {
// 1. 为每一个线程建立一个QueueObject
QueueObject queueObject = new QueueObject();
boolean isLockedForThisThread = true;
synchronized (this) {
// 2. 添加当前线程的QueueObject到队列中
waitingThreads.add(queueObject);
}
while (isLockedForThisThread) {
synchronized (this) {
isLockedForThisThread = isLocked || waitingThreads.get(0) != queueObject;
if (!isLockedForThisThread) {
// 3. 锁住当前公平锁
isLocked = true;
waitingThreads.remove(queueObject);
lockingThread = Thread.currentThread();
return;
}
}
try {
// 4. 调用该线程对应QueueObject的wait()方法进入等待状态
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");
}
// 1. 释放公平锁
isLocked = false;
lockingThread = null;
if (waitingThreads.size() > 0) {
// 2. 调用队列头部线程一一对应的QueueObject唤醒线程
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
.取而代之的是将须要作同步限制的代码块嵌套到synchronized
代码块中.
每一个线程调用lock()方法后都会建立与之对应的QueueObject
实例,并进入到队列中.线程调用unlock()方法后会从队列的头部取得QueueObject
对象并调用它的doNotify()方法来唤醒与之对应的线程.对于全部等待的线程来讲,这种方式每次仅会唤醒一个线程.这部分就是FairLock用来确保公平的代码.
咱们注意到lock的锁住状态会在同一个代码块中不停的检查和设置来解决滑动条件带来的问题.
同时咱们注意到QueueObject
就是一个Semaphore
.doWait()和doNotify()调用所产生的状态会存储在QueueObject
内部.这用来解决信号丢失问题,即一个线程在调用queueObject().doWait()时,被另外一个线程抢先机会调用了unlock()中的queueObject.doNotify(). queueObject.doWait()调用被放置在synchronized(this)
同步代码块以外,用于解决Nested Monitor Lockout问题.这样当没有线程在lock()方法的synchronized(this)
代码块中执行时,其余线程能够正常调用unLock()方法.
最后须要注意的是,为何须要将queueObject.doWait()调用放置在try-catch中.当线程经过抛出InterruptedException
来终止lock()方法调用时,咱们须要将线程与之对应的QueueObject
踢出队列.
当你比较Lock.class和FairLock.class的lock()和unLock()实现时,你会发如今FairLock.class中多了许多代码.这部分代码会让FairLock同步机制的运行相较于Lock会慢一些.至于影响多大取决于FairLock所限制的临界区代码的运行时长.运行时长越长,FairLock带来的负面影响越小,固然这还取决于这部分代码的运行频率.
该系列博文为笔者复习基础所著译文或理解后的产物,复习原文来自Jakob Jenkov所著Java Concurrency and Multithreading Tutorial