1
0

Multi Threaded Programming in Java

The earliest computers did one thing at a time. A computer while calculating the salary of employees could not process sales data. All programs were run sequentially, one at a time, and each had full control of the computer. However, the two programs couldn’t run at the same time. This is called batch processing, and it’s a very efficient way to get maximum usage out of a very expensive computer. Almost all the computer’s time is spent on user programs. However, batch processing can be very annoying when your differential equation integration program that would take two seconds of CPU time gets stuck in line behind the physics department’s nuclear structure modeling project that’s going to run for the next three days.

Time-sharing operating systems were invented to allow multiple people to use more than one very expensive computer at the same time. On a time-sharing system, many people could run programs at the same time. The operating system is responsible for splitting the time among the different programs that are running. That way you can finish integrating your differential equation while the physics department’s nuclear modeling program is still churning away. The physics department’s nuclear modeling program might take two weeks to run instead of three days, but everyone with the shorter programs was happy (at least until the physicists figured out how to hack the computer so that it only ran their program)

Once systems allowed different users to run programs at the same time it was a short step to letting the same user run multiple programs simultaneously. Each running program (generally called a process) had its own memory space, its own set of variables, its stack and heap, and so on. One process could spawn another process, but afterward, the two processes behaved more or less independently. Mechanisms like inter-procedure calls (IPC) were developed to allow processes to interact with each other, but such interaction was expensive and difficult. And this is where matters rested for about twenty years.

However, it’s not just users that want to do different things at the same time. Many programs also need to do several things at once. A web browser, for instance, can print a file in the background while it downloads a page in one window and formats the page as it downloads. The ability of an individual program to do more than one thing at the same time is most efficiently implemented through threads.

Threads VS Processes #

Both threads and processes are methods of parallelizing an application. However, processes are independent execution units that contain their state information, use their own address spaces, and only interact with each other via interprocess communication mechanisms (generally managed by the operating system). Applications are typically divided into processes during the design phase, and a master process explicitly spawns sub-processes when it makes sense to logically separate significant application functionality. Processes, in other words, are architectural constructs.

By contrast, a thread is a coding construct that doesn’t affect the architecture of an application. A single process might contain multiple threads; all threads within a process share the same state and same memory space and can communicate with each other directly because they share the same variables.

Threads typically are spawned for a short-term benefit that is usually visualized as a serial task, but which doesn’t have to be performed in a linear manner (such as performing a complex mathematical computation using parallelism or initializing a large matrix), and they are absorbed when no longer required. The scope of a thread is within a specific code module-which is why we can bolt on threading without affecting the broader application.

Thread Introduction #

A thread can be loosely defined as a separate stream of execution that takes place simultaneously with and independently of everything else that might be happening. A thread is like a classic program that starts at point A and executes until it reaches point B. It does not have an event loop. A thread runs independently of anything else happening on the computer. Without threads, an entire program can be held up by one CPU-intensive task or one infinite loop, intentional or otherwise. With threads, the other tasks that don’t get stuck in the loop can continue processing without waiting for the stuck task to finish.

It turns out that implementing threading is harder than implementing multitasking in an operating system. The reason it’s relatively easy to implement multitasking is that individual programs are isolated from each other. Individual threads, however, are not. To return to the printing example, suppose that while the printing is happening in one thread, the user deletes a large chunk of text in another thread. What’s printed? Is the document as it was before the deletion? Is the document as it was after the deletion? The document with some but not all of the deleted text? Or does the whole system go down in flames? Most often in a non-threaded or poorly threaded system, it’s the latter.

Threaded environments like Java allow a thread to put locks on shared resources so that while one thread is using data no other thread can touch that data. This is done with synchronization. Synchronization should be used sparingly since the purpose of threading is defeated if the entire system gets stopped waiting for a lock to be released. The proper choice of objects and methods to synchronize is one of the more difficult things to learn about threaded programming.

Threads in Java #

