Writing and testing multithreaded programs has always been difficult. This is the case because often the concurrency mechanism is coupled with program logic. One way to reduce this coupling is to abstract away as much of the concurrency concerns as possible. In this project you will implement a concurrency abstraction. We have provided you with a detailed specification with most of the details for this project. Here, you will find a general overview and we will stress some important issues.
The concurrency abstraction you will be required to write is a compound lock. Such an abstraction can be useful for a wide array of applications. For example, you could use such an abstraction to implement the Spring 2005 Project 4. Note that it is not at all necessary to read that project description to understand this one.
Briefly, a compound lock is a lock which consists of zero or more component locks. Users may lock and unlock (acquire and release) individual component locks. Users may also lock and unlock the global lock (or the "whole" compound lock, if you will.) While the global lock is held, no component locks may be acquired until the global lock is released. While even a single component lock is held, the global lock may not be acquired.
Global lock acquisition requests have priority over component lock acquisition requests. That is, component lock acquisition requests should only complete if the global lock is released and there are no pending global lock acquisition requests. For example, say two threads A and B hold component locks. If a third thread C tries to acquire a component lock at this point, it will succeed. However, if thread D were to attempt to acquire the global lock first, then C's attempt would fail; it would need to wait until the global lock has been acquired and released by D.
You will implement the CompoundLock interface in the source file CompoundLockImpl.java. The CompoundLockImpl class must have a zero-argument constructor.
As was explained in class, you will lose points for improper synchronization, busy-waiting, and over-synchronization.
To facilitate testing, there are a number of restrictions on how you many implement your project. You should use Java intrinsic locks and not the new Java 1.5 java.util.concurrent.locks package (indeed, importing the Lock interface under java.util.concurrent.locks would conflict with the Lock interface we have provided). Using the static MultithreadedTestCase.waitOn(Object o) method rather than o.wait() will allow you to use the skipNextWait method in your test cases. You don't need to do this; we will rewrite your code when testing it. You may, however, use the standard Java notify/notifyAll methods.
We have provided you with a framework for writing multithreaded test cases. You can see an example of using this framework in PublicTests. You should use this framework when writing your StudentTests. One way to use this framework is outlined here.
For each test, or likely for each type of test, you will create an inner class inside StudentTests with the following structure:
private class AnInnerClass extends MultithreadedTestCase {
public AnInnerClass(...) {
...
}
public void initialize() {
...
}
public void thread0() {
...
}
public void thread1() {
...
}
public void finish() {
...
}
}
When you use this class in a test (see below), first
initialize will run and then the testing framework will run
the code in each method whose name is prefixed by thread
(here, thread0 and thread1) in a separate thread.
You may use the regular assortment of JUnit assertTrue,
assertFalse, etc. Lastly, finish will run.
Finally, you will write your tests inside StudentTests, which will look something like:
public void testATest() throws Throwable {
...
TestFramework.runOnce(new AnInnerClass(...));
}
For example, here is an except from the TwoLocksProvideMutualExclusion public test:
static class TwoLocksProvideMutualExclusion extends MultithreadedTestCase {
final Lock lock0, lock1;
public TwoLocksProvideMutualExclusion(Lock lock0, Lock lock1) {
this.lock0 = lock0;
this.lock1 = lock1;
}
public void thread0() {
lock0.lock();
assertEquals(0, getTick());
waitForTick(2);
lock0.unlock();
assertEquals(2, getTick());
}
public void thread1() {
waitForTick(1);
lock1.lock(); // should block here
assertEquals(2, getTick());
waitForTick(3);
lock1.unlock();
assertEquals(3, getTick());
}
}
In tick 0, thread 0 acquires the lock. In tick 1, thread 1 attempts to acquire the lock but should be blocked. In tick 2, thread 0 releases the lock. Thread 0 should complete execution and thread 1 should acquire the lock. In tick 3, thread 1 releases the lock and completes execution.