The ultimate superclass, Part 3

My previous post in this three-part series on the java.lang.Object class and its methods covered finalize(), getClass(), and hashCode(). In this post, you explore toString(), the wait/notification methods, and Object's methods in an interface and Java 8 context.

String representation

Q: What does the toString() method accomplish?

A: The toString() method returns a string representation of the object on which this method is called. The returned string is useful for debugging purposes.

Q: What does the string representation look like when toString() isn't overridden?

A: When toString() isn't overridden, the string representation is returned in the format classname@hashcode, where hashcode is presented in hexadecimal. For example, given a hypothetical Employee class, toString() might return Employee@1c7b0f4d.

Q: Can you provide me with an example on how to override toString()?

A: Check out Listing 1.

Listing 1. Returning a non-default string representation

public class Employee
{
   private String name;
   private int age;

   public Employee(String name, int age)
   {
      this.name = name;
      this.age = age;
   }

   @Override
   public String toString()
   {
      return name + ": " + age;
   }
}

Listing 1 presents an Employee class with private name and age fields, which are initialized by the constructor. It also overrides toString() to return a String object composed of these fields' values and a colon separator.

Q: How do I obtain the string representation?

A: Given an object reference, call toString() on this reference. For example, assuming that emp contains an Employee reference, call emp.toString() to return this object's string representation.

Q: What is the difference between System.out.println(o.toString()); and System.out.println(o);?

A: Each of System.out.println(o.toString()); and System.out.println(o); obtains and outputs object o's string representation. System.out.println(o.toString()); explicitly calls toString(), whereas System.out.println(o); implicitly calls toString().

Waiting and notification

Q: What do the wait(), notify(), and notifyAll() methods accomplish?

A: The wait(), notify(), and notifyAll() methods allow threads to coordinate their actions. For example, one thread might produce an item that another thread consumes. The producing thread should not produce an item before the previously produced item is consumed. Instead, it should wait until it's notified that the item was consumed. Similarly, the consuming thread should not attempt to consume a nonexistent item. Instead, it should wait until it's notified that the item is produced. These methods support this coordination. Essentially, one thread waits for a condition to occur (e.g., an item to be produced) and another thread notifies the waiting thread after creating the condition (e.g., by producing an item).

Q: What is the difference between the various wait() methods?

A: The no-argument wait() method causes the calling thread to wait until another thread calls the notify() method or the notifyAll() method on the same object (i.e., the object on which wait() was called). In contrast, the wait(long timeout) and wait(long timeout, int nanos) methods wait for notification or for the specified timeout to occur, whichever happens first.

Q: What is the difference between the notify() and notifyAll() methods?

A: The notify() method wakes up one of the waiting threads (the woken thread is arbitrarily chosen), whereas notifyAll() wakes up all waiting threads.

Q: What happens after a thread has been woken up?

A: After a thread has been woken up, it's unable to proceed until the current thread relinquishes the lock on the object (on which notify() or notifyAll() was called). The awakened thread competes in the usual manner with any other threads that might be actively competing to synchronize on this object. With notifyAll(), either none of the awoken threads or only one of these threads will be able to proceed.

Q: Why is it necessary to call the waiting and notification methods in a synchronized context?

A: It's necessary to call the waiting and notification methods in a synchronized context to avoid a race condition. Consider that a waiting thread confirms that a condition doesn't exist (usually by checking a variable) before calling wait(), and another thread sets this condition (by setting the same variable) before calling notify(). Given this scenario, the race condition occurs as follows:

  1. The first threads tests the condition and verifies that it needs to wait.
  2. The second thread sets the condition.
  3. The second thread calls notify(). Because the first thread isn't waiting at this point, this call accomplishes nothing.
  4. The first thread calls wait(). It doesn't wake up because it didn't receive the notification.

Q: What happens when these methods are called outside of a synchronized context?

A: When these methods are called outside of a synchronized context, they throw java.lang.IllegalMonitorStateException.

Q: What happens when wait() is called inside a synchronized context?

