Multi-threaded assignment surprises

  A volatile brew is formed by mixing assignment (topic of our previous column—"Assignment surprises," Vol. 3, No. 7) and threads. Perils and surprises lurk within the most innocent-looking statement. This time, we expose those perils and surprises and point out where you need to proceed with due caution if you are to ensure the effective use of locked objects.

Maxim 2: Never Assign To A Locked Object
We'll examine these pitfalls and their unhappy consequences within the setting of an aircraft ground service system. In this system, "checking out" an Airplane object and performing a large number of checks and modifications corresponds to the real-world allocation of an actual airplane to one of a number of service crews for refueling, repair, and refurbishment. For safety reasons, only one service crew is permitted to work on the airplane at the same time.
Because of that limitation, the methods for each service operation acquire exclusive access to the Airplane object for the duration of the task, locking the object with the synchronized statement. This forces all the other crews to have to wait until the airplane has been released for its next service operation.¹


public class ParkingBay {
	Airplane airplane = null;

	public void parkPlane(Airplane airplane) {
		this.airplane = airplane;
	}

	public void mechanicalService() {
		synchronized (airplane) {
			if (airplane.isAirWorthy())
				airplane.carryOutMaintenance();
			else
				airplane = new Airplane();

			airplane.refuel();
	}
	}
}


Let's say that the mechanical service crew is the first to get to work today and is currently checking the airplane's airworthiness. Meanwhile the hospitality crew is waiting for access to the airplane so that they can give it a cleaning and load the next flight's meals.


public void hospitalityService() {
	synchronized (airplane) {
		airplane.loadMeals();
		airplane.vacuum();
		airplane.replaceAirSicknessBags();
	}
}


Because the mechanicalService() method started first, the hospitalityService() method is blocked and will not resume until the mechanicalService() method releases its lock on the Airplane object when it completes.
The advantage of this resource contention scheme is its simplicity. (Its disadvantage is that it does not permit the service crews to work in parallel on the same airplane). Because the Airplane class has so many methods that would require synchronization, the designer of the class chose not to add any synchronization at all within the class. This requires its users to provide these checks themselves at a higher level, as we've done in the ParkingBay class.
This is a perfectly reasonable compromise to make. On the one hand, placing multi-threaded checks in a class may complicate its implementation enormously. On the other hand, controlling access to objects of the class at a higher level places the responsibility for ensuring serialized access onto the users of the class and also reduces the amount of possible sharing of the class because it increases the granularity of the locking scope.
However, despite the simplicity of this scheme, it demonstrates the trickiness of working with multiple threads, as the peril we spoke of is lurking within the mechanicalService() method. The problem arises when the maintenance crew determines that the airplane is not airworthy and decides to replace it with a new plane that the airline keeps for just this sort of contingency:


airplane = new Airplane();


This new instance is not locked-that's okay though, because only the mechanical crew is privy to this exchange of airplanes (the other crews will go about their tasks unaware that the planes have been switched).
But what happens when the blocked hospitalityService() method proceeds, having acquired exclusive access to the Airplane object? Figure 1 shows the sequence of events.
The T0 point on the timeline represents the state of the parking bay just after the airplane has been parked there and before any service crews have started. The instance variable airplane refers to the parked plane (Instance 1).
At T1, the mechanicalService() method is invoked and it acquires an exclusive lock on the Airplane instance using the synchronized statement.
The hospitality crew arrives later at point T2 but discovers that the mechanical crew is not yet finished. The hospitalityService() method attempts to lock the Airplane instance and blocks.
At point T3, the mechanical crew concludes that the airplane is not fit to fly and decides to swap in a replacement. A new Airplane instance (Instance 2) is assigned to the airplane instance variable. The refueling of the new plane is completed and so at point T4, by exiting the synchronized block, the mechanicalService() method releases its lock on the first Airplane instance, the one that was previously referred to by airplane. Now remember, at the point that the mechanicalService() method released its lock the airplane instance variable had already been modified to refer to Instance 2 so the airplane switch should have been transparent to the other crews.
However, the hospitalityService() method has been waiting since the airworthiness checks started to get a lock on the original Airplane instance. At point T5 (which occurs very shortly after the release of the lock at point T4) the hospitalityService() method acquires access to the original Airplane instance on which it synchronized at point T2. Unfortunately, the airplane instance variable now refers to a different Airplane object.
The resulting situation at point T6 is obviously not a desirable one. The Airplane object now referenced by the airplane instance variable (and the one on which the hospitality crew will be working) has not been not locked by the hospitalityService() method, and the one on which that method does hold an exclusive lock is now being melted down in a local foundry.²
Things get worse. Suppose a further ParkingBay method, performTestFlight(), has been invoked in order to check the new airplane's ability to get off the ground after having been mothballed for so long. This method will also attempt to get exclusive access to the airplane by locking the instance now referred to by the airplane instance variable. This will immediately succeed because no thread currently has that instance locked. One hopes that the pilot would think to disconnect the vacuum cleaner power leads trailing out the back door before taxiing off down the runway!
We can trace the origin of this misfortune back to a failure to realize the implications of Java's distinction between object instances and object references. The lock held by a thread is a lock on the instance, not the reference. On the other hand, assignment acts on the reference, not the instance, as we emphasized in our last column ("Assignment surprises," Vol. 3, No. 7). Applied to a locked object in a threaded environment, assignment switches the reference to another instance, while any currently blocked lock requests will eventually be granted on the original instance; hence, our maxim warning you against assigning to a locked object.
There is a solution that will allow a method to gain a lock on the current instance associated with an object reference even when other threads may perform assignment on it. Applied to the hospitalityService() method, it looks like this:


