Chapter: Java The Complete Reference - The Java Language - Multithreaded Programming

Study Material, Lecturing Notes, Assignment, Reference, Wiki description explanation, brief detail

Interthread Communication

The preceding examples unconditionally blocked other threads from asynchronous access to certain methods.

Interthread Communication

The preceding examples unconditionally blocked other threads from asynchronous access to certain methods. This use of the implicit monitors in Java objects is powerful, but you can achieve a more subtle level of control through interprocess communication. As you will see, this is especially easy in Java.

As discussed earlier, multithreading replaces event loop programming by dividing your tasks into discrete, logical units. Threads also provide a secondary benefit: they do away with polling. Polling is usually implemented by a loop that is used to check some condition repeatedly. Once the condition is true, appropriate action is taken. This wastes CPU time. For example, consider the classic queuing problem, where one thread is producing some data and another is consuming it. To make the problem more interesting, suppose that the producer has to wait until the consumer is finished before it generates more data. In a polling system, the consumer would waste many CPU cycles while it waited for the producer to produce. Once the producer was finished, it would start polling, wasting more CPU cycles waiting for the consumer to finish, and so on. Clearly, this situation is undesirable.

To avoid polling, Java includes an elegant interprocess communication mechanism via the wait( ), notify( ), and notifyAll( ) methods. These methods are implemented as final methods in Object, so all classes have them. All three methods can be called only from within a synchronized context. Although conceptually advanced from a computer science perspective, the rules for using these methods are actually quite simple:

 

        wait( ) tells the calling thread to give up the monitor and go to sleep until some other thread enters the same monitor and calls notify( ) or notifyAll( ).

 

        notify( ) wakes up a thread that called wait( ) on the same object.

 

        notifyAll( ) wakes up all the threads that called wait( ) on the same object. One of the threads will be granted access.

 

These methods are declared within Object, as shown here:

 

final void wait( ) throws InterruptedException final void notify( )

final void notify All( )

 

Additional forms of wait( ) exist that allow you to specify a period of time to wait. Before working through an example that illustrates interthread communication, an

important point needs to be made. Although wait( ) normally waits until notify( ) or notifyAll( ) is called, there is a possibility that in very rare cases the waiting thread could be awakened due to a spurious wakeup. In this case, a waiting thread resumes without notify( ) or notifyAll( ) having been called. (In essence, the thread resumes for no apparent reason.) Because of this remote possibility, Oracle recommends that calls to wait( ) should take place within a loop that checks the condition on which the thread is waiting. The following example shows this technique.

 

Let’s now work through an example that uses wait( ) and notify( ). To begin, consider the following sample program that incorrectly implements a simple form of the producer/ consumer problem. It consists of four classes: Q, the queue that you’re trying to synchronize; Producer, the threaded object that is producing queue entries; Consumer, the threaded

object that is consuming queue entries; and PC, the tiny class that creates the single Q,

 

Producer, and Consumer.

 

// An incorrect implementation of a producer and consumer. 

class Q {

 

int n;

 

synchronized int get() { System.out.println("Got: " + n); return n;

 

}

 

synchronized void put(int n) { this.n = n; System.out.println("Put: " + n);

 

}

 

}

 

class Producer implements Runnable { Q q;

 

Producer(Q q) { this.q = q;

 

new Thread(this, "Producer").start();

 

}

 

public void run() { int i = 0;

 

while(true) { q.put(i++);

}

 

}

 

}

 

class Consumer implements Runnable { Q q;

 

Consumer(Q q) { this.q = q;

 

new Thread(this, "Consumer").start();

 

}

 

public void run() { while(true) {

 

q.get();

 

}

 

}

 

}

 

class PC {

 

public static void main(String args[]) {

Q q = new Q(); new Producer(q); new Consumer(q);

 

System.out.println("Press Control-C to stop.");

 

}

 

}

 

Although the put( ) and get( ) methods on Q are synchronized, nothing stops the producer from overrunning the consumer, nor will anything stop the consumer from consuming the same queue value twice. Thus, you get the erroneous output shown here (the exact output will vary with processor speed and task load):

 

Put: 1

 

Got: 1

 

Got: 1

 

Got: 1

 

Got: 1

 

Got: 1

 

Put: 2

 

Put: 3

 

Put: 4

 

Put: 5

 

Put: 6

 

Put: 7

 

Got: 7

 

As you can see, after the producer put 1, the consumer started and got the same 1 five times in a row. Then, the producer resumed and produced 2 through 7 without letting the consumer have a chance to consume them.

 

The proper way to write this program in Java is to use wait( ) and notify( ) to signal in both directions, as shown here:

 

// A correct implementation of a producer and consumer. 

