Java Monitor

From APIDesign

(Difference between revisions)
Jump to: navigation, search
(Implications for API Design)
Line 30: Line 30:
=== Implications for [[API]] Design ===
=== Implications for [[API]] Design ===
-
TBD: Use internal locks
+
Now, after blaming [[Java]] of having wrong synchronization model, it is our turn to explain what implications does this have for [[API]] design. Are the above described problems reason why one cannot write good [[API]] in [[Java]]? No, not at all. One just cannot do it [[Cluelessness|cluelessly]], one cannot use the most obvious tool - '''synchronized''' methods - when writing a shared library.
 +
 
 +
Exposing locks in an [[API]] has its own obligations. Other people can obtain them and the [[API]] has to know what to do that. Having some global, publicly accessible locks is a way to ask for problems with [[deadlock]]s.
 +
 
 +
Using ''synchronized'' methods in classes accessible to [[API]] clients also exposes these monitors to public. Moreover often unconsciously, which is even more dangerous. The fix is simple. Just hide the locks as private objects:
 +
 
 +
// TBD
 +
 
 +
With a change like this the call
 +
 
 +
<source lang="java" snippet="monitor.pitfalls.brokencall"/>
 +
 
 +
can never [[deadlock]] again, regardless how bad [[API]] clients one has.

Revision as of 06:08, 12 February 2009

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

Now, after blaming Java of having wrong synchronization model, it is our turn to explain what implications does this have for API design. Are the above described problems reason why one cannot write good API in Java? No, not at all. One just cannot do it cluelessly, one cannot use the most obvious tool - synchronized methods - when writing a shared library.

Exposing locks in an API has its own obligations. Other people can obtain them and the API has to know what to do that. Having some global, publicly accessible locks is a way to ask for problems with deadlocks.

Using synchronized methods in classes accessible to API clients also exposes these monitors to public. Moreover often unconsciously, which is even more dangerous. The fix is simple. Just hide the locks as private objects:

// TBD

With a change like this the call

Code from CacheTest.java:
See the whole file.

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

can never deadlock again, regardless how bad API clients one has.

Personal tools
buy