4010-441 Principles of Concurrent System Design
The Banker's Algorithm

This page is accessible at www.se.rit.edu/~se441/Assignments/Bankers_Algorithm.html.

Here are pointers to other discussions of the Banker's Algorithm; the Web is full of them.

Overview

One of the trickiest problems in concurrency is system resource management; the Banker's Algorithm is a classic deadlock free solution that is appropriate when (a) the resources are mutable, (b) processes / threads may request multiple units of a resource (e.g., printers), and (c) each process / thread can determine, at startup, the maximum number of units it needs of the resource (actually, we can relax these restrictions a bit at the cost of some additional complexity, but we won't).

The banker is initially configured with the number of resources it has to loan. Newly created threads register their maximum resource claim; obviously, this cannot exceed the number of resources the banker has. For each registered thread the banker maintains the maximum claim and the number of resources on loan to the thread (initially 0).

Once registered, a thread can request and release resources. We will ignore the details of how specific units (e.g., specific printers) are associated with a thread; all we care about is the following:

The information above defines the state of the system from the perspective of the Banker. Note the following state invariants:

  1. The unallocated units plus the sum of the units allocated to each thread equals the total number of units available.
  2. For each thread, its allocation plus its remaining claim equals its maximum claim.

Safe States

The design of the system revolves around the notion of a safe state. In a safe state, there is some order in which threads could repay all outstanding loans (return all allocated resources) even if every thread requests all the remaining resources it can claim. Any other state is unsafe, as it may lead to deadlock (deadlock is not certain, because threads are not required to request all the resources in their claim). Below are examples of safe and unsafe states in a system with two threads and four allocatable resources. The columns are (C)laim (as initially sent to the banker), (A)llocated and (R)emaining claim. Note that, for each thread, C = A + R is an invariant.

State 1
Thread C A R
T_1 4 1 3
T_2 3 2 1

The banker has 1 unit out of 4 on hand.

The state is safe because all loans can be repaid the the following order:
  • T_2 has can satisfy its remaining claim with the unit on hand, when it can repay the 2 allocated units, resulting in 3 units on hand.
  • T_1 can request its 3 additional units, which can be satisfied by the 3 on hand; eventually it repays its loan and the banker has all 4 units..

Thus state 1 is safe.

Assume T_2 requests one unit; if granted, the system state would look like:

State 2a
Thread C A R
T_1 4 1 3
T_2 3 3 0

The banker has 0 units out of 4 on hand.

The state is safe because all loans can be repaid if threads are run in the the following order:
  • T_2 has all the resources it can claim; thus eventually it will repay the 3 on hand and the banker will have 3 units.
  • T_1 can request its 3 additional units, which can be satisfied by the 3 on hand; eventually it repays its loan and the banker has all 4 units..

Thus, state 2a is also safe and the request can proceed.

Now assume we are in state 1 again and T_1 requests 1 unit; if granted, the system state would look like:

State 2b
Thread C A R
T_1 4 2 2
T_2 3 2 1

The banker has 0 units out of 4 on hand.

The state is unsafe; if both T_1 and T_2 proceed to request their remaining claims the banker will not be able to satisfy the requests and the system will deadlock.

We want to keep the banker is a safe state at all times:
  1. The initial state, where no threads have registered a claim, is safe.
  2. If the system is in a safe state and  new thread registers a claim for no more than the available resources, the resulting state is safe (in the worst case we can run all the threads in the original state to completion, and then run the new thread to completion).
  3. Thus, the only time we can create an unsafe state is when a thread attempts to acquire more resources (up to its remaining claim). In this case, we "pretend" to allocate the resource(s) by creating a copy of the current state, and altering the copy to reflect an increase of the requesting thread's allocation (and decrease of its remaining claim) by the number of units requested.
  4. Run the bankers algorithm (see below) on the pretend sate, and
    1. If the pretend state is safe, use it as the current state and return the resources to the requester.
    2. If the pretend state is unsafe, block the requester until some resources are released, and repeat the "pretend" allocation again.
  5. If the system is in a safe state and a thread releases some resources, the resulting state is safe (why?). As this may allow a blocked requester to get the resources it needs, we unblock any and all blocked requesters in the hope that at least one will be able to proceed.

The Banker's Algorithm: Determining State Safety

Inputs:
(Copy of) the numberOfUnitsOnHand for the Banker to allocate.
(Copy of) a map from Threads to the pair of integers representing each Thread's currentAllocation and remainingClaim.
Output:
    true if the input state is safe, false otherwise.
Algorithm sketch:
    Create an array holding the allocation/remaining claim pairs (the identity of the threads is irrelevant to safety).
    Sort the array by increasing order of remaining claim.

    For i in 0 to array.length - 1 Do

        // If there are not enough units for the ith thread's claim, then it cannot be guaranteed to
        // complete. Because the array is sorted, no thread after i can be guaranteed to complete,
        // so we have possible deadlock.

        If array[i].remainingClaim > numberOfUnitsOnHand Then    // Too few units remain
            Return false
        End

        // There are enough resources on hand so that we could run this thread until it releases all its
        // resources, in which case we'd reclaim them and advance to the array entry for the next thread.

        numberOfUnitsOnHand += array[i].currentAllocation
    End

    // We get here if it is possible for all threads to complete.
    Return true

Design

Banker Class

Create a passive (non-threaded) Banker class with constructors and methods as specified below. Of course, in developing the Banker class you'll need to find some way to map each registered thread to its current allocation and remaining claim. This will also require appropriate synchronization within the class which is missing from the information below.
public Banker(int nUnits)
Initialize the new Banker object to manage nUnits units of resource.
No threads are registered with the Banker.
public void setClaim(int nUnits)
The current  thread (available via static method Thread. currentThread()) attempts to register a claim for up to nUnits units of resource.
public boolean request(int nUnits)
The current thread requests nUnits more resources.
public void release(int nUnits)
The current thread releases nUnits resources.
public int allocated( )
Returns the number of units allocated to the current thread.
public int remaining( )
Returns the number of units remaining in the current thread's claim.

Client Class

The Client class extends Thread; multiple Client objects content for the pool of resources by calling methods in a shared Banker object.

public Client(String name, Banker banker, int nUnits, int nRequests, long minSleepMillis, long maxSleepMillis)
Initialize the new Client  thread object. The name string is passed to the super (Thread) class's constructor. The remaining 5 arguments should be saved in instance variables of the same names for use when the thread is started and run( ) is called.
public void run( )
Register a claim for up to nUnits of resource with the banker. Then create a loop that will be executed nRequests times; each iteration will either request or release resources by invoking methods in the banker. When the loop is done, release any units still allocated and simply return from run( ) - this will terminate the client thread.

Driver Class

The driver class contains the public static void main( ... ) method that (a) creates a Banker object, (b) creates several Client objects, (c) starts all of the Clients, and (d) waits for all the clients to complete via the instance method join( ). With proper setting of the configuration parameters you should be able to produce runs where a Client is delayed because granting its request would lead to an unsafe state.

The Driver should also contain a set of well-documented final private static int variables that specify the configuration parameters (number of resources for the Banker, number of Clients, arguments to the Client constructors, etc.).  I may use these to experiment with your solution to see if I can break it.

Deliverables

Submit three files (and only three files), Driver.java, Client.java, and Banker.java to the Banker Activity dropbox by due date. Of the 10 points available for this activity:

In all cases, points assigned represent your instructor's judgement as a professional engineer, and are not subject to negotiation.