'. '

Java Monitor

From APIDesign

Revision as of 10:00, 11 February 2009 by JaroslavTulach (Talk | contribs)
Jump to: navigation, search

The Java synchronization model with all synchronized, notify and wait its is commonly thought of as an example of monitors. However with closer look one finds out that it is far different to the model used in Concurrent Pascal. It is not that robust, it is not well enough integrated to the language and sort of feels like an assembly language with synchronization primitives than high-level abstraction provided by real monitor. However, it has yet another hidden flaw, kind of example of Copy Based Design problem: the Java Monitors implant the monitor into object oriented language, in an object oriented style and that does not work.

Imagine there is an Cache support class in some API:

Code from Cache.java:
See the whole file.

public abstract class Cache<From,To> {
    private Map<From,To> cache;
 
    protected abstract To createItem(From f);
 
    public final To get(From f) {
        To t = inspectValue(f);
        if (t != null) {
            return t;
        }
        To newT = createItem(f);
        return registerValue(f, newT);
    }
 
 
    private synchronized To inspectValue(From f) {
        if (cache == null) {
            cache = new HashMap<From, To>();
        }
        return cache.get(f);
    }
 
    private synchronized To registerValue(From f, To newT) {
        To t = cache.get(f);
        if (t == null) {
            cache.put(f, newT);
            return newT;
        } else {
            return t;
        }
    }
}
 

Its synchronization is sufficiently well designed. It never calls foreign code while holding own lock, which is one of necessary Deadlock Conditions. As such one would expect that there is no way to deadlock on the Caches lock. However that would be false assumption. Simple call like:

Code from CacheTest.java:
See the whole file.

int value =  cache.get("123");
assertEquals("3*10=30", 30, value);
 

can lead to deadlock while waiting to enter internal(!) Cache's monitor:

Thread Test Watch Dog: testDeadlockViaAPI
 org.apidesign.javamonitorflaws.Cache.inspectValue:32
 org.apidesign.javamonitorflaws.Cache.get:22
 org.apidesign.javamonitorflaws.CacheTest.testDeadlockViaAPI:134

Moreover this all is achievable using the best Java synchronization practices. Just imagine there is a subclass of the Cache class which defines few properties and for better or worse guards them as Java Monitor:

Code from MultiplyCache.java:
See the whole file.

public class MultiplyCache extends Cache<String,Integer>
implements CacheTest.CacheToTest {
    private PropertyChangeSupport pcs;
    private int multiply;
    public static final String PROP_MULTIPLY = "multiply";
 
    public synchronized int getMultiply() {
        return multiply;
    }
    public synchronized void setMultiply(int multiply) {
        int oldMultiply = this.multiply;
        this.multiply = multiply;
        pcs.firePropertyChange(PROP_MULTIPLY, oldMultiply, multiply);
    }
 
    public synchronized void addPropertyChangeListener(
        PropertyChangeListener listener
    ) {
        if (pcs == null) {
            pcs = new PropertyChangeSupport(this);
        }
        pcs.addPropertyChangeListener(listener);
    }
    public synchronized void removePropertyChangeListener(
        PropertyChangeListener listener
    ) {
        if (pcs != null) {
            pcs.removePropertyChangeListener(listener);
        }
    }
 
    @Override
    protected Integer createItem(String f) {
        return f.length() * multiply;
    }
}
 

This code is not bulletproof. It notifies its listeners while holding own lock and as such this can lead to deadlocks. However it is common that people do these sort of mistakes when using an API. The API shall be ready to handle clueless clients. It is acceptable to let the clients of an API deadlock on their own locks, yet there is no reason to block the API itself. Yet this is the case:

Code from CacheTest.java:
See the whole file.

private static void testDeadlockViaAPI(final CacheToTest cache)
throws Exception {
    class ToDeadlock implements Runnable, PropertyChangeListener {
        int lastMultiply;
 
        public void run() {
            cache.setMultiply(10);
        }
        public void propertyChange(PropertyChangeEvent evt) {
            try {
                storeMultiply();
            } catch (InterruptedException ex) {
                // ok
            }
        }
 
        private synchronized void storeMultiply()
        throws InterruptedException {
            lastMultiply = cache.getMultiply();
            // simulates "starvation"
            wait();
        }
 
        public void assertMultiplyByTen() {
        }
    }
    ToDeadlock toDeadlock = new ToDeadlock();
    cache.addPropertyChangeListener(toDeadlock);
    Thread t = new Thread(toDeadlock, "Deadlock using API");
    t.start();
 
    Thread.sleep(100);
 
    int value =  cache.get("123");
    assertEquals("3*10=30", 30, value);
}
 

Simple call to get("123") deadlocks. Why? Because the Java Monitors implant the monitor concept into object oriented world and in the process of doing so break the most fundamental assumption: the monitor shall be encapsulated. One shall always define all internal data structures and operations working on them in one monitor. This is not the case of Java's synchronized methods at all. By default, the Cache and its subclass MultiplyCache share the same monitor lock!

Implications for API Design

TBD: Use internal locks

Personal tools
buy