Lock Starvation

Whenever multiple threads compete for a scarce resource, there’s a danger of starvation. Earlier we discussed this concept in the context of CPU starvation: with a bad choice of scheduling options, some threads never had the opportunity to become the currently running thread and suffered from CPU starvation.

A similar situation is theoretically possible when it comes to locks granted by the synchronized keyword. Lock starvation occurs when a particular thread attempts to acquire a lock and never succeeds because another thread is already holding the lock. Clearly, this can occur on a simple basis if one thread acquires the lock and never releases it: all other threads that attempt to acquire the lock will never succeed and will starve. But lock starvation can be more subtle than that: if there are six threads competing for the same lock, it’s possible that each of five threads will hold the lock for only 20% of the time, thus starving out the sixth thread.

Like CPU starvation, lock starvation is not something most threaded Java programs need to consider. If our Java program is producing a result in a finite period of time, then eventually all threads in the program will acquire the lock, if only because all the other threads in the program have exited. But also like CPU starvation, lock starvation includes the question of fairness: there are certain times when we want to make sure that threads acquire locks in a reasonable order, so that one thread won’t necessarily have to wait for all other threads to exit before it has its chance to acquire a lock.

Consider the case of two threads that are competing for a lock. Assume that thread A acquires the object lock on a fairly periodic basis, as shown in Figure 8.2.

Call graph of synchronized methods; thread A repeatedly calls a synchronized method

Figure 8-2. Call graph of synchronized methods; thread A repeatedly calls a synchronized method

Also assume that the two threads are operating under a timeslicing scheduler that selects a new thread every 500 milliseconds. Here’s what happens at the various points on the graph:

T0

At time T0, both thread A and thread B are in the runnable state, and thread A is the currently running thread.

T1

Thread A is still the currently running thread, and it acquires the object lock when it enters the synchronized block.

T2

A timeslice occurs; this causes thread B to become the currently running thread.

T3

Very soon after becoming the currently running thread, thread B attempts to enter the synchronized block. This causes thread B to enter the blocked state, which in turn causes thread A to become the currently running thread. Thread A continues executing in the synchronized block.

T4

Thread A exits the synchronized block. This causes thread B to enter the runnable state but does not affect the timeslicing of the scheduler, so thread A continues to be the currently running thread.

T5

Thread A once again enters the synchronized block and acquires the lock. Thread B remains in the runnable state.

T6

Thread B once again becomes the currently running thread. It immediately tries to enter the synchronized block, but the lock for the synchronized block is once again held by thread A, so thread B immediately enters the blocked state. Thread A is left to become the currently running thread again, and we are now at the same state we were in at time T3.

It’s possible for this cycle to continue forever, so that even though thread B is often in the runnable state, it can never acquire the lock and actually do useful work.

Clearly this example is a pathological case: the timeslicing must occur only during those time periods when thread A holds the lock for the synchronized block. With two threads, that’s extremely unlikely and generally indicates that thread A is holding the lock almost continuously. With several threads, however, it’s not out of the question that one thread may find that every time it is scheduled, another thread already holds the lock the first wants.

The common pitfall that creates lock starvation is to implement code similar to the following:

public class MyThread extends Thread {
    public void run() {
        while (true) {
            synchronized(someObject) {
                // ... Do some calculations ...
            }
        }
    }
}

public class Test {
    public static void main(String args[]) {
        MyThread t1, t2;
        t1 = new MyThread();
        t2 = new MyThread();
        t1.start();
        t2.start();
    }
}

At first glance, we might expect this code to work just fine, thinking that when thread t1 exits the synchronized block, thread t2 then immediately gets the lock on someObject and the two threads continue alternating the acquisition of the lock. But as we’ve seen, that is not the case: unless the timeslicing occurs during the short interval between the end of the synchronized block (when the lock is released) and the beginning of the next iteration of the loop (when the lock is reacquired), thread t2 will never acquire the someObject lock and will never become the currently running thread. Adding a call to the yield() method will solve this simple case, but it is not a general solution.

There are two points to take away from this:

Acquisition of locks does not queue

When a thread attempts to acquire a lock, it does not check to see if another thread is already attempting to acquire the lock (or, more precisely, if another thread has tried to acquire the lock and blocked because it was already held). In pseudocode, the process looks like this:

while (lock is held)
    wait for a while
acquire lock

For threads of equal priority, there’s nothing in this process that prevents a lock from being granted to one thread even if another thread is waiting.

Releasing a lock does not affect thread scheduling

