Programming Java threads in the real world, Part 7

Singletons, critical sections, and reader/writer locks

1 2 3 4 Page 3
Page 3 of 4
Listing 2 (/src/com/holub/asynch/JDK_11_unloading_bug_fix.java): Fixing the 1.1 JDK's unloading problem
01  
02  
package com.holub.asynch;
/**
 |    
(c) 1999, Allen I. Holub.

This code may not be distributed by yourself except in binary form, incorporated into a java .class file. You may use this code freely for personal purposes, but you may not incorporate it into any commercial product without getting my express permission in writing.

This class provides a workaround for a bug in the JDK 1.1 VM that unloads classes too aggressively. The problem is that if the only reference to an object is held in a static member of the object, the class is subject to unloading, and the static member will be discarded. This behavior causes a lot of grief when you're implementing a singleton. Use it like this:

   class Singleton
    {   private Singleton()
        {   new JDK_11_unloading_bug_fix(Singleton.class);
        }
        // ...
    }

In either event, once the "JDK_11_unloading_bug_fix" object is created, the class (and its static fields) won't be unloaded for the life of the program.

 */
03  
04  
05  
06  
07  
08  
09  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  
21  
public class JDK_11_unloading_bug_fix
{
   public JDK_11_unloading_bug_fix( final Class the_class )
    {   
        if( System.getProperty("java.version").startsWith("1.1") )
        {
            Thread t = new Thread()
           {   private Class singleton_class = the_class;
               public synchronized void run()
                {   try{ wait(); }catch(InterruptedException e){}
                }
            };
            t.setDaemon(true);  // otherwise the program won't shut down
            t.start();
        }
    }
}

Reader/writer locks

And now for something completely different...

Controlling access to a shared resource such as a file or a data structure in a multithreaded environment is a commonplace problem. Typically, you'd like to allow any number of threads to simultaneously read from or otherwise access a resource, but you want only one thread at a time to be able to write to or otherwise modify the resource. That is, read operations can go on in parallel, but write operations must be serialized -- and reads and writes can't go on simultaneously. Moreover, it's nice if the write requests are guaranteed to be processed in the order they are received so that sequential writes to a file, for example, are indeed sequential.

The simplest solution to this problem is to lock the entire data structure -- just synchronize everything. But this approach is too simplistic to be workable in the real world. With most resources (such as data structures and file systems), there's absolutely no problem with multiple threads all accessing a shared resource simultaneously, provided the resource isn't modified while it's being accessed. If the "read" operations were all synchronized methods, though, no thread could read while another was in the process of reading: You'd effectively serialize the read operations.

This problem is solved using a reader/writer lock. An attempt to acquire the lock for reading will block only if any write operations are in progress, so simultaneous read operations are the norm. An attempt to acquire the lock for writing will block while ether read or write operations are in progress, and the requesting thread will be released when the current read or write completes. Write operations are serialized (on a first-come, first-served basis in the current implementation), so that no two writing threads will be permitted to write simultaneously. Readers who are waiting when a writer thread completes are permitted to execute (in parallel) before subsequent write operations are permitted.

Listing 3 implements a reader/writer lock that behaves as I've just described. Generally, you'll use it like this:

public class Data_structure_or_resource
{
    Reader_writer lock = new Reader_writer();
   public void access( )
    {       try
        {   lock.request_read();
                // do the read/access operation here.       }
        finally
        {   lock.read_accomplished();
        }
    }
   public void modify( )
    {       try
        {   lock.request_write();
                // do the write/modify operation here.       }
        finally
        {   lock.write_accomplished();
        }
    }
}

I've also provided nonblocking versions of request_write() (request_immediate_write(), Listing 3, line 65) and request_read() (request_immediate_read(), Listing 3, line 24), which return error flags (false) if they can't get the resource, but these are not used as often as the blocking forms.

The implementation logic is straightforward, and requires a surprisingly small amount of code. (Most of Listing 3 is made up of comments and a test routine.) I keep a count of the number of active readers -- readers that are in the process of reading (active_readers (Listing 3, line 8)). This count is incremented when a reader requests the lock, and is decremented when the reader releases the lock. If a writer thread comes along and requests access to the resource while reads are in progress, we have to wait for the active readers to finish before the writer can be let loose. A lock is created (on line 49), and the requesting thread is made to wait() on that lock. These locks are queued up in the writer_locks linked list (Listing 3, line 12). If any additional reader threads come along while a writer is waiting, they are blocked (by a wait() on line 20) until the current batch of readers and the waiting writer have finished. (The waiting_readers field [Listing 3, line 9] keeps track of how many readers are blocked, waiting for access.) Same goes with additional writers that come along at this point; they're just added to the queue of waiting writers, blocked on a roll-your-own lock.

As the readers finish up, they call read_accomplished() (Listing 3, line 32), which decrements the active_readers count. When that count goes to zero, the first writer in the queue is released. That thread goes off and does its thing, then it calls write_accomplished() (Listing 3, line 74). If any readers have been patiently waiting while all this is going on, they're released all at once at this point (they're all waiting on the current Reader_writer object's internal condition variable). When that batch of readers finishes reading, the process just described is repeated, and the next batch of readers is released. If no readers are waiting when a writer completes, then the next writer in line is released.

1 2 3 4 Page 3
Page 3 of 4