A: When wait() is called inside a synchronized context, it releases the lock that was obtained by the calling thread upon entering the synchronized context prior to this method being called. This lock is reacquired before wait() returns. There is no race condition between releasing and reacquiring the lock because wait() is tightly integrated with the lock mechanism. The lock won't be released until wait() is able to receive notifications.

Q: Why should I call wait() in a while loop context as opposed to an if decision context?

A: You should call wait() in a while loop context as opposed to an if decision context because of the possibility for spurious wakeups. Check out stackoverflow's Do spurious wakeups actually happen? topic to learn more about this phenomenon.

Q: Can you provide me with an example of the waiting and notification methods?

A: Check out Listing 2.

Listing 2. Sending and receiving messages

public class WaitNotifyDemo
{
   public static void main(String[] args)
   {
      class Shared
      {
         private String msg;

         synchronized void send(String msg)
         {
            while (this.msg != null)
               try
               {
                  wait();
               }
               catch (InterruptedException ie)
               {
               }
            this.msg = msg;
            notify();
         }

         synchronized String receive()
         {
            while (msg == null)
               try
               {
                  wait();
               }
               catch (InterruptedException ie)
               {
               }
            String temp = msg;
            msg = null;
            notify();
            return temp;
         }
      }

      final Shared shared = new Shared();

      Runnable rsender;
      rsender = new Runnable()
                {
                   @Override
                   public void run()
                   {
                      for (int i = 0; i < 10; i++)
                      {
                         shared.send("A"+i);
                         try
                         {
                            Thread.sleep((int)(Math.random()*200));
                         }
                         catch (InterruptedException ie)
                         {
                         }
                      }
                      shared.send("done");
                   }
                };
      Thread sender = new Thread(rsender);

      Runnable rreceiver;
      rreceiver = new Runnable()
                  {
                     @Override
                     public void run()
                     {
                        String msg;
                        while (!(msg = shared.receive()).equals("done"))
                        {
                           System.out.println(msg);
                           try
                           {
                              Thread.sleep((int)(Math.random()*200));
                           }
                           catch (InterruptedException ie)
                           {
                           }
                        }
                     }
                  };
      Thread receiver = new Thread(rreceiver);
 
      sender.start();
      receiver.start();     
   }
}

Listing 2 presents a WaitNotifyDemo class whose main() method creates and starts a pair of threads that send and receive messages.

main() first declares a local class named Shared that handles the sending and receiving tasks. Shared declares a private String field named msg that stores the message being sent, and also declares synchronized send() and receive() methods that perform the send and receive operations.

The sending thread calls send(). Because the previous message passed to send() may not have been received, this method first tests the "message waiting" condition by evaluating this.msg != null. While this expression returns true, a message is waiting to be sent and the sending thread must wait, by calling wait(). Once the message has been received, the receiving thread will assign null to msg and notify the waiting sending thread, which saves the new message and calls notify(), to awaken a waiting receiving thread (if it's waiting).

The receiving thread calls receive(). Because there may not be a message waiting to be received, this method first tests the "message not waiting" condition by evaluating msg == null. While this expression returns true, a message isn't waiting to be received and the receiving thread must wait, by calling wait(). Once a message is available for sending, the sending thread will assign it to msg and notify the waiting receiving thread, which assigns null to msg and calls notify(), to awaken a waiting sending thread (if it's waiting).

After declaring Shared, main() instantiates this class. It then creates a pair of runnables and Thread objects for the sending and receiving threads. The runnables take care of sending and receiving the messages. Lastly, these threads are started by calling each Thread object's start() method.

Compile the source code (javac WaitNotifyDemo.java) and run the application (java WaitNotifyDemo). You should observe the following output:

A0
A1
A2
A3
A4
A5
A6
A7
A8
A9

Q: I would like to dig deeper into how the wait/notify mechanism works. Can you recommend a good resource that will provide me with this insight?

A: Check out Chapter 20: Thread Synchronization of Bill Venners book, Inside the Java Virtual Machine, at the artima developer website.

1 2 Page 1
Notice to our Readers
We're now using social media to take your comments and feedback. Learn more about this here.