When a lock is released, any threads that were blocked waiting for that lock are moved from the blocked state into the runnable state. However, no actual scheduling occurs, so none of the threads that have just moved into the runnable state becomes the currently running thread; the thread that has just released the lock remains the currently running thread (again, assuming that all threads had the same priority).

Nonetheless, lock starvation remains, as might be guessed from our example, something that occurs only in rare circumstances. In fact, each of the following circumstances must be present for lock starvation to occur:

Multiple threads are competing for the same lock

This lock becomes the scarce resource for which some threads may starve.

There must be a period of time during which there is not enough CPU time to accommodate all the threads. At least two threads must always be in the runnable state during this time period, or a thread that holds the lock must enter the blocked state while it still holds the lock (which is generally a bad thing).

If there is adequate CPU time to satisfy all threads, and no thread blocks while holding the lock, then a thread that wants to acquire the lock must at some point actually acquire the lock, if only because it’s the only thread in the runnable state.

The results that occur during this period of contention must be interesting to us

If, for example, we’re calculating a big matrix, there’s probably a point in time at the beginning of our calculation during which multiple threads are competing for the same lock and the CPU. But since all we care about is the final result of this calculation, it doesn’t matter to us that some threads are temporarily starved for the lock: we’ll still get the final answer in the same amount of time.

As in the case of CPU starvation, we’re only concerned about lock starvation if there’s a period of time during which it matters that the lock be given out fairly.

These threads must all have the same priority

In the example we discussed earlier, if thread B has a higher priority than thread A, consider what would happen at time T4. When thread B moves from the blocked state to the runnable state because thread A has released the lock, thread B becomes the currently running thread by virtue of its priority.

Of course, if thread A has a higher priority than thread B, thread B would still never get the opportunity to become the currently running thread, but in that case, thread B would be subject to CPU starvation rather than lock starvation.

These threads must be under control of a round-robin scheduler

If the equal-priority threads are not under control of a round-robin scheduler, they are again subject to CPU starvation rather than lock starvation. Note also that this round-robin scheduler must not adjust the priorities of the threads, or the previous rule might be violated. Threads that are under control of the SimpleScheduler in Chapter 7 are subject to lock starvation, as are native threads.

All of the properties of lock starvation stem from the fact that a thread attempting to acquire a lock checks only to see if another thread already holds the lock, and not if another thread is already waiting for the lock. So if we’re in one of those rare situations where lock starvation can occur, we need to develop a lock that has a queue associated with it so that the lock is given out fairly to every thread that wants to acquire the lock.

This is a simple class to write: we can use the Vector class to implement the queue, and then we need only write methods to allow classes to acquire and release the lock. The getBusyFlag() method places requests on the queue, and the freeBusyFlag() method notifies the next thread on the queue that the lock is now available.

Our QueuedBusyFlag class then looks like this:

import java.util.Vector;

public class QueuedBusyFlag extends BusyFlag {
    protected Vector waiters;

    public QueuedBusyFlag() {
        waiters = new Vector();
    }

    public synchronized void getBusyFlag() {
        Thread me = Thread.currentThread();
        if (me == busyflag) {
            busycount++;
            return;
        }
        waiters.addElement(me);
        while ((Thread) waiters.elementAt(0) != me) {
            try {
                wait();
            } catch (Exception e) {}
        }
        busyflag = me;
        busycount = 0;
    }

    public synchronized void freeBusyflag() {
        if (Thread.currentThread() != busyflag)
            throw new IllegalArgumentException(
                                    "QueuedBusyflag not held");
        if (busycount == 0) {
            waiters.removeElementAt(0);
            notifyAll();
            busyflag = null;
        }
        else busycount--;
    }

    public synchronized boolean tryGetBusyflag() {
        if (waiters.size() != 0 && busyflag != Thread.currentThread())
            return false;
        getBusyFlag();
        return true;
    }
}

Although QueuedBusyFlag shares the same interface as the BusyFlag class, we’ve had to reimplement a number of methods. When a thread attempts to acquire a lock, it enters the getBusyFlag() method and puts itself into the waiters vector. It then waits until it is the first element in the waiters vector. Similarly, when a thread releases the lock, it removes itself from the waiters vector and notifies the other threads waiting on the vector that they should check to see if they are now first in line.

