dcsimg
December 16, 2018
Hot Topics:

Understanding the Intricacies of Threads and Locks in Java

  • December 3, 2018
  • By Manoj Debnath
  • Send Email »
  • More Articles »

Multi-threaded programming is the consequence of the synchronized processing of multiple threads. A thread forms the basic ingredients in this process. There are numerous intricacies associated with multi-threading. Here, we delve into the aspect of thread creation, synchronization, and locks as handled by Java.

Overview

Every program in Java is bound by the concept of a thread. This means that even the simplest "Hello World" program one may write is nothing but a thread in execution. But—a single thread. When we write several such threads and put them to work in a synchronous manner, it becomes a functioning multi-threaded program. Do not be confused by the word "thread" because it can be equally treated as a process in Java unless you are aware that there is a difference between multi-threading and multiprocessing. We'll take that up later. But for now, let's explore the question: What is the point of multi-thread or multi-process programming?

Multiprocessing

A process usually executes on the basis of a fairly linear set of instructions given with occasional loops, gotos, and jumps. Initially, that's fine under a simplistic hardware system. But, with time, processors evolved tremendously with an increased number of transistors. (Transistors are the minutest part of a processor from where it derives its powers. Initially, the idea was: the more the merrier, Moore's law.) They got more and more compact and tinier, apt to handle more work with more processing power. It began to finish its task in milliseconds, tasks that took several minutes or even hours by its older prototypes. The processor, therefore, sleeps most of the time while the system is busy with the I/O work. So, to squeeze the most out of the processor, programmers started giving it work in batches, which means a number of tasks compiled together and given to the processor so that it would be busy for a while. Yet still, the processor slept most of the time because a large part of a program also dealt with I/O subsystem. As a result, as soon as the I/O work started, the processor is free to sleep again. The processor has very little to do with the I/O processing.

This led to the idea of multiprocessing. In multiprocessing, a process is forked to create a duplicate of itself so that a part of the instruction can take its share of CPU time while another part is busy with other work, such as dealing with the I/O subsystem. In this manner, multiple processes can execute simultaneously, keeping the CPU busy most of the time. However, the main problem with this technique is the synchronous use of shared resources. Provisions must be made so that the competing processes do not get into a race or deadlock situation where, say, process P1 holds a resource R1 while seeking another (R2) before releasing R1. Meanwhile, another process, P2, may be holding R2 and seeks the resource R1 which was held by P1 before releasing it. In a nutshell (to disturb the CPU's sleep, programmers started having sleepless nights), it is a complex scenario, but there are ways to overcome them with techniques such as semaphore, mutex, and so forth. Let not get into that now.

But, Where Does Multithreading Fit In?

Well, multiprocessing is nice but multithreading is nicer. For the sake of the game, multi-threading and multiprocessing mean the same thing—to keep the CPU busy with multiple sets of shared instructions to process. Both cases help to spawn a new concurrent process flow. It goes out of question that both are effective on machines that have multi-core CPUs where the process flow can be scheduled to jump between idle processors, leveraging speed of execution through parallel and distributed processing. But, the difference lies in the word thread. Unlike a process, a thread is less demanding on resources. It has less overhead than forking or spawning a process. A thread does not allocate a new system virtual memory space and environment like the process. Threads share the same address space and are spawned by defining a function and its arguments are processed by the thread. A thread is effective not only on multi-core systems but also can boost performance in a single processing system by exploiting latency in I/O and other system functions that halts the execution.

Both multiprocessing or multithreading are, however, not parallel programming per se. They are good to leverage efficiency in a non-distributed environment. Parallel programming is a different model altogether. There are technologies, such as MPI and PVM, dedicated to be used in distributed computing environment. The playground for threads is confined within a single computer system.

How Threads Work in Java

A thread in Java is implemented by extending the Thread class or the Runnable interface. The run() method is overridden in the extending class. Note that the JVM takes all the help required from the underlying Operating System to create and execute the thread. In fact, the thread creation and execution begin with the JNI function call which, in turn, uses the POSIX APIs (in Linux) to create and execute the thread. The Thread class in java.lang is defined as follows:

package java.lang;
// ...
public class Thread implements Runnable {
   // ...
   public synchronized void start() {
      if (threadStatus != 0)
         throw new IllegalThreadStateException();

      group.add(this);

      Boolean started = false;
      try {
         start0();
         started = true;
      } finally {
         try {
            if (!started) {
               group.threadStartFailed(this);
            }
         } catch (Throwable ignore) {
         /* do nothing. If start0 threw a Throwable then
            it will be passed up the call stack */
         }
      }
   }
   // ...
   private native void start0();
   //  ...
   private native void setPriority0(int newPriority);
   private native void stop0(Object o);
   private native void suspend0();
   private native void resume0();
   private native void interrupt0();
   private native void setNativeName(String name);
   // ...
}

When a Thread object is created in Java, a call to the start() method starts a new thread. Note that the start() method invokes the start0() native method. The start0() is a platform specific native method written in C/C++. This method is invoked through JNI. The JNI is a specification for native method interface that describes how Java interacts with the program written in native code and how to integrate them in Java. Refer to Java Native Interface Overview.

In Linux JVM, Java threads uses the POSIX APIs calls as its native implementation. The source code given in the preceding link just confirms that.

Therefore, a study on the POSIX thread APIs can reveal more intricate details of how Java threads function internally.

Thread Synchronization and Locks

It's obvious that when there is an execution of multiple threads, there must be some mechanism to establish communication between threads. There are several ways that threads can communicate with each other in Java. But, the problem that occurs during multi-thread communication is the synchronization, without which one thread may inconsistently modify a shared data while another thread is updating it. This result is erroneous and must be avoided.

One of the basic ways to handle synchronization is by using monitors. Every Java object is associated with a monitor. The monitor can be locked or unlocked by the thread, but with the condition that only one thread may hold a lock on a monitor. Any other thread attempting to lock the monitor is blocked until it has been released.

Java provides a keyword to make a synchronized statement. A designated synchronized statement blocks all attempts to access the monitor locks until the lock action by the object's monitor has successfully completed. Once the lock has be made, only then it proceeds to execute the synchronized statements. The programmer need not bother; the unlock action is automatically performed on that same monitor either after successful or abrupt completion. Therefore, statements within a synchronized block ensure consistency.

Java, therefore, does not require any detection of deadlock conditions nor does it prevent it. According to the Java Language Specification, ...Programs where threads hold (directly or indirectly) locks on multiple objects should use conventional techniques for deadlock avoidance, creating higher-level locking primitives that do not deadlock, if necessary.

// Ensures that only one thread can execute at a time the sync
// object is the reference whose lock associates with the monitor
// the code within this block ensures synchronized execution

synchronized (sync_obj) {

  // Process shared resources and variables

}

Conclusion

Using threads in Java with superficial understanding can be confusing and counter intuitive in the long run. Here, we tried to touch upon two most important aspects of multi-threading, although not with complete detail, yet enough to take the study further in the direction of better understanding. There are other intricate aspects of multi-threading that we'll take up subsequently with hints to where and what to look for.






Comment and Contribute

 


(Maximum characters: 1200). You have characters left.

 

 


Enterprise Development Update

Don't miss an article. Subscribe to our newsletter below.

Sitemap

Thanks for your registration, follow us on our social networks to keep up-to-date