If a thread is not granted CPU time because other threads grab it all, it is called "starvation". The thread is "starved to death" because other threads are allowed the CPU time instead of it. The solution to starvation is called "fairness" - that all threads are fairly granted a chance to execute.
Causes of Starvation in Java
The following three common causes can lead to starvation of threads in Java:
- Threads with high priority swallow all CPU time from threads with lower priority.
- Threads are blocked indefinately waiting to enter a synchronized block, because other threads are constantly allowed access before it.
- Threads waiting on an object (called wait() on it) remain waiting indefinitely because other threads are constantly awakened instead of it.
Implementing Fairness in Java
public class Synchronizer{
public synchronized void doSynchronized(){
//do a lot of work which takes a long time
}
}
Using Locks Instead of Synchronized Blocks
To increase the fairness of waiting threads first we will change the code block to be guarded by a lock rather than a synchronized block:
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();
}
}
Notice how the doSynchronized() method is no longer declared synchronized. Instead the critical section is guarded by the lock.lock() and lock.unlock() calls.
A simple implementation of the Lock class could look like this:
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();
}
}
A Fair Lock
Below is shown the previous Lock class turned into a fair lock called FairLock. You will notice that the implementation has changed a bit with respect to synchronization andwait()
/notify()
compared to the Lock class shown earlier.
What is important is, that every thread callinglock()
is now queued, and only the first thread in the queue is allowed to lock the FairLock instance, if it is unlocked. All other threads are parked waiting until they reach the top of the queue.
FairLock creates a new instance ofQueueObject
and enqueue it for each thread callinglock()
. The thread callingunlock()
will take the topQueueObject
in the queue and calldoNotify()
on it, to awaken the thread waiting on that object. This way only one waiting thread is awakened at a time, rather than all waiting threads. This part is what governs the fairness of theFairLock
.
A Note on Performance
If you compare theLock
andFairLock
classes you will notice that there is somewhat more going on inside thelock()
andunlock()
in theFairLock
class. This extra code will cause theFairLock
to be a sligtly slower synchronization mechanism thanLock
. How much impact this will have on your application depends on how long time the code in the critical section guarded by theFairLock
takes to execute. The longer this takes to execute, the less significant the added overhead of the synchronizer is. It does of course also depend on how often this code is called.