Java applications and applets are naturally threaded. The runtime environment starts the execution of the program with the main() method in one thread. Garbage collection takes place in another thread. Screen updating occurs in a third thread. There may be other threads running as well, mostly related to the behavior of the virtual machine. All of this happens invisibly to the programmer. Some of the time you’re only concerned with what happens in the primary thread which includes the main() method of a program. If this is the case you may not need to worry about threading at all.

Sometimes, however, you need to add your threads to an applet or application. The simplest reason for adding a separate thread is to perform a long calculation. For instance, if you’re trying to find the ten million prime number, you probably don’t want to make users twiddle their thumbs while you search. Or you may be waiting for a resource that isn’t available yet, a large graphic to download from the Internet, for example. Once again you shouldn’t make the user wait while your program waits. Any operation that is going to take a noticeable period should be placed in its thread.

The other reason to use threading is to more evenly divide the computer’s power among different tasks. If you want to draw random rectangles on the display, you would still like the applet to respond to user input. If all the CPU time is spent drawing rectangles, there’s nothing left over for the user. On a preemptively multitasking operating system like Solaris or Windows NT, the user may at least be able to kill the application. On a cooperatively multitasking operating system like macOS 9 or Windows 9x, the user may have to reboot their machine. This is a bad thing. With threads, you can set the priority of different processes so that user input receives a high priority and drawing pretty pictures receives a low priority. Then the user can stop the applet without flipping the power switch on their machine.

Creating Threads #

In Java Thread can be created in two ways. One is to create a subclass of java.lang.Thread class. However, sometimes you’ll want to thread an object that’s already a subclass of another class. Then you use the java.lang.Runnable interface.

The Thread class has three primary methods that are used to control a thread:

public void start()
public void run()
public final void stop()

The start() method prepares a thread to be run; the run() method performs the work of the thread; and the stop() method halts the thread. The thread dies when the run() method terminates or when the thread’s stop() method is invoked.

You never call run() explicitly. It is called automatically by the runtime as necessary once you’ve called start(). There are also methods to suspend and resume threads, to put threads to sleep and wake them up, to yield control to other threads, and many more. These are discussed later.

The Runnable interface allows you to add threading to a class that, for one reason or another, cannot conveniently extend the Thread class. It declares a single method, run():

public abstract void run()

