3

I am looking at some code that is causing an issue (Deadlock) in Java 6 and above, but not in Java 1.5.

BMP Bean:

private MyClass m_c;
public String ejbCreate(String id) throws CreateException, MyException
{
    try
    {
        m_c = Singleton.getInstance().getObj(id);
    }
    catch (MyException e)
    {
        synchronized (Singleton.getInstance())
        {
            //check again
            if (!Singleton.getInstance().hasObj(id)) {
                m_c = new MyClass(id);
                Singleton.getInstance().addObj(id, m_c);
            }
            else {
                m_c = Singleton.getInstance().getObj(id);
            }
        }
    }
}

Singleton:

private Map objCache = new HashMap();
private static Singleton INSTANCE = new Singleton();
public static Singleton getInstance() {
    return INSTANCE;
}
public void addObj(String id, MyClass o)
{
    if (this.objCache.containsKey(id)) {
        this.objCache.remove(id);
    }
    this.objCache.put(id, o);
}

public MyClass getObj(String id) throws Exception
{
    MyClass o = null;
    o = (MyClass)this.objCache.get(id);
    if (o == null) {
        throw new MyException("Obj " +id+ " not found in cache");
    }
    return o;
}

public boolean hasObj(String id)
{
    return this.objCache.containsKey(id);
}

The empirical evidence so far shows that putting synchronization round the whole try/catch resolves the deadlock when using Java 6.

Clearly there can be one or more threads calling

Singleton.getInstance().getObj(id) 

without obtaining the lock whilst another thread has the lock and is executing the code in the synchronized block, but even after considering memory synchronization detailed in JSR-133, it doesn't look like there should be any issues in this scenario.

I am aware that I haven't explained what the issue is apart from saying it is a deadlock and that it is not ideal to paint only a prat of the picture but to paint the whole picture would take a very big canvas.

I have looked at the notes for Java 6 release and the only area that sounds relevant is around uncontended synchronization, but I do not know if that is significant in this case.

Thank you for any help.

Paul
  • 85
  • 8
  • 2
    To get a deadlock you need to be attempting to get two or more locks at once in different threads. One lock alone will not deadlock. Can you show use the other lock and how those locks can be obtained in a different order? – Peter Lawrey Mar 04 '16 at 16:26
  • 1
    Note: there is an issue where you can go into an infinite loop if you update a Map in an unsafe manner. This has always been there. I would consider making the whole code thread safe, and if you need concurrent access, use a ConcurrentMap (added in Java 5.0) – Peter Lawrey Mar 04 '16 at 16:28
  • 1
    btw, if you have a true deadlock, and it's all from using the built-in `synchronized` keyword, then a thread dump will actually detect that deadlock and tell you about it. If you're not seeing that, it's strong evidence for Peter's theory. – yshavit Mar 04 '16 at 16:33
  • That’s symptomatically. You are performing several unnecessary hash lookups right after another, but trying to improve performance via double checked locking anti-pattern. Look at `addObj`; it does `if(map.containsKey(key)) { map.remove(key); } map.put(key, …);`, performing three hash lookups for doing what a single `map.put(key, …);` does anyway. Or, what will happen in `ejbCreate` when the key is not present, first, an unsynchronized `get`, then a synchronized `hasObj` (`containsKey`), followed by `addObj` (another `containsKey`, followed by `put`) making a total of four lookups… – Holger Mar 04 '16 at 17:22
  • This code is definitely broken due to double checked locking, however, a *deadlock* with two locks involved can’t happen in a piece of code having only one lock, but that code obviously isn’t your real code anyway, as it doesn’t even compile, due to unhandled/undeclared exceptions and missing return statements, etc. – Holger Mar 04 '16 at 17:35
  • to debug deadlocks or livelocks you should either use a debugger or at least get thread dumps, e.g. via `jstack`. that's far simpler than speculating about parts of your code in relation to other, unseen parts. – the8472 Mar 04 '16 at 22:37

2 Answers2

4

