The locking package merely consists of two interfaces for a lock manager and a multilevel lock and specific implementations. There is a generic implementation of a multi level lock with a subtle, yet simple compatibility mechanism. This mechanism allows for implementing quite a number of different lock types such as a read write lock which is provided for covenience. As explained earlier the locking agent can be any Java object. Have a look at the simple method for acquiring a lock for read access as a reference.
If you need a single or pre-determined number of classes the generic or read/write lock is all you need. However, sometimes you may not know how many locks and which locks you will need in advance. In this case you can use the lock manager with its default implementation. The most important method is for either creating a non existing lock in a specified resource or retrieving it if it was created before. It is important to make this atomic so there is no chance of accidently creating more than one lock for a single resource.
The whole trick is to have at most one lock for a resource and have different compatible lock levels for different agents acquiring the lock on that resource.
Lets get to the first example which is how Jakarta Slide handles fine grained locking in its WebDAV layer. Slide can store resources in all kinds of stores which most likely will implement their own locks for consistency. However, Slide has generic locking to be certain there are no deadlocks, and the behavior is similar for all stores to provide locking for stores that do not have locks of their own. All locks must be set at the very beginning of a request and released at the end of the request in an atomic block to ban the hazard of deadlocks. Slide does this with a simple synchronized block, but to have something to show here we will exercise it with a multi level lock. Such a lock is very simple and is merely exclusive. It will look like this:
GenericLock exclusive = new new GenericLock("Mutex", 1, new PrintWriterLogger(new PrintWriter(System.out), "Locking", false));
Now we have a lock called "Mutex" with a highest lock level of 1 and a logger that simply writes to stdout. Different lock levels and their compatibility is a special feature of GenericLock and is explained there in detail. As we have one locking level, this also is the only level we can possibly acquire causing an exclusive lock:
exclusive.acquire("Owner", 1, true, true, Long.MAX_VALUE);
This tries to acquire a lock of level one (=exclusive) for the agent "Owner". Parameters three and four tell the lock that it should block the current thread if this lock level can not be acquired and that this lock is reentrant. Reentrant means that the same agent can require lock levels that would be otherwise incompatible. Finally, there is a timeout which is the maximum time the agent will wait for a lock. If a lock still is unavailable after this time the method will return false. In our case the request will (practically) never time out; we just wait until we are served.
Now all requests with fine grained locks will be braced with the above statement and the release statement after all fine grained locks are acquired:
As a single agent ("Owner" in our case) can have at most one lock level on a lock - only the highest will be effective - no more than the agent is required as a parameter.
Coming to the fain grained locks: Slide puts read and write locks on resources accessed by WebDAV requests. As we really have no idea which resources will be accessed until we receive the request we use a lock manager which looks like this:
public final static READ_LOCK = 1; public final static WRITE_LOCK = 2; LockManager lockManager = new GenericLockManager(WRITE_LOCK, new PrintWriterLogger(new PrintWriter(System.out), "Locking", false));
Logs are the same as with the generic lock: they go to stdout. Before explaining more details, let's see how we get a lock from that manager:
GenericLock lock = (GenericLock) lockManager.atomicGetOrCreateLock("/");
This gives us the lock for the resource called "/" which is semantically associated with the root of Slide's resource tree. The highest lock level for the lock has been defined to two using the constant WRITE_LOCK. It is good practice to use constants for the lock levels in order to avoid unreadable code. This returns a lock that could have been created by this statement as well:
GenericLock readWrite = new new GenericLock("/", 2, new PrintWriterLogger(new PrintWriter(System.out), "Locking", false));
However, if we ask the lock manager for this lock the next time it will not create a new lock. Instead it will return the lock created first - preserving all lock levels already set. The assertion that all this will be done atomically is very important for correctness and because of this it is reflected in the name of the method.
The locks created by the above lock manager have two levels where the write level is exclusive and the read level is shared. This means if there is a write lock there can be no read locks and the other way round. However, in the absense of a write lock read locks can happily coexist with other read locks - they are shared - while write locks can not coexist with other write locks - they are exclusive. This is very useful for webdav server locks as a there is no reason not to allow two GET requests to the same resource at the same time. However, you should not allow a PUT and a GET request to the same resource at the same time or two PUT requests. This means that none of the below statements will block
readWrite.acquire("request1", READ_LOCK, true, true, Long.MAX_VALUE); readWrite.acquire("request2", READ_LOCK, true, true, Long.MAX_VALUE);
which would mean two requests called "request1" and "request2" can read the root resource at the same time. Without releasing both of the above locks the following write request will be blocked:
readWrite.acquire("request3", WRITE_LOCK, true, true, Long.MAX_VALUE);
In Slide there is a search method that allows you to query for a set of documents that satisfy certain search conditions. When you execute such a query the result documents and indices will be touched with read access. As we learned before we need to acquire all locks on the documents accessed before starting the actual request to guarantee correctness and eliminate the hazard of deadlocks. Of course there is no way to determine which documents this will be beforehand as that would require anticipating the search results. Effectively, we need a global read lock. Now, while there is a global read lock no other request may be allowed to write concurrently. More than one concurrent search request having a global read lock would be no problem, but all requests that write in any way must acquire an exclusive write lock. Easy, one could say, so we have a lock with levels READ and WRITE which is a simple read/write-lock as we have seen above. However, this is not quite adequate as it will demolish our fine grained locking mentioned before: writes would now be globally exclusive again. So, what we want from our lock is that an unrestricted number of search requests - in absence of any write requests - may happen concurrently. Additionally, an unrestricted number of concurrent write requests - in absence of any search requests - should be allowed as well. Sometimes the solution to a problem becomes obvious as soon as you have written it down - like in or case: we need two read/write locks!
Now a search request would try to acquire those locks before it executes:
public final static SHARED_LOCK = 1; public final static EXCLUSIVE_LOCK = 2; searchLock.acquire("search request", SHARED_LOCK, true, true, Long.MAX_VALUE); writeLock.acquire("search request", EXCLUSIVE_LOCK, true, true, Long.MAX_VALUE);
Note that we now call lock level one SHARED and lock level two EXCLUSIVE instead of READ and WRITE as it fits the semantics of these operations better. Any write request will call these statements
writeLock.acquire("write request", SHARED_LOCK, true, true, Long.MAX_VALUE); searchLock.acquire("write request", EXCLUSIVE_LOCK, true, true, Long.MAX_VALUE);
as the counter part of the above.
That's it, isn't it? No, it is not. You might have already noticed that nothing has been gained. Now all write methods block each other in the searchLock and all search methods block each other in the writeLock! Sigh! This is not what we wanted, we just wanted to disallow any search when writing and any write when searching. The exclusive locks for write and search should have been compatible. This is tough as the compatibility mechanism of the generic lock is based on the order of the levels and the highest simply is exclusive. Adding another lock level to make the formerly highest non-exclusive would only cause a mess as no one would understand anything any more and most important it would still not be compatible to itself. Luckily, next to the additional reentrant compatibility we have already used above there is the so called support compatibility of locks. When acquiring a lock level you can ask the generic lock not to consider lock levels acquired by other locking agents - such as the search method - for compatibility calculation if they have the same lock level you desire. This results in the following code which is the final version:
searchLock.acquire("search request", SHARED_LOCK, true, true, Long.MAX_VALUE); writeLock.acquire("search request", EXCLUSIVE_LOCK, true, GenericLock.COMPATIBILITY_REENTRANT_AND_SUPPORT, Long.MAX_VALUE);
writeLock.acquire("write request", SHARED_LOCK, true, true, Long.MAX_VALUE); searchLock.acquire("write request", EXCLUSIVE_LOCK, true, GenericLock.COMPATIBILITY_REENTRANT_AND_SUPPORT, Long.MAX_VALUE);
As much for our short round trip. More information about this additional compatibility can be found here. If you still need more turn to the mailing list, go through the Javadocs and as the last resort through the sources ;)