By passing an object which implements Runnable to a Thread() constructor, you can substitute the Runnable’s run() method for the Thread’s own run() method. (More properly the Thread object’s run() method simply calls the Runnable’s run() method.

A simple thread

When writing a threaded program you can pretend that you’re writing many different programs, each with its run() method. Each thread is a subclass of java.lang.Thread. The following program is a thread that prints the numbers between -8 and 7.

Program to create user Thread using Thread class

import java.lang.Thread;
public class BytePrinter extends Thread
{
    public void run()
    {
        for(int b=-8; b<8; b++)
        {
            System.out.println(b);
        }
    }
}

You launch this thread from another method, probably in another class, by instantiating an object of this class using new and calling its start() method. To create a thread just call the default constructor for your subclass of Thread. For instance

BytePrinter bp = new BytePrinter();

This class only has the default, no-args constructor, but there’s absolutely no reason you can’t add other constructors to your Thread subclass.

Constructing a thread object puts it at the starting line. The bell goes off and the thread starts running when you call the thread’s start() method like this:

bp.start();

Once the start() method is called, program execution splits in two. Some CPU time goes into whatever statements follow bp.start() and some go into the bp thread. It is unpredictable which statements will run first. Most likely they will be intermixed. The bp thread will now continue running until one of seven things happens:

  1. bp’s run() method completes.
  2. bp’s stop() method is called.
  3. bp’s suspend() method is called.
  4. bp’s sleep() method is called.
  5. bp’s yield() method is called.
  6. bp blocks waiting for an unavailable resource
  7. Another thread preempts this thread.

Once program control reaches the end of the bp’s run() method, the thread dies. It cannot be restarted, though you can create a new instance of the same Thread subclass and start that.

Invoking a thread

For example, the following program launches a single BytePrinter thread:

public class ThreadTest
{
    public static void main(String[] args)
    {
        System.out.println("Constructing the thread...");
        BytePrinter bp = new BytePrinter();
        System.out.println("Starting the thread...");
        bp.start();
        System.out.println("The thread has been started.");
        System.out.println("The main() method is finished.");
        return;
    }
}

Here’s some sample output:

Constructing the thread...
Starting the thread...
The thread has been started.
-8
-7
-6
The main() method isi finishing
-5
-4
-3
-2
-1
0
...

Of course, this continues for a couple of more lines.

Multiple threads

The following program launches three BytePrinter threads:

public class ThreadsTest
{
    public static void main(String[] args)
    {
        BytePrinter bp1 = new BytePrinter();
        BytePrinter bp2 = new BytePrinter();
        BytePrinter bp3 = new BytePrinter();
        bp1.start();
        bp2.start();
        bp3.start();
    }
}

Multiple threads output:

The order of the output you see from this program is implementation dependent and mostly unpredictable. It may look something like this:

-8
-8
-8
-7
-7
-7
-6
-6
-6
-5
-5
-5
...

In this case, the three threads run turn-wise, one after the other. However, on a few systems, you may see something different.

Multiple preemptive threads

However, on a few systems, you may see something like this:

-8
-7
-6
-5
-4
...
-8
-7
-6
-5
-4
...
-8
-7
-6
-5
-4
...

In this case output from the three different threads is sequential. Some systems use cooperative threads that require a thread to explicitly yield control before other, equal-priority threads get a chance to run. Other systems use preemptive threading in which the virtual machine guarantees that all threads of the same priority get time in which to run. However, even on preemptive VMs, this thread takes so little time to run it’s unlikely any intermixing will occur.

Naming Threads

You can give different threads in the same class names so you can tell them apart. The following constructor allows you to do that:

public Thread(String name)

This is normally called from the constructor of a subclass, like this:

Program to create named thread

public class NamedBytePrinter extends Thread
{
    public NamedBytePrinter(String name)
    {
        super(name);
    }
    public void run()
    {
        for(int b=1; b<8; b++)
        {
            System.out.println(this.getName() + ": " + b);
        }
    }
}

The getName() method of the Thread class returns the Thread name. Note that the getName() method is inherited from the Thread class and used by NamedBytePrinter class in the run() method.

The following program now distinguishes the output of different threads:

public class NamedThreadsTest
{
    public static void main(String[] args)
    {
        NamedBytePrinter nbpX = new NamedBytePrinter("nbpX");
        NamedBytePrinter nbpY = new NamedBytePrinter("nbpY");
        NamedBytePrinter nbpZ = new NamedBytePrinter("nbpZ");
        nbpX.start();
        nbpY.start();
        nbpZ.start();
    }
}

Named Threads Output:

nbpX: 1
nbpY: 1
nbpZ: 1
nbpX: 2
nbpY: 2
nbpZ: 2
nbpX: 3
nbpY: 3
nbpZ: 3
nbpX: 4
nbpY: 4
nbpZ: 4
nbpX: 5
nbpY: 5
nbpZ: 5
nbpX: 6
nbpY: 6
nbpZ: 6
nbpX: 7
nbpY: 7
nbpZ: 7

Again, the exact ordering and even whether the thread output is intermixed, can vary from system to system and run to run.

The Runnable interface

So far all the threads you’ve seen have been subclasses of java.lang.Thread. Sometimes, however, you want to add threading to a class that already inherits from a class other than Thread. The most common occurrence is when you want to add threading to an applet.

The Runnable interface declares just one method, run().

public void run()

Your class needs to implement this method just as it would if it were a subclass of Thread and declare that it implements the Runnable interface like this:

public class MyThreadedClass extends SomeClass implements Runnable
{
    ...
    public void run()
    {
        ...
    }
}

To start the threaded object create a new Thread and pass the Runnable object to the Thread constructor. Then call the Thread’s start() method like this:

MyThreadedClass mtc = new MyThreadedClass();
Thread t = new Thread(mtc);
t.start();

Program using Runnable Interface

import java.lang.Thread;
class BytePrinter implements Runnable
{
    public void run()
    {
        for(int b=1; b<8; b++)
        {
            System.out.println(b);
        }
    }
}
public class RunnableTest
{
    public static void main(String args[])
    {
        BytePrinter bpr = new BytePrinter();
        Thread t = new Thread(bpr);
        t.start();
        System.out.println("End of main thread");
    }
}

Output:

End of main thread
1
2
3
4
5
6
7

Life Cycle of Thread #

A thread passes through a number of states during its lifetime. The lifetime of a thread is the time span from its creation to death. The various states that a thread occupies in its lifetime are:

  1. Newborn
  2. Runnable
  3. Running
  4. Blocked &
  5. Dead

At a particular time, a thread is in one of the above states. When a thread is created it is in a newborn state. State transitions are shown in the diagram below:

Newborn State: When a thread object is created it is said to be in a Newborn state. At this state, if the stop() method is invoked the thread directly enters in Dead state. It can be sent to the Runnable state by invoking the start() method on this thread. Invocation of any other method in this state generates an exception.

Runnable State: Runnable state implies that the thread is ready for execution. In this state, the thread is waiting for the availability of the CPU. All threads after the invocation of the start() method, except one that is currently running, are in a runnable state. These threads wait in the ready queue. If all threads are at the same priority then the CPU allocate time to them using round robin scheduling algorithm. In this algorithm, the CPU executes the threads in a first come first serve fashion. If different priorities are defined for threads then the CPU is allocated to them using a preemptive priority scheduling algorithm. In this algorithm, highest priority threads are executed before low priority threads. While executing a low priority thread if a high priority thread enters in ready queue, the CPU jumps immediately to the high priority thread.

Thread Life Cycle
Thread Life Cycle

Running State: In this state, the CPU is actually executing the thread. A running thread can release the CPU in the following situations:

  • By executing the yield() method it enters in Runnable state.
  • The time slice allocated to the thread expires and it goes to a Runnable state.
  • It is preempted by high priority thread and it goes to Runnable state.
  • It completes its execution and enters in Dead state.
  • It is suspended by using suspend() method. A suspended thread enters the Blocked state and can be sent to a Runnable state by using the resume() method.
  • It executes the sleep() method to enter a Blocked state. A sleeping thread re-enters in a Runnable state after the sleeping time expires.
  • It has to wait for some event. The thread enters in a Blocked state by executing the wait() method. It can be sent to the Runnable state by using notify() method.

Blocked State: A blocked thread does not compete for CPU time. A thread can be blocked by using suspend(), sleep(), or wait() methods. A thread in a blocked state is considered non-runnable. A blocked thread can be revived by using an appropriate method as explained above.

Dead State: This is the last state of the thread life cycle. When a thread enters this state it ceases to exist. When a thread enters in this state after completing its execution, it is called natural death. However, a thread can be prematurely killed by executing the stop() method in the Newborn state, Running state, or Blocked state.

Thread Priority #

All threads are created with equal priority. Sometimes you want to give one thread more time than another. Threads that interact with the user should get very high priorities. Threads that calculate in the background should get low priorities.

Thread priorities are defined as integers between 1 and 10. Ten is the highest priority. One is the lowest. The normal priority is 5. Higher priority threads get more CPU time.

Warning: This is exactly opposite to the normal UNIX way of prioritizing processes where the higher the priority number of a process, the less CPU time the process gets.

For your convenience java.lang.Thread defines three mnemonic constants, which you can use in place of the numeric values.

Mnemonic Constant Integer value
Thread.MAX_PRIORITY 10
Thread.MIN_PRIORITY 1
Thread.NORM_PRIORITY 5

You set a thread’s priority with the setPriority(int priority) method. The following program sets nbpZ’s priority higher than nbpY’s whose priority is higher than nbpX’s. It is therefore likely that even though nbpZ starts last and nbpX starts first, nbpZ will finish before nbpY who will finish before nbpX.

public class MixedPriorityTest
{
    public static void main(String[] args)
    {
        NamedBytePrinter nbpX = new Named BytePrinter("nbpX");
        NamedBytePrinter nbpY = new Named BytePrinter("nbpY");
        NamedBytePrinter nbpZ = new Named BytePrinter("nbpZ");
        nbpX.setPriority(Thread.MIN.PRIORITY);
        nbpY.setPriority(Thread.NORM.PRIORITY);
        nbpZ.setPriority(Thread.MAX.PRIORITY);
        nbpX.start();
        nbpY.start();
        nbpZ.start();
    }
}

Sleep:

Sometimes the computer may run too fast for human beings. If this is the case you need to slow it down. You can make a particular Thread slow down by making a call to the Thread.sleep(ms) method. ms is the number of milliseconds you want the thread to wait before proceeding on. There are one thousand milliseconds in a second.

Another thread can invoke a sleeping thread’s interrupt() method. If this happens, the sleep() method throws an InterruptedException. Therefore when you put a thread to sleep, you need to catch InterruptedExceptions. Thus every call to sleep() should be wrapped in a try-catch block like this:

try
{
    Thread.sleep(1000);
}
catch(InterruptedException ex)
{
}

To delay a program for a fixed amount of time you often do something like shown above.

Safely Stopping Threads

Stopping threads at arbitrary times can leave objects in inconsistent, half-updated states they could not normally reach.

To avoid this, provide the method that stops the thread at only precise well-defined locations in the thread’s execution. If you like you can also add a cleanup() method to be called here as well.

public class StoppableThread extends Thread
{
    private boolean stopReqested;
    public void run()
    {
        while(true)
        {
            System.out.println("Hee");
            System.out.println("Ha");
            System.out.println("Ho");
            System.out.println("");
            if(stopRequested)
            {
                cleanup();
                break;
            }
        }
    }
    public void requestStop()
    {
        stopRequested = true;
    }
    private void cleanup()
    {
        // ...
    }
}

Thread Synchronization #

In Java, the threads are executed independently of each other. These types of threads are called asynchronous threads. But there are two problems that may occur with asynchronous threads.

  1. Two or more threads share the same resource (variable or method) while only one of them can access the resource at one time.
  2. If the producer and the consumer are sharing the same kind of data in a program then either producer may produce the data faster or the consumer may retrieve an order of data and process it without its existing.

Suppose, we have created two methods as increment() and decrement() which increase or decrease the value of the variable “count” by 1 respectively shown as:

public void increment()
{
    count++;
}
public void decrement()
{
    count--;
}
public int value()
{
    return count();
}

When the two threads are executed to access these methods (one for increment(), another for decrement()) then both will share the variable “count”. In that case, we can’t be sure what value will be returned for the variable “count”.

To avoid this problem, Java uses a monitor to prevent data from being corrupted by multiple threads by a keyword synchronized to synchronize them and intercommunicate with each other. It is basically a mechanism that allows two or more threads to share all the available resources in a sequential manner. Java’s synchronized word is used to ensure that only one thread is in a critical region. The critical region is a lock area where only one thread is run (or lock) at a time. Once the thread is in its critical section, no other thread can enter to that critical region. In that case, another thread will have to wait until the current thread leaves its critical section.

The general form of the synchronized statement is as:

synchronized(object)
{
    // statements to be synchronized
}

Lock

Lock term refers to the access granted to a particular thread that can access the shared resources. At any given time, only one thread can hold the lock and thereby have access to the shared resource. Every object in Java has a built-in lock that only comes into action when the object has synchronized method code. By associating a shared resource with a Java object and its lock, the object can act as a guard, ensuring synchronized access to the resource. Only one thread at a time can access the shared resource guarded by the object lock.

Since there is one lock per object, if one thread has acquired the lock, no other thread can acquire the lock until the lock is not released by the first thread. Acquiring the lock means the thread is currently in the synchronized method and releasing the lock means exits the synchronized method.

Remember the following points related to lock and synchronization:

  • Only methods (or blocks) can be synchronized, Classes and variables cannot be synchronized.
  • Each object has just one lock.
  • All methods in a class need not be synchronized. A class can have both synchronized and non-synchronized methods.
  • If two threads want to execute a synchronized method in a class, and both threads are using the same instance of the class to invoke the method then only one thread can execute the method at a time.
  • If a class has both synchronized and non-synchronized methods, multiple threads can still access the class’s non-synchronized methods. If you have methods that don’t access the data you’re trying to protect, then you don’t need to synchronize them. Synchronization can cause a hit in some cases (or even deadlock if used incorrectly), so you should be careful not to overuse it.
  • If a thread goes to sleep, it holds any locks it has? but it doesn’t release them.
  • A thread can acquire more than one lock. For example, a thread can enter a synchronized method, thus acquiring a lock, and then immediately invoke a synchronized method on a different object, thus acquiring that lock as well. As the stack unwinds, locks are released again.
  • You can synchronize a block of code rather than a method.
  • Constructors cannot be synchronized

There are two ways to synchronize the execution of code:

  • Synchronized Methods
  • Synchronized Blocks (Statements)

Synchronized Methods:

Any method specified with the keyword synchronized is only executed by one thread at a time. If any thread wants to execute the synchronized method, firstly it has to obtain the object’s lock. If the lock is already held by another thread, then the calling thread has to wait.

Synchronized methods are useful in those situations where methods are executed concurrently so that these can be intercommunicated to manipulate the state of an object in ways that can corrupt the state if. Stack implementations usually define the two operations push and pop of elements as synchronized, that?s why pushing and popping are mutually exclusive operations. For Example, if several threads were sharing a stack, if one thread is popping the element on the stack then another thread would not be able to push the element on the stack.

Program to demonstrate the synchronized method:

class Share extends Thread
{
    static String msg[] = {"This", "is", "a", "synchronized", "variable"};
    Share(String threadname)
    {
        super(threadname);
    }
    public void run()
    {
        display(getName());
    }
    public synchronized void display(String threadN)
    {
        for(int i=0; i<=4; i++)
            System.out.println(threadN + msg[i]);
        try
        {
            this.sleep(1000);
        }catch(Exception e){}
    }
}
public class SynThread1
{
    public static void main(String args[])
    {
        Share tr1 = new Share("Thread One: ");
        t1.start();
        Share tr2 = new Share("Thread Two: ");
        t2.start();
    }
}

The output of the program is:

Thread One: This
Thread One: is
Thread One: a
Thread One: synchronized
Thread One: variable
Thread Two: This
Thread Two: is
Thread Two: a
Thread Two: synchronized
Thread Two: variable

In this program, the method “display()” is synchronized that will be shared by both thread’s objects at the time of program execution. Thus only one thread can access that method and process it until all statements of the method are executed.

Synchronized Blocks (Statements)

Another way of handling synchronization is Synchronized Blocks (Statements). Synchronized statements must specify the object that provides the native lock. The synchronized block allows the execution of arbitrary code to be synchronized on the lock of an arbitrary object.

The general form of synchronized block is:

synchronized (object reference expression)
{
    // statements to be synchronized
}

The following program demonstrates the synchronized block that shows the same output as the output of the previous example:

Program to demonstrate synchronized block

class Share extends Thread
{
    static String msg[] = {"This", "is", "a", "synchronized", "variable"};
    Share(String threadname)
    {
        super(threadname);
    }
    public void run()
    {
        display(getName());
    }
    public void display(String threadN)
    {
        synchronized(this)
        {
            for(int i=0; i<=4; i++)
                System.out.println(threadN+msg[i]);
            try
            {
                this.sleep(1000);
            }catch(Exception e){}
        }
    }
}
public class SynStatement
{
    public static void main(String[] args)
    {
        Share t1 = new Share("Thread One: ");
        t1.start();
        Share t2 = new Share("Thread Two: ");
        t2.start();
    }
}

The output of the Program

Thread One: This
Thread One: is
Thread One: a
Thread One: synchronized
Thread One: variable
Thread Two: This
Thread Two: is
Thread Two: a
Thread Two: synchronized
Thread Two: variable

Inter-Threaded Communication #

We have a classic ‘Producer-Consumer’ problem to explain the use of Inter-Thread communications in java, where one thread is producing some data and another is consuming it The producer has to wait until the consumer is finished before it generates more data.

Let us start with an incorrect implementation of the Producer-Consumer problem, where we are just using the mercy of the synchronized method.

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();
        }
    }
}
public class Main
{
    public static void main(String args[])
    {
        Q q = new Q();
        new Producer(q);
        new Consumer(q);
    }
}

