littleAlien
littleAlien

Reputation: 821

Why Condition creating from Lock, not with 'new' operator?

If Condition could be used separately itself and creating code is merely:

    final ConditionObject newCondition() {
        return new ConditionObject();
    }

why it's not created just this way? In class ArrayBlockingQueue there is code in constructor:

    lock = new ReentrantLock(fair);
    notEmpty = lock.newCondition();
    notFull =  lock.newCondition();

where both notEmpty and notFull are instances of Condition class.

Upvotes: 0

Views: 453

Answers (3)

garykwwong
garykwwong

Reputation: 2427

As mentioned by Slaw, instantiating a new Condition separately just doesn't make sense as it will lost association with the original Lock object, and it would just similar to creating a new Condition from another Lock (because java.util.concurrent.locks.Condition cannot be instantiated separately).

We can refer to below 2 examples to understand more:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionTest {

    final ReentrantLock lock = new ReentrantLock();
    final Condition notEmpty = lock.newCondition();
    final Condition notFull =  lock.newCondition();
            
    String message;
    boolean ready;
    boolean isCompleted;

    public void consume() {
        
        ReentrantLock lock = this.lock;
        lock.lock();
        try { 
        
            while(!ready)
                notEmpty.await();
            
            System.out.println("Received message: " + message);
            ready = !ready; // reverse the "ready" state
            notFull.signal();
            
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally { 
            lock.unlock();
        }
        
    }
    
    public void publish(String message) {
        ReentrantLock lock = this.lock;
        lock.lock();
        try { 
            while(ready)
                notFull.await();
            
            System.out.println("Adding message");
            this.message = message;
            ready = !ready; // reverse the "ready" state
            
            notEmpty.signal();
            
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally { 
            lock.unlock();
        }       
    }
    
    
    public static void main(String[] args) {
        
        ConditionTest ct = new ConditionTest();
        
        Thread msgProducerThread = new Thread(() -> {
            List<String> messages = new ArrayList<String>();
            messages.add("Hello!");
            messages.add("here we get a message");
            messages.add("then we get another message");        
            
            messages.forEach(m -> ct.publish(m));
            
            ct.isCompleted = true;
        });
        
        Thread msgConsumerThread = new Thread(() -> {
            while (!ct.isCompleted)
                ct.consume();
        });
        
        msgProducerThread.start();
        msgConsumerThread.start();
        
    }   
}

Then we would get below result which means the Lock and Condition are functioning properly:

Adding message

Received message: hello

Adding message

Received message: current project is complete

Adding message

Received message: here is the estimation for new project

However, if we try to use separate Condition by replacing the original code:

Condition notEmpty = lock.newCondition();
Condition notFull =  lock.newCondition();

with :

ReentrantLock lock2 = new ReentrantLock(fair);
ReentrantLock lock3 = new ReentrantLock(fair);
Condition notEmpty = lock2.newCondition();
Condition notFull =  lock3.newCondition();

Then, it would get:

Adding message
Exception in thread "Thread-0" Received message: Hello!
Exception in thread "Thread-1" java.lang.IllegalMonitorStateException
    at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.signal(AbstractQueuedSynchronizer.java:1939)
    at ConditionTest.publish(ConditionTest.java:54)
    at ConditionTest.lambda$0(ConditionTest.java:75)
    at java.lang.Thread.run(Thread.java:745)
java.lang.IllegalMonitorStateException
    at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.signal(AbstractQueuedSynchronizer.java:1939)
    at ConditionTest.consume(ConditionTest.java:33)
    at ConditionTest.lambda$1(ConditionTest.java:83)
    at java.lang.Thread.run(Thread.java:745)

Which means there are no relevant association between the Condition and the Lock, which leads to IllegalMonitorStateException being thrown.

Upvotes: 1

Slaw
Slaw

Reputation: 46116

From the documentation of Condition:

Condition factors out the Object monitor methods (wait, notify and notifyAll) into distinct objects to give the effect of having multiple wait-sets per object, by combining them with the use of arbitrary Lock implementations. Where a Lock replaces the use of synchronized methods and statements, a Condition replaces the use of the Object monitor methods.

Conditions (also known as condition queues or condition variables) provide a means for one thread to suspend execution (to "wait") until notified by another thread that some state condition may now be true. Because access to this shared state information occurs in different threads, it must be protected, so a lock of some form is associated with the condition. The key property that waiting for a condition provides is that it atomically releases the associated lock and suspends the current thread, just like Object.wait.

A Condition instance is intrinsically bound to a lock. To obtain a Condition instance for a particular Lock instance use its newCondition() method.

As explained, a Condition instance must be associated with a Lock instance1. Having Lock function as a factory for creating instances of Condition makes perfect sense with that in mind as it implies the relationship between the two. Another way this relationship could have been enforced is to give Condition a constructor which accepts a Lock instance, but since Condition is also an interface it cannot declare constructors. I'm also of the opinion that a no-argument factory method is more user friendly in this case anyway.

Note: If it's not already clear, the ReentrantLock class is an implementation of the Lock interface and the ConditionObject class is an implementation of the Condition interface.

The other problem with attempting to use ConditionObject directly is that it's an inner class (i.e. non-static nested class) of AbstractQueuedSynchronizer2. This means you would need an instance of the latter class in order to create an instance of the former class. However, the implementation of AbstractQueuedSynchronizer used by ReentrantLock is an implementation detail and not exposed to the public. In other words, you have no way to call the constructor of ConditionObject which means the only way to create an instance is via newCondition().

To recap, there's at least three reasons why a factory method is used to create Condition objects:

  1. It makes the relationship between Lock and Condition clear.
  2. With both Lock and Condition being interfaces, you need a way to associate a Condition with a Lock without knowing about the implementations. Otherwise it would not be possible to "program to an interface".
  3. Due to ConditionObject being an inner class it cannot be instantiated directly—at least, not by code which doesn't have access to an instance of the enclosing class.

1. The methods of Condition only make sense in the context of owning a Lock. Just like how a thread must be synchronized on an object before it can legally invoke that object's monitor methods (i.e. wait/notify), a thread must own the associated Lock before it can legally invoke the methods of the Condition (i.e. await/signal).

2. There's also AbstractQueuedLongSynchronizer which declares its own ConditionObject inner class. While the class has the same name as the one declared by AbstractQueuedSynchronizer, the two are actually separate classes.

Upvotes: 1

Kayaman
Kayaman

Reputation: 73568

Locks and Conditions provide a mechanism that was previously (pre 1.5) available only through synchronization, wait/notify and custom condition code.

Here's an idiomatic example

synchronized(foo) {           // Lock
    while(!conditionMet)      // Condition
        foo.wait();           // Condition.signalAll();

    // Condition met, do something and notify
    conditionMet = false;
    foo.notifyAll();
}

It might seem that the condition isn't related to the lock object in any way, but that guarantees thread safety. You can't write the above code without access to the condition being inside the synchronized block. (You can access the condition boolean from non-threadsafe code and mess things up, but you can always mess things up).

The whole ReentrantLock class is actually a functionality wrapper over the Sync class object, which is responsible for the low level synchronization and the creation of conditions. Conditions are bound to a specific Lock (or Sync rather) to basically make sure that they're thread-safe. As the Javadoc for Condition states

Because access to this shared state information occurs in different threads, it must be protected, so a lock of some form is associated with the condition.

Upvotes: 1

Related Questions