public void hospitalityService() {
	for (;;) {
		Airplane local = airplane;

		synchronized (local) {
			if (local != airplane)
			continue;

			airplane.loadMeals();
			airplane.vacuum();
			airplane.replaceAirSicknessBags();

			break;
		}
	}
}	


We loop around locking the instances referred to by airplane until the locked instance matches the current value of the reference. If assignments to airplane always occur while the instance it refers to is locked, then this method will operate on the latest instance referred to by airplane and that instance will have been locked by the method.
A better solution would be one that avoids needing to make assignments to locked objects in the first place. Accepting that the assignment performs a vital action that cannot be avoided, to solve the problem we will have to eliminate locking on the same object, which brings us to the next maxim.

Maxim 3: Lock With One Class And Assign With Another
Despite its problems, the simplicity of the original scheme has some appeal, as we still want to avoid placing the locking code into the Airplane class. The solution entails taking a lock not just on the right object but on the right object of the right class.
Let's review what went wrong with the airplane servicing. The hospitality crew requested a lock on a particular airplane scheduled for service. By the time that airplane had become available (and the lock granted), it had already been consigned to the scrap heap. The hospitality crew unwittingly carried out their duties on an airplane different from the one they'd locked. The designers of this system could have prevented this mishap if they'd arranged for the lock to be obtained on the parking bay instead of the airplane. This is safer because, even though airplanes may be swapped into and out of service unpredictably, the number of parking bays an airport has is generally fixed. Presumably, the Airport class is defined something like this:


public class Airport {
	private ParkingBay[] parkingBays;

	Airport(int nbParkingBays) {
		parkingBays = new ParkingBay[nbParkingBays];
	}
}


Applied to the ParkingBay class, the solution simply requires, in each method, the replacement of


synchronized (airplane) {


with a lock on the ParkingBay object itself


synchronized (this) {


or simply to make the relevant methods synchronized, which is a functionally equivalent form:


public synchronized void hospitalityService() {
	airplane.loadMeals();
	airplane.vacuum();
	airplane.replaceAirSicknessBags();
}


The problems of assigning to locked objects cannot arise when the this reference is locked or synchronized methods are invoked because assignment to the this reference is not permitted.
The mechanical crew, having carried out their tasks in drastic fashion by substituting the airplane in the parking bay, in due course would have released their lock on that bay. This would have allowed the hospitality crew to enter and lock the parking bay (and the replacement airplane now in it) and obstruct the actions of the test flight crew, who would then be compelled to wait their turn.
This solution works by ensuring that the layer of abstraction that is subjected to synchronization constraints is a different one than the layer at which new instances are assigned to existing references. We achieved this by moving the locking up to a higher level of abstraction—from Airplane class to ParkingBay class.

Maxim 4: Encapsulate Your Locking Mechanisms In Separate Classes
An alternative application of Maxim 3 could separate the locking from the assignment by creating a peer class for the Airplane class that would handle the synchronization issues for it.
To do this we'll need the assistance of a separate locking class. Listing 1 defines a Mutex (mutual exclusion) class that can be used for this purpose.
Using separate locking classes introduces its own problems—most notably that stand-alone locking objects are not automatically unlocked—but is generally a good idea in any non-trivial application. It also provides the opportunity we've been waiting for (no pun intended) to signal that the airplane is not yet ready for the next crew and would allow them to perform some other task other than merely wait for the airplane to become available.


public class ParkingBay {
	static Mutex available = new Mutex();

	public boolean hospitalityService(boolean wait) {
		if (!available.lock(false)) {	// return if
// unavailable
			if (!wait)
				return false;
			available.lock(true);	// block if 
// unavailable
		}

		airplane.loadMeals();
		airplane.vacuum();
		airplane.replaceAirSicknessBags();

		available.unlock();
		return true;
	}
}


Writing thread-safe code requires special vigilance, especially when contention is over objects that may be assigned new instances. An awareness of the difference between object instances and object references is, as always, key. When you face that type of resource contention look for other related objects that can be locked instead so that you will never need to assign to a locked object.


1. Requiring the service crews wait idly for the airplane to become available (in our code, to block in the synchronized statement) is far from ideal. A more sophisticated scheme would allow a service crew to inquire as to the airplane's availability and move on to a different airplane if appropriate.

2. if the hospitalityService() method were to invoke the wait() or notify() methods on the airplane instance variable these methods would throw an IllegalMonitorStateException exception even though the methods would be invoked on the same reference as the one used with the synchronized statement that contains them!

Featured

Most   Popular
Upcoming Events

AppTrends

Sign up for our newsletter.

Terms and Privacy Policy consent

I agree to this site's Privacy Policy.