Although the put() and get() methods on Q are synchronized, nothing stops the producer from producing more things than the consumer can use and nothing stops the consumer from using the same product more than once. Thus, you will get the wrong output shown below.

Put: 1
Got: 1
Got: 1
Got: 1
Put: 2
Put: 3
Put: 4
Put: 5
Put: 6
Got: 6

Here Consumer used the same product, product-1 thrice and Consumer didn’t get a chance to use the products, Product-2, Product-3, Product-4, and Product-5.

Now we came to know that, with the only use of synchronized, can’t do proper communication between multiple threads. Don’t worry, Java is rich enough to provide a solution for this issue, providing three methods, wait(), notify(), and notifyAll() for Inter Thread Communication.

Let us try the Producer-Consumer problem with the wait() and notify() methods.

Correct implementation of a producer and consumer.

class Q
{
    int n;
    boolean isQueryEmpty = true;
    synchronized int get()
    {
        if(isQueueEmpty)
        try
        {
            wait();
        } catch(IntereuptedException e)
        {
            System.out.println("InteruptedException caught");
        }
        System.out.println("Got:" + n);
        isQueueEmpty = true;
        notify();
        return n;
    }
    synchronized void put(int n)
    {
        if(!isQueueEmpty)
        try
        {
            wait();
        } catch(InterruptedException e)
        {
            System.out.println("InterruptedException caught");
        }
        this.n = n;
        isQueueEmpty = false;
        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();
        }
    }
}
public class Main
{
    public static void main(String args[])
    {
        Q q = new Q();
        new Producer(q);
        new Consumer(q);
    }
}

Here you will get the expected result as shown below:

Put: 1
Got: 1
Put: 2
Got: 2
Put: 3
Got: 3
Put: 4
Got: 4
Put: 5
Got: 5

We can see that Producer is not overrunning the Consumer and the Consumer is not using the same product twice.

Deadlock #

Other threads that want access to a synchronized object must wait for the first thread to release the object before they can continue. When a thread is waiting for another thread to release the lock on an object it is blocked.

A situation where a thread is waiting for an object lock that holds by a second thread, and this second thread is waiting for an object lock that holds by the first thread, this situation is known as a Deadlock.

Daemon Threads #

Threads that work in the background to support the runtime environment are called daemon threads. For example, the clock handler thread, the idle thread, the screen updater thread, and the garbage collector thread are all daemon threads. The virtual machine exits whenever all non-daemon threads have been completed.

public final void setDaemon(boolean isDaemon)
public final boolean isDaemon()

By default, a thread you create is not a daemon thread. However, you can the setDaemon(true) method to turn it into one.

Leave a Reply