class Q {

 

int n;

 

boolean valueSet = false;

 

synchronized int get() { while(!valueSet)

 

try { wait();

 

} catch(InterruptedException e) { 

System.out.println("InterruptedException caught");

}

 

System.out.println("Got: " + n); valueSet = false;

 

notify(); return n;

 

}

synchronized void put(int n) {

while(valueSet) try {

wait();

 

} catch(InterruptedException e) { 

System.out.println("InterruptedException caught");

}

 

this.n = n; valueSet = true;

 

System.out.println("Put: " + n); notify();

 

}

 

}

 

class Producer implements Runnable { Q q;

 

Producer(Q q) { this.q = q;

 

new Thread(this, "Producer").start();

 

}

 

public void run() { int i = 0;

 

while(true) { q.put(i++);

}

 

}

 

}

 

class Consumer implements Runnable { Q q;

 

Consumer(Q q) { this.q = q;

 

new Thread(this, "Consumer").start();

 

}

 

public void run() { while(true) {

 

q.get();

 

}

 

}

 

}

 

class PCFixed {

 

public static void main(String args[]) { Q q = new Q();

 

new Producer(q); new Consumer(q);

 

System.out.println("Press Control-C to stop.");

 

}

 

}

Inside get( ), wait( ) is called. This causes its execution to suspend until Producer notifies you that some data is ready. When this happens, execution inside get( ) resumes. After the data has been obtained, get( ) calls notify( ). This tells Producer that it is okay to put more data in the queue. Inside put( ), wait( ) suspends execution until Consumer has removed the item from the queue. When execution resumes, the next item of data is put in the queue, and notify( ) is called. This tells Consumer that it should now remove it.

 

Here is some output from this program, which shows the clean synchronous behavior:

 

Put: 1

 

Got: 1

 

Put: 2

 

Got: 2

 

Put: 3

 

Got: 3

 

Put: 4

 

Got: 4

 

Put: 5

 

Got: 5

 

Deadlock

 

A special type of error that you need to avoid that relates specifically to multitasking is deadlock, which occurs when two threads have a circular dependency on a pair of synchronized objects. For example, suppose one thread enters the monitor on object X and another thread enters the monitor on object Y. If the thread in X tries to call any synchronized method on Y, it will block as expected. However, if the thread in Y, in turn, tries to call any synchronized method on X, the thread waits forever, because to access X, it would have to release its own lock on Y so that the first thread could complete. Deadlock is a difficult error to debug for two reasons:

 

        In general, it occurs only rarely, when the two threads time-slice in just the right way.

 

        It may involve more than two threads and two synchronized objects. (That is, deadlock can occur through a more convoluted sequence of events than just described.)

 

To understand deadlock fully, it is useful to see it in action. The next example creates two classes, A and B, with methods foo( ) and bar( ), respectively, which pause briefly before trying to call a method in the other class. The main class, named Deadlock, creates an A and a B instance, and then starts a second thread to set up the deadlock condition. The foo( ) and bar( ) methods use sleep( ) as a way to force the deadlock condition to occur.

 

// An example of deadlock. 

class A {

 

synchronized void foo(B b) {

 

String name = Thread.currentThread().getName();

 

System.out.println(name + " entered A.foo");

 

try { Thread.sleep(1000);

} catch(Exception e) { System.out.println("A Interrupted");

}

 

System.out.println(name + " trying to call B.last()"); b.last();

 

}

 

synchronized void last() { System.out.println("Inside A.last");

 

}

 

}

 

class B {

 

synchronized void bar(A a) {

 

String name = Thread.currentThread().getName(); 

System.out.println(name + " entered B.bar");

 

try { Thread.sleep(1000);

 

} catch(Exception e) { 

System.out.println("B Interrupted");

}

 

System.out.println(name + " trying to call A.last()"); 

a.last();

 

}

 

synchronized void last() { 

System.out.println("Inside A.last");

 

}

 

}

 

class Deadlock implements Runnable { A a = new A();

 

B b = new B();

 

Deadlock() { 

Thread.currentThread().setName("MainThread"); 

Thread t = new Thread(this, "RacingThread"); 

t.start();

 

a.foo(b); // get lock on a in this thread. 

System.out.println("Back in main thread");

 

}

 

public void run() {

 

b.bar(a); // get lock on b in other thread. 

System.out.println("Back in other thread");

 

}

 

public static void main(String args[]) { new Deadlock();

 

}

 

}

When you run this program, you will see the output shown here:

 

MainThread entered A.foo

 

RacingThread entered B.bar

 

MainThread trying to call B.last()

 

RacingThread trying to call A.last()

 

Because the program has deadlocked, you need to press ctrl-c to end the program. You can see a full thread and monitor cache dump by pressing ctrl-break on a PC. You will see that RacingThread owns the monitor on b, while it is waiting for the monitor on a. At the same time, MainThread owns a and is waiting to get b. This program will never complete. As this example illustrates, if your multithreaded program locks up occasionally, deadlock is one of the first conditions that you should check for.


Study Material, Lecturing Notes, Assignment, Reference, Wiki description explanation, brief detail


Copyright © 2018-2020 BrainKart.com; All Rights Reserved. Developed by Therithal info, Chennai.