Multithreading in JAVA 

Introduction 

In today’s computing landscape, where the demand for faster and more responsive software is ever-growing, the concept of multithreading has become increasingly important.  

What is Multithreading? 

Multithreading is the ability of a CPU to provide concurrent execution of multiple threads. A thread is the smallest unit in a program representing a sequence of instructions. A program can have a set of instructions which are independent of another set of instructions. This allows us to assign independent jobs to different threads. By utilizing multiple threads, a program can perform several tasks simultaneously, sharing the same memory space and resources. This, in turn, can substantially improve performance. 

Why Use Multithreading? 

Multithreading offers several advantages 

Improved Performance 

One of the primary reasons for adopting multithreading is to boost performance. By dividing a task into smaller threads and executing them concurrently, a program can take advantage of multiple CPU cores. This parallel execution can lead to significant speed improvements for computationally intensive or time-critical applications. 

Resource Utilization 

Multithreading optimizes resource utilization by efficiently utilizing the shared resources. This is crucial in server applications, where handling multiple client requests concurrently is essential. 

Modularity 

Threads enable you to structure your code into smaller, manageable units, making it easier to design, understand, and maintain complex systems. 

Ways to Create and Execute Threads :  

In Java, we can create thread in two ways :-  

By extending the Thread class : Thread class internally implements the Runnable interface. 

class MyThread extends Thread { 

 public void run() { 

        // Code to be executed by the user defined thread 

      } 

  } 

MyThread myThread = new MyThread(); 

myThread.start();  // to run the thread  

By implementing Runnable interface  

class MyRunnable implements Runnable { 

    public void run() { 

        // Code to be executed by the user defined thread 

    } 

— 

MyRunnable myRunnable = new MyRunnable(); 

Thread thread = new Thread(myRunnable); 

thread.start(); 

This interface has abstract run(), which is implemented by Thread class so we can further override this method (if the thread was created by extending Thread class) to perform the task by thread, or we can directly implement this run() in our user created thread. 

To start the thread, programmer needs to call start() which internally calls the implemented run() method. 

However, order of thread execution is not determined by the programmer but by the thread scheduler. Since threads execute concurrently and they share the same memory space, this often causes several challenges and can leave the shared resources in an inconsistent state. 

Let’s understand by viewing the example below :- 

Here both threads are executing simultaneously and using the same memory space and the shared data singletonClassObject

Both the threads are accessing getInstance() simultaneously and getting the singletonClassObject as null. Hence, both threads instantiate the class at around the same time. This results in a synchronization issue. 

Challenges with a Multithreaded Environment 

Synchronization 

One of the major challenges with multithreaded environments is when multiple threads access shared data or resources. Synchronization becomes critical. Without proper synchronization mechanisms, race conditions and data corruption can occur. 

The following example represents data corruption as no synchronization mechanism is applied. We can use synchronized blocks or the synchronized keyword on the method level to prevent some of these issues. 

Deadlocks 

Deadlocks can happen when two or more threads are waiting for resources held by each other, causing the program to halt indefinitely.  

Debugging Complexity 

Debugging the application becomes more complex because the order of execution of threads can’t be determined by the programmer. Thread-safe logging and careful error handling are required in a multithreaded environment. 

Rules for synchronization : 

⦁ A thread must acquire the object lock associated with a shared resource before it can enter/use the shared resource. 

⦁ The runtime system ensures that no other thread can enter a shared resource if another thread already holds the object lock associated with it. 

⦁ If a thread cannot immediately acquire the object lock, it is blocked. (i.e, it must wait for lock to become available.) 

⦁ When a thread exits a shared resource, the runtime system ensures that the object lock is also released. If another thread is waiting for this object lock, it can try to acquire the lock to gain access to the shared resource. 

Synchronized Methods 

⦁ While a thread is inside a synchronized method of an object, all other threads that wish to execute this synchronized method or any other synchronized method of the object will have to wait. 

⦁ This restriction doesn’t apply to the thread that already has the lock and is executing a synchronized method of the object. 

⦁ Such a method can invoke other synchronized methods of the object without being blocked. 

⦁ The non-synchronized methods of the object can be called at any time by any thread. 

Synchronized Blocks 

⦁ Execution of synchronized methods of an object are synchronized on the lock of the object, however, the synchronized block allows execution of arbitrary code to be synchronized on the lock of an arbitrary object. 

⦁ We can define a synchronized block as follows:  

              synchronized( object ref expression){<code>}  

⦁ The object ref expression must evaluate to a non-null reference value. 

Conclusion 

Multithreading is a powerful technique that enables developers to create responsive, high-performance applications by harnessing the capabilities of modern hardware. However, it’s not without its challenges, and careful design and implementation are essential to avoid issues like synchronization problems and deadlocks. By following best practices and understanding the specific needs of your application, you can unlock the benefits of multithreading while minimizing its potential pitfalls. 

Author
Latest Blogs

SEND US YOUR RESUME

Apply Now