Search code examples
javamultithreadinglockingthread-synchronization

using tryLock() together with wait() and notify()/notifyAll()


I'm new to threading and I'm trying to do a hybrid approach here. I have below code.

if(lock.tryLock())
{
    try
    {
        //do some actions
        lock.notifyAll(); // error throwing line
    }
    finally
    {
        lock.unlock();
    }
}

If I run the program like this Illegal monitor exception is thrown at that error throwing line. but if I call inside a synchronized block like below it works.

if(lock.tryLock())
{
    try
    {
        //do some actions

        synchronized ( lock )
        {
            lock.notifyAll(); 
        }
    }
    finally
    {
        lock.unlock();
    }
}

My problem is since I have tryLock to true, doesn't it mean that I have already got the lock and I can safely call wait() and notify() methods? Thanks in advance..


Solution

  • Forget the "hybrid approach" here, this doesn't work.

    Every object has an implicit lock. That includes objects of Lock classes like ReentrantLock. Calling wait and notify always uses the implicit lock, these methods don't use the locking capabilities of the Lock object you're calling them on. The wait, notify, and notifyAll methods are declared in Object as native and final.

    To get wait and notify to work you'd have to synchronize on the lock object, and locking done by methods like tryLock would be irrelevant, this would end up as functionally equivalent to final Object lock = new Object();, just more confusing.

    Lock objects have their own equivalents, if you are using a java.util.concurrent.locks.Lock then get a condition from the lock, and call await (which is the equivalent of wait) and signal/signalAll (the equivalent of notify/notifyAll).

    With Lock objects you can have multiple conditions, allowing you to signal subsets of threads waiting for the lock. As a consequence you don't need signalAll anywhere near as much as implicit-locking code needs notifyAll.

    For example, if you look at how ArrayBlockingQueue is implemented, it uses ReentrantLock, and there's one condition for consumers and another condition for producers:

    /** Main lock guarding all access */
    final ReentrantLock lock;
    
    /** Condition for waiting takes */
    private final Condition notEmpty;
    
    /** Condition for waiting puts */
    private final Condition notFull;
    

    constructed with

    public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }
    

    The equivalent code using an implicit lock would have to call notifyAll to avoid losing notifications because we don't know whether the notified thread will be a producer or a consumer, but with separate conditions we know which type of thread will get notified. The dequeueing code, for example, calls signal on the notFull condition, waking up at most one thread:

    /**
     * Extracts element at current take position, advances, and signals.
     * Call only when holding lock.
     */
    private E dequeue() {
        // assert lock.getHoldCount() == 1;
        // assert items[takeIndex] != null;
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        E x = (E) items[takeIndex];
        items[takeIndex] = null;
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        notFull.signal();
        return x;
    }