由于其他线程占用了CPU的所有时间片,导致了当前线程抢不到时间片,这就是饥饿。由于线程不能强迫其他线程释放时间片,因此只能等待。为了解决饥饿问题,Java引入了公平锁,让每个线程都有抢到CPU时间片的机会。
造成线程饥饿的原因
- 高优先级的线程抢占了所有CPU时间片,低优先级的线程分配不到
- 一个线程总被阻塞在同步块外,因为其他线程持续在它之前抢到锁
- 线程的
wait()
方法总迟于其他线程,唤醒不了对象
高优先级线程抢占了所有CPU时间片
我们可以为每个线程分别设置优先级,优先级越高CPU时间片就越容易得到。一般优先级从1到10,具体情况取决于运行你程序的操作系统。对于大多数线程还是不要随意改变优先级。
线程阻塞在同步块之外
Java同步块不保证线程先来先得,理论上存在线程永远阻塞在同步块外的风险,因为其他线程总能抢在该线程之前进入同步块。形象地将这个现象称为『饥饿』,该线程被阻塞得『饿死了』。
Java中的wait()
与notify()
notify()
方法不保证会释放的资源会被哪个线程拿到,多线程下每个线程的wait()
方法都有几率抢占资源,极端情况下,某个线程会永远拿不到资源。
Java公平锁的实现
要实现绝对公平的锁当然不可能,我们只能让锁尽量公平。从最简单的同步块代码入手:
public class Synchronizer {
public synchronized void doSynchronized(){
// your code here
}
}
如果多线程调用该方法,大多线程会阻塞,直到第一个进入同步块的线程执行完毕释放资源。当然了,这个阻塞等待根据之前描述是不公平的。
使用锁来代替同步块
我们去掉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();
}
}
锁这个类其实很简单,可以采用synchronized
关键字实现,参考之前的文章
public class Lock {
private boolean isLocked = false;
private Thread lockingThread = null;
public synchronized void lock() thrwos 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();
}
}
如果多线程调用call()
方法,线程会阻塞(因为只有一个线程会抢到锁)。其次,当资源被锁,线程会阻塞在while
中的wait()
调用上。调用wait()
方法会释放类上的同步锁,因此其他等待抢占锁的线程现在就可以获取资源了。最终,多个线程都能在lock()
内部调用wait()
。
lock()
与unlock()
之间的代码可能需要很长时间去执行,相比之下进入lock()
方法和调用wait()
的时间可以忽略不计,这就意味着大部分等待锁以及执行代码的时间都被消耗在等待wait()
方法上,而不是进入lock()
方法。
如之前所述,如果有多个线程,则同步块不能保证授予哪个线程访问权限。wait()
也不保证在调用notify()
时唤醒了哪个线程。因此,当前的Lock
类版本与doSynchronized()
的在公平性方面基本相同,但是我们可以改变这一点。
当前版本的Lock
类将调用其自己的wait()
方法。相反,如果每个线程在一个单独的对象上调用wait()
,从而只有一个线程在每个对象上调用了wait()
,则Lock
类可以决定在这些对象中的哪个上调用notify()
,从而有效地准确选择要使用的线程。
公平锁
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();
}
}
}
现在,每个调用lock()
的线程都已排队,并且只有队列中的第一个线程才能抢到锁。所有其他线程将被暂存到它们到达队列的顶部。
首先,lock()
方法不再使用synchronized
关键字修饰,仅将同步所需的块嵌套在同步块内。
FairLock
创建一个新的QueueObject
实例,每个调用lock()
的线程都会入队,调用unlock()
的线程出队,并调用doNotify()
,以唤醒在该对象上等待的线程。这样,一次仅唤醒一个等待线程,而不唤醒所有等待线程。这决定着FairLock
的部分公平性。
QueueObject
实际上是一个信号量。doWait()
和doNotify()
方法将信号存储在QueueObject
中。这样做是为了避免由于线程在调用queueObject.doWait()
之前被另一个线程又调用unlock()
从而调用queueObject.doNotify()
先占而导致的信号丢失。queueObject.doWait()
被置于synchronized(this)
块的外部,以避免嵌套的监视器锁定,因此,当在lock()
方法的synchronized(this)
块内部没有执行任何线程时,另一个线程实际上可以调用unlock()
。
最后,在try-catch
块内调用queueObject.doWait()
。万一抛出InterruptedException
,线程将离开lock()
方法,我们需要将其出队。