I suspect you are not getting a deadlock (holding two locks in two different threads obtained in a different order), but rather going into an infinite loop. This can happen with HashMap if you are accessing it in a manner which is not thread safe. What happens in the linked list used to handle collisions appears to go back on itself and the reader runs forever. This has always been an issue, though some subtle difference in Java 6 could show up this problem when a different version might not.

I suggest you fix this class so it uses a thread safe collection and not retry on Exception because there is not guarantee this will happen.

There is a lot you could do to improve this class but what you really need is ConcurrentMap.computeIfAbsent added in Java 8.

Note: there is no reason to

  • check a key exists before attempting to remove it.
  • remove a key just before attempting to put it.
  • throw an Exception instead of returning null.
  • returning null when you can pass it a factory. (as per computeIfAbsent)
  • use a factory when the type is known in advance.

I suggest you

  • use a ConcurrentMap for thread safe concurrent access.
  • use an enum for a Singleton.

Both of these were added in Java 5.0.

public enum MyClassCache {
    INSTANCE;

    private final Map<String, MyClass> cache = new ConcurrentHashMap<>();

    public boolean hasId(String id) {
        return cache.containsKey(id);
    }

    public MyClass get(String id) throws IllegalStateException {
        MyClass ret = cache.get(id);
        if (ret == null) throw new IllegalStateException(id);
        return ret;
    }

    public MyClass getOrCreate(String id) throws IllegalStateException {
        MyClass ret = cache.get(id);
        if (ret == null) {
            synchronized (cache) {
                ret = cache.get(id);
                if (ret == null) {
                    cache.put(id, ret = new MyClass(id));
                }
            }
        }
        return ret;
    }
}

In Java 8 you can use computeIfAbsent

public MyClass getOrCreate(String id)  {
    return cache.computeIfAbsent(id, MyClass::new);
}
Peter Lawrey
  • 525,659
  • 79
  • 751
  • 1,130
  • Hi Peter. Thank you for replying. It is most definitely a deadlock we are seeing - an org.jboss.util.deadlock.ApplicationDeadlockException. We have looked at the threads and it is indeed a classic deadlock - Thread 1 has A and wants B, Thread 2 has B and wants A. – Paul Mar 04 '16 at 16:48
  • @Paul can you show us the two code paths which are deadlocking. – Peter Lawrey Mar 04 '16 at 16:49
  • 2
    @Paul: in the code you have shown, there is only one lock involved, so if there is a deadlock with two locks involved, your question is incomplete, at best. – Holger Mar 04 '16 at 17:25
1

Am I right that the core of this question is the difference between:

public void ejbCreate1(String id) throws Exception {
    try {
        m_c = Singleton.getInstance().getObj(id);
    } catch (Exception e) {
        synchronized (Singleton.getInstance()) {
            //check again
            if (!Singleton.getInstance().hasObj(id)) {
                m_c = new MyClass(id);
                Singleton.getInstance().addObj(id, m_c);
            } else {
                m_c = Singleton.getInstance().getObj(id);
            }
        }
    }
}

and

public void ejbCreate2(String id) throws Exception {
    synchronized (Singleton.getInstance()) {
        try {
            m_c = Singleton.getInstance().getObj(id);
        } catch (Exception e) {
            //check again
            if (!Singleton.getInstance().hasObj(id)) {
                m_c = new MyClass(id);
                Singleton.getInstance().addObj(id, m_c);
            } else {
                m_c = Singleton.getInstance().getObj(id);
            }
        }
    }
}

in Java-6 that can cause the first to hang and the second to work fine.

Clearly the primary difference is that getObj might be called by two different threads at the same time, and may even be called while another threads is creating the new object.

From Is it safe to get values from a java.util.HashMap from multiple threads (no modification)? it is likely that you are not in that situation. Conclusion is that one thread is readng from the Map (perhaps o = (MyClass) this.objCache.get(id);) while another is writing to the map by calling addObj. This is clearly a recipe for the read to crash and burn.

See Is a HashMap thread-safe for different keys? for details about the potential sinkholes.

Community
  • 1
  • 1
OldCurmudgeon
  • 64,482
  • 16
  • 119
  • 213