This implementation is a little inefficient, in that it relies on the notifyAll() method to wake up the threads waiting to acquire the lock. If there are 30 threads waiting for the lock, all 30 threads will be wakened, even though only one thread will acquire the lock and the other 29 threads will just call the wait() method again. So you only want to use this technique in those special cases when you know that lock starvation will be a problem. We could develop a more efficient implementation by using the targeted notification technique we discussed in Chapter 4; we leave that as an exercise for the reader.

Since it is a BusyFlag, we can use this new class in a predictable fashion:

public class DBAccess {
    private QueuedBusyFlag lock;

    public DBAccess() { 
        lock = new QueuedBusyFlag(); 
    }
    public Object read() {
        Object o;
        try {
            lock.getBusyFlag();
            o = someMethodThatReturnsData();
            return o;
        } finally {
            lock.freeBusyFlag();
        }
    }

    public void write(Object o) {
        try {
            lock.getBusyFlag();
            someMethodThatSendsData(o);
        } finally {
            lock.freeBusyFlag();
        }
    }

There are no surprises to this code: the only difference between running code like this and running code with a standard BusyFlag is that the requests to the database in this case will be granted sequentially, whereas if we used a standard BusyFlag, the requests would be granted in a somewhat random order (depending on the underlying platform).

Reader-Writer Locks

Sometimes you need to read information from an object in an operation that might take a fairly long period of time. You’ll need to lock the object so that the information you read is consistent, but you don’t necessarily need to prevent another thread from also reading data from the object at the same time: as long as all the threads are only reading the data, there’s no reason why they shouldn’t read the data in parallel, since this doesn’t affect the data each thread is reading.

In fact, the only time we need data locking is when the data is being changed; that is, when the data is being written. The change to the data introduces the possibility that a thread reading the data sees the data in an inconsistent state. Until now, we’ve been content to have a lock that allowed only a single thread to access that data whether the thread is reading or writing the data, based on the theory that the lock is only held for a short period of time.

If the lock needs to be held for a long period of time, it makes sense to consider the possibility of allowing multiple threads to read the data simultaneously so that these threads don’t need to compete against each other to acquire the lock. Of course, we must still allow only a single thread to write the data, and we must make sure that none of the threads that were reading the data are still active while our single writer thread is changing the internal state of the data.

Consider the case of a binary tree that contains some sort of information that is designed to be searched quite often by multiple threads. Depending on the amount of information contained in the binary tree, searching for a particular entry may require a long period of time. The interface for such a binary tree might look like this:

public class BTree {
    public synchronized boolean find(Object o) {
        // Perform time-consuming search, returning the object if
        // found or null if the object is not found
    }

    public synchronized void insert(Object o) {
        // Perform a time-consuming insert
    }
}

The problem here is that if two threads call the find() method at the same time, one of them blocks while it waits to acquire the lock; this thread remains blocked for a long time while the first thread continues to perform its search. If these two threads are operating in a timesliced environment, they won’t be able to timeslice since they’re competing for the same single lock; if they’re running on a machine with multiple CPUs, they won’t both be able to execute at the same time on separate CPUs. If this binary tree is part of a server that is to be accessed by multiple clients, we’d really like the threads calling the find() method to operate in parallel.

This is where the reader-writer lock comes in. If we have a lock that allows multiple threads to read a data structure simultaneously, we could use an interface that looks like this:

public class BTree {
    RWLock lock;
    public boolean find(Object o) {
        try {
            lock.lockRead();
            // Perform time-consuming search, returning the object
            // if found or null if the object is not found.
            return answer;
        } finally {
            lock.unlock();
        }
    }

    public void insert(Object o) {
        try {
            lock.lockWrite();
            // Perform a time-consuming insert.
        } finally {
            lock.unlock();
        }
    }
}

We now have the capability of allowing multiple threads to read the binary tree simultaneously, even though the binary tree still can be updated only by a single thread.

The bad news is that the Java API does not provide anything like reader-writer locks; the good news is that writing your own reader-writer lock is not difficult. We’ll now look at a simple implementation of a reader-writer lock:

import java.util.*;

class RWNode {
    static final int READER = 0;
    static final int WRITER = 1;
    Thread t;
    int state;
    int nAcquires;
    RWNode(Thread t, int state) {
        this.t = t;
        this.state = state;
        nAcquires = 0;
    }
}

public class RWLock {
    private Vector waiters;

    private int firstWriter() {
        Enumeration e;
        int index;
        for (index = 0, e = waiters.elements();
                                e.hasMoreElements(); index++) {
            RWNode node = (RWNode) e.nextElement();
            if (node.state == RWNode.WRITER)
                return index;
        }
        return Integer.MAX_VALUE;
    }

    private int getIndex(Thread t) {
        Enumeration e;
        int index;
        for (index = 0, e = waiters.elements();
                                e.hasMoreElements(); index++) {
            RWNode node = (RWNode) e.nextElement();
            if (node.t == t)
                return index;
        }
        return -1;
    }

    public RWLock() {
        waiters = new Vector();
    }

    public synchronized void lockRead() {
        RWNode node;
        Thread me = Thread.currentThread();
        int index = getIndex(me);
        if (index == -1) {
            node = new RWNode(me, RWNode.READER);
            waiters.addElement(node);
        }
        else node = (RWNode) waiters.elementAt(index);
        while (getIndex(me) > firstWriter()) {
            try {
                wait();
            } catch (Exception e) {}
        }
        node.nAcquires++;
    }
    public synchronized void lockWrite() {
        RWNode node;
        Thread me = Thread.currentThread();
        int index = getIndex(me);
        if (index == -1) {
            node = new RWNode(me, RWNode.WRITER);
            waiters.addElement(node);
        }
        else {
            node = (RWNode) waiters.elementAt(index);
            if (node.state == RWNode.READER)
                throw new IllegalArgumentException("Upgrade lock");
            node.state = RWNode.WRITER;
        }
        while (getIndex(me) != 0) {
            try {
                wait();
            } catch (Exception e) {}
        }
        node.nAcquires++;
    }

    public synchronized void unlock() {
        RWNode node;
        Thread me = Thread.currentThread();
        int index;
        index = getIndex(me);
        if (index  > firstWriter())
            throw new IllegalArgumentException("Lock not held");
        node = (RWNode) waiters.elementAt(index);
        node.nAcquires--;
        if (node.nAcquires == 0) {
            waiters.removeElementAt(index);
            notifyAll();
        }
    }
}

The interface to the reader-writer lock is very simple: there’s a method lockRead() to acquire the read lock, a method lockWrite() to acquire the write lock, and a method unlock() to release the lock (only a single unlock() method is required, for reasons we’ll explore in a moment). Just as in our QueuedBusyFlag class, threads attempting to acquire the lock are held in the waiters vector until they are first in line for the lock, but the definition of first in line has changed somewhat.

Because we need to keep track of how each thread wants to acquire the lock—whether it wants to acquire the read lock or the write lock—we need to create a class to encapsulate the information of the thread that made the request and the type of request it made. This is the RWNode class; our waiters queue now holds elements of type RWNode instead of the Thread elements that were present in the QueuedBusyFlag class.

The acquisition of the read lock is the same as the logic of the QueuedBusyFlag class except for the new definition of first in line. First in line for the read lock means that no other node ahead of us in the waiters queue wants to acquire the write lock. If the nodes that are ahead of us in the waiters queue want only to acquire the read lock, then we can go ahead and acquire the lock. Otherwise, we must wait until we are in position zero.

The acquisition of the write lock is stricter: we must be in position for the lock in order to acquire it, just as was required in our QueuedBusyFlag class.

The logic to keep track of the number of times a particular thread has acquired a lock has undergone a slight change. In the QueuedBusyFlag class, we were able to keep track of this number as a single instance variable. Since the read lock can be acquired by multiple threads simultaneously, we can no longer use a simple instance variable; we must associate the nAcquires count with each particular thread. This explains the new logic in both acquisition methods that checks to see if there is already a node associated with the calling thread.

Our reader-writer lock class does not have the notion of “upgrading” a lock; that is, if you hold the reader lock, you cannot acquire the writer lock. You must explicitly release the reader lock before you attempt to acquire the writer lock, or you will receive an IllegalArgumentException. If an upgrade feature were provided, the class itself would also have to release the reader lock before acquiring the writer lock. A true upgrade is not possible.

Finally, our reader-writer lock class contains some helper methods to search the waiters queue for the first node in the queue that represents a thread attempting to acquire the write lock (firstWriter()) and to find the index in the queue of the node associated with the calling thread (getIndex()). We can’t use the Vector class indexOf() method for this purpose because we’d have to pass the indexOf() method an object of type RWNode, but all we have is a thread.

Figure 8.3 shows the state of the waiters queue through several attempts at lock acquisition. Threads that have acquired the lock have a white background, whereas threads that are waiting to acquire the lock have a shaded background; each box notes whether the thread in question is attempting to acquire the read or the write lock.

Reader-writer lock queue

Figure 8-3. Reader-writer lock queue

At point 1, thread T1 has acquired the read lock. Since it is the only thread in the waiters queue, the getIndex() method returns while the firstWriter() method returns MAX_VALUE. Since the index was less than the first writer, the lock is granted. At point 2, thread T2 has requested (and been granted) the read lock based on the same logic. Here’s a point at which two threads simultaneously have the read lock.

At point 3, thread T3 attempts to acquire the write lock. Because the index of T3 in the queue is 2, it cannot grab the lock and instead executes the wait() method inside the lockWrite() method. Then at point 4, thread T1 releases the read lock. The unlock() method calls notifyAll(), which wakes up T3, but because T3’s index in the queue is now 1, it again executes the wait() method.

At point 5, thread T1 again attempts to acquire the read lock, but this time, because its index in the queue (2) is greater than the index of the first writer (1), it does not immediately get the lock and instead executes the wait() method inside the lockRead() method. We might be tempted at this point to allow T1 to acquire the read lock since T2 already has the read lock and we generally allow multiple simultaneous acquisitions of the read lock. But if we implement that logic, we will starve the threads attempting to acquire the write lock: we could have multiple threads acquiring the read lock, and even though they might individually give up the lock frequently, one of them could always prevent a thread from acquiring the write lock. That’s the rationale for always putting the requesting thread into the waiters queue and then testing its index against other threads in the queue, as happens again at point 6.

At point 7, thread T2 releases the read lock, notifying all other threads that the lock is free. Because T3 is a writer lock with an index of 0, the lockWrite() method gives it the lock while the other threads in the lockRead() method execute wait().

Finally, at point 8, thread T3 releases the lock. This time when the two remaining threads are notified that the lock is free, they are both able to acquire it, as their indices are less than MAX_VALUE (the integer returned when there are no threads attempting to acquire the write lock). Once again we have multiple threads that have simultaneous access to the read lock. This is also a case where the notifyAll() method makes it easy to wake up multiple threads at once.

Priority-Inverting Locks

The last example that we’ll look at in this section is the starvation that is associated with priority inversion. On the virtual machines that we’ve looked at, priority inversion is solved by priority inheritance.

But what if we need to use the BusyFlag class to lock at a large scope in our program? How does priority inheritance affect our BusyFlag class? Not surprisingly, it does not have any affect on the behavior of this class, because we are only simulating a lock, and are using Java’s synchronization locks only to protect against the race conditions that occur within this task. Once a BusyFlag is acquired and the getBusyFlag() method exits, the synchronization lock protecting the getBusyFlag() method is released. As far as the Java virtual machine is concerned, no synchronization locks are held at this point.

A low-priority thread that holds a BusyFlag will never have its priority adjusted by the virtual machine if a high-priority flag attempts to acquire the same BusyFlag: because they never attempt to execute the same synchronized method at the same time, the virtual machine is unaware that they are competing with each other at all.

We can easily implement a version of the BusyFlag class that has support for priority inheritance:

public class PriorityBusyFlag extends BusyFlag {
    protected int currentPriority;

    public synchronized void getBusyFlag() {
        while (tryGetBusyFlag() == false) {
            Thread prevOwner = getBusyFlagOwner();
            try {
                int curP = Thread.currentThread().getPriority();
                if (curP > prevOwner.getPriority()) {
                    prevOwner.setPriority(curP);
                }
                wait();
            } catch (Exception e) {}
        }
    }

    public synchronized boolean tryGetBusyFlag() {
        boolean succeed = super.tryGetBusyFlag();
        if (succeed)
            currentPriority = Thread.currentThread().getPriority();
        return succeed;
    }

    public synchronized void freeBusyFlag() {
        if (getBusyFlagOwner() == Thread.currentThread()) {
            super.freeBusyFlag();
            if (getBusyFlagOwner() == null) {
                Thread.currentThread().setPriority(currentPriority);
                notifyAll();
            }
        }
    }
}

Usage of the PriorityBusyFlag class is similar to usage of the BusyFlag class. The two differences are that the requesting thread will raise the priority of the thread that already owns the BusyFlag if the priority of the requesting thread is higher than the priority of the owning thread, and the original priority of the thread will be restored when the BusyFlag is freed.

This behavior is functionally identical to native-threading systems that support priority inheritance. However, in a virtual machine, these details are handled internally. The best that we can do is to use the PriorityBusyFlag class in a cooperative manner by using the setPriority() method. If another thread also changes the priority of threads, or the threads themselves are changing their priority, this cooperative technique will not work.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset