Java

Exception Handling

Mastering Exception Handling in Java: A Comprehensive Guide

Exception handling is a critical aspect of Java programming that allows developers to gracefully manage errors and unexpected situations in their code. Java provides a robust mechanism for handling exceptions, which is essential for writing reliable and maintainable applications. In this guide, we’ll explore exception handling in Java in depth, covering various aspects including types of exceptions, try-catch blocks, exception propagation, best practices, and more.

Understanding Exceptions & Exception Handling in Java

Exception

An unexpected, unwanted event that disturbs the normal flow of the program is called an exception. For example, TyredPuncturedException, SleepingException, and FileNotFoundException. It is highly recommended to handle exceptions. The main objective of exception handling is the graceful termination of the program. Exception handling doesn’t mean repairing an exception; rather, it involves providing an alternative way to continue the rest of the program normally. This is the concept of exception handling. For example, if our programming requirement is to read data from a remote file located in London at runtime, and if the London file is not available, our program should not terminate abnormally. Instead, we have to provide some local file to continue the rest of the program normally. This method of defining alternatives is nothing but exception handling.

Java
try {
    // Read data from the remote file located in London
} catch (FileNotFoundException e) {
    // Use a local file and continue the rest of the program normally
}

Runtime Stack Mechanism

For every thread, the JVM will create a runtime stack. Each method call performed by that thread will be stored in the corresponding stack. Each entry in the stack is called a stack frame or activation record. After completing every method call, the corresponding entry is removed from the stack. After completing all method calls, the stack will become empty, and that empty stack will be destroyed by the JVM just before terminating the thread. This is an example of when everything goes well. Next, we will discuss the default exception handling in Java.

Default Exception Handling in Java

Inside a method, if any exception occurs, the method in which it is raised is responsible for creating an exception object by including the following:

  1. Name of the exception.
  2. Description of the exception.
  3. Location at which the exception occurs (Stack Trace).

After creating the exception object, the method hands over that object to the JVM. The JVM checks whether the method contains any exception handling code. If the method doesn’t contain exception handling code, the JVM terminates that method abnormally and removes the corresponding entry from the stack. Then, the JVM identifies the caller method and checks whether the caller method contains handling code or not. If the caller method doesn’t contain handling code, the JVM terminates that caller method also abnormally and removes the corresponding entry from the stack. This process continues until the main method. If the main method also doesn’t contain handling code, the JVM terminates the main method abnormally and removes the corresponding entry from the stack. Then, the JVM hands over the responsibility of exception handling to the default exception handler, which is part of the JVM. The default exception handler prints exception information in the following format and terminates the program abnormally:

Java
----------------------------------------------------------------
Exception in thread "xxx": Name of the exception: Description
                      Stack trace
----------------------------------------------------------------

For example:

Java
class Test {
  public static void main(String[] args) {
    doStuff();
  }
  
  public static void doStuff() {
    doMoreStuff();
  }
  
  public static void doMoreStuff() {
    System.out.println(10 / 0);
  }
}

Runtime Stack:

Java
--------------
doMoreStuff()
--------------
doStuff()
--------------
main()
--------------

Output:

Java
Exception in thread "main": java.lang.ArithmeticException: / by zero
                      at Test.doMoreStuff()
                      at Test.doStuff()
                      at Test.main()

Let’s see one more example,

Java
class Test {
  public static void main(String[] args) {
    doStuff();
    System.out.println(10 / 0);
  }
  
  public static void doStuff() {
    doMoreStuff();
    System.out.println("Hi");
  }
  
  public static void doMoreStuff() {
    System.out.println("Hello");
  }
}

Runtime stack

Java
----------------
----------------
----------------
main()
----------------

Output

Java
Hello
Hi
Exception in thread "main" java.lang.ArithmeticException: / by zero
             at Test.main(Test.java:5)

Note: In a program, if at least one method terminates abnormally, then the program termination is considered abnormal. If all methods terminate normally, then the program termination is considered normal.

Exception Hierarchy

The Throwable class acts as the root for the Java exception hierarchy. The Throwable class defines two child classes:

  1. Exception
  2. Error

Exception: Most of the time, exceptions are caused by our program and are considered recoverable. For example:

Java
try {
    // Read data from the remote file located in London
} catch (FileNotFoundException e) {
    // Use a local file and continue the rest of the program normally
}

Error: Errors are non-recoverable. For example, if an OutOfMemoryError occurs, being a programmer, we cannot do anything, and the program will be terminated abnormally. System admins or server admins are responsible for increasing heap memory.

Visual Representation

Java
Throwable

├── Exception
│   │
│   ├── RuntimeException
│   │   │
│   │   ├── ArithmeticException
│   │   ├── NullPointerException
│   │   ├── ClassCastException
│   │   ├── IndexOutOfBoundsException
│   │   │   │
│   │   │   ├── ArrayIndexOutOfBoundsException
│   │   │   └── StringIndexOutOfBoundsException
│   │   └── IllegalArgumentException
│   │       └── NumberFormatException
│   │
│   ├── IOException
│   │   │
│   │   ├── EOFException
│   │   ├── FileNotFoundException
│   │   └── InterruptedIOException
│   │
│   └── ServletException

└── Error

    ├── VirtualMachineError
    │   │
    │   ├── StackOverflowError
    │   └── OutOfMemoryError

    ├── AssertionError
    └── ExceptionInInitializerError

Types of Exceptions

Checked Exceptions

Checked exceptions are the exceptions that are checked at compile-time. These exceptions are subclasses of Exception but not subclasses of RuntimeException. Examples of checked exceptions include IOException, SQLException, and FileNotFoundException. It is mandatory for a method to either handle these exceptions using a try-catch block or declare them using the throws keyword in the method signature.

Unchecked Exceptions

Unchecked exceptions are the exceptions that are not checked at compile-time. They are subclasses of RuntimeException. Examples of unchecked exceptions include NullPointerException, ArrayIndexOutOfBoundsException, and ArithmeticException. It is not mandatory to handle unchecked exceptions explicitly, although it’s good practice to do so.

Checked Exception vs Unchecked Exception

Checked exceptions are those that are checked by the compiler for smooth program execution. Examples include HallTicketMissingException, PenNotWorkingException, and FileNotFoundException. If there is a chance of raising a checked exception in our program, we must handle it either by using try-catch blocks or by declaring it with the throws keyword; otherwise, we will get a compile-time error.

Unchecked exceptions, on the other hand, are not checked by the compiler for whether the programmer handles them or not. Examples include ArithmeticException and BomBlastException.

Note: Regardless of whether an exception is checked or unchecked, every exception occurs at runtime only; there is no chance of an exception occurring at compile time.

Note: RuntimeException and its child classes, as well as Error and its child classes, are unchecked. All other exceptions are checked.

Fully Checked vs Partially Checked

A checked exception is fully checked if all its child classes are also checked, e.g., IOException and InterruptedException. A checked exception is partially checked if only some of its child classes are unchecked, such as Exception and Throwable.

Exception Behavior Description:

  • IOException: Checked (fully)
  • RuntimeException: Unchecked
  • InterruptedException: Checked (fully)
  • Error: Unchecked
  • Throwable: Checked (partially)
  • ArithmeticException: Unchecked
  • NullPointerException: Unchecked
  • Exception: Checked (partially)
  • FileNotFoundException: Checked (fully)

Customized Exception Handling by Using Try-Catch

It is highly recommended to handle exceptions. The code that may raise an exception is called risky code, and we have to define that code inside the try block. The corresponding handling code should be defined inside the catch block.

Java
try {
    // Risky code
} catch(Exception e) {
    // Handling code
}

Without Try-Catch

Java
class Test {
    public static void main(String[] args) {
        System.out.println("stmt 1");
        System.out.println(10/0);
        System.out.println("stmt 3");
    }
}

Output:

Java
stmt 1
RE: ArithmeticException: / by zero

Abnormal Termination

With Try-Catch

Java
class Test {
    public static void main(String[] args) {
        System.out.println("stmt 1");
        try {   
            System.out.println(10/0);
        } catch(ArithmeticException e) {
            System.out.println(10/2);
        }
        System.out.println("stmt 3");
    }
}

Output:

Java
Stmt 1
5
Stmt 3

Normal Termination

In the first example without try-catch, the program encounters an ArithmeticException due to division by zero, leading to abnormal termination. In the second example with try-catch, the exception is caught, and the program continues executing the remaining statements, resulting in normal termination.

Control Flow in Try-Catch

Java
try {
    stmt1;
    stmt2;
    stmt3;
} catch(Exception e) {
    stmt4;
}
stmt5;

Case 1: If there is no exception, then output will be (1, 2, 3, 5, Normal Termination).

Case 2: If an exception is raised at statement 2 and the corresponding catch block is matched, then the output will be (1, 4, 5, Normal Termination).

Case 3: If an exception is raised at statement 2 and the corresponding catch block is not matched, then the output will be (1, Abnormal Termination).

Case 4: If an exception is raised at statement 4 or statement 5, then it is always an abnormal termination.

Note:

  1. Within the try block, if an exception is raised anywhere, then the rest of the try block won’t be executed, even if we handle that exception. Therefore, within a try block, we should only place risky code, and the length of the try block should be as short as possible.
  2. In addition to the try block, there may be a chance of an exception occurring inside catch and finally blocks. If any statement outside the try block raises an exception, then it always results in an abnormal termination.

Methods to Print Exception Information

The Throwable class defines the following methods to print exception information:

printStackTrace(): This method prints the exception’s name, description, and stack trace, showing the sequence of method calls that led to the exception.

Printable Format:

Java
NameOfException: Description
Stack Trace

toString(): Returns a string representation of the exception, including its name and description.

Java
Name of Exception: Description

getMessage(): Returns the description of the exception.

Java
Description

Example Program:

Java
class Test {
    public static void main(String[] args) {
        try {
            System.out.println(10/0);
        } catch(ArithmeticException e) {
            e.printStackTrace(); // java.lang.ArithmeticException: / by zero at Test.main(Test.java:4)
            System.out.println(e); // java.lang.ArithmeticException: / by zero
            System.out.println(e.toString()); // java.lang.ArithmeticException: / by zero
            System.out.println(e.getMessage()); // / by zero
        }
    }
}

Note: Internally, the default exception handler will use printStackTrace() to print stack trace information to the console.

try with multiple catch blocks

The way of handling an exception varies from exception to exception; hence, for every exception type, it is highly recommended to use a separate catch block. Using try with multiple catch blocks is always possible and recommended.

Worst Programming Practice:

Java
try {
    // Risky code
} catch(Exception e) {
    // For all exceptions, use this single catch block
}

Best Programming Practice:

Java
try {
    // Risky Code
} catch(ArithmeticException e) {
    // Perform alternative arithmetic operation
} catch(SqlException e) {
    // Use MySQL DB instead of Oracle DB
} catch(FileNotFoundException e) {
    // Use a local file instead of a remote file
} catch(Exception e) {
    // Default exception handling
}

In the best programming practice approach, each specific exception type is caught and handled appropriately, providing a more tailored and robust error-handling mechanism.

Important Loopholes

Some important loopholes in exception handling:

1. Order of catch blocks matters

If a try with multiple catch blocks is present, then the order of catch blocks is very important. We have to take the child exceptions first and then the parent exceptions; otherwise, we will get a compile-time error saying “Exception XXX has already been caught”.

Java
try {
    // Risky code
} catch (Exception e) {
    // Parent exception catch block
} catch (ArithmeticException e) {
    // Child exception catch block
}

// CE: Exception java.lang.ArithmeticException has already been caught

The correct code order should be:

Java
try {
    // Risky code
} catch (ArithmeticException e) {
    // Child exception catch block
} catch (Exception e) {
    // Parent exception catch block
}
2. Cannot declare duplicate catch blocks

We cannot declare two catch blocks for the same exception; otherwise, we will get a compile-time error.

Java
try {
    // Risky code
} catch (ArithmeticException e) {
    // Catch block for ArithmeticException
} catch (ArithmeticException e) {
    // Another catch block for ArithmeticException (Duplicate)
}

// CE: Exception java.lang.ArithmeticException has already been caught

The correct code should be:

Java
try {
    // Risky code
} catch (ArithmeticException e) {
    // Catch block for ArithmeticException
} catch (Exception e) {
    // Catch block for other exceptions
}

These loopholes highlight the importance of careful handling and structuring of catch blocks to ensure effective exception management in Java programs.

Difference between final, finally, and finalize

final

  • final is a modifier applicable for classes, methods, and variables.
  • If a class is declared as final, then we can’t extend that class, meaning we can’t create a child class for that class. Inheritance is not possible for final classes.
  • If a method is final, then we can’t override that method in the child class. If a variable is declared as final, then we can’t perform reassignment for that variable.

finally

  • finally is a block always associated with try-catch to maintain clean-up code.
  • The speciality of the finally block is that it will be executed always, irrespective of whether an exception is raised or not, and whether it is handled or not.
  • It is responsible for performing clean-up activities related to the try block. Any resources opened as part of the try block will be closed inside the finally block.
Java
try {
    // Risky code
} catch (Exception e) {
    // Handling code
} finally {
    // Clean-up code
}

finalize()

  • finalize() is a method always invoked by the garbage collector just before destroying an object to perform clean-up activities.
  • Once the finalize() method completes, immediately the garbage collector destroys that object.
  • It is responsible for performing clean-up activities related to the object. Any resources associated with the object will be deallocated before destroying the object using the finalize() method.

Note:

The finally block is responsible for performing clean-up activities related to the try block. Any resources opened as part of the try block will be closed inside the finally block. It ensures that resources are released regardless of whether an exception occurs or not.

On the other hand, the finalize() method is responsible for performing clean-up activities related to the object. It is invoked by the garbage collector just before destroying an object. Any resources associated with the object will be deallocated before destroying the object using the finalize() method. This method is used to release resources held by the object and perform any necessary clean-up actions before the object is removed from memory.

In short, final is used to denote immutability or prevent extension/overriding, finally is used for try-catch cleanup, and finalize() is used for object-specific cleanup just before garbage collection. Each serves a distinct purpose in Java programming.

Various Possible Combinations of Try-Catch-Finally

Rules:

  1. Order is Important: In try-catch-finally, the order is important, the order should be try, catch, and then finally.
  2. Compulsory Usage: Whenever we write try, it’s compulsory to include either catch or finally; otherwise, we will get a compile-time error (try without catch or finally is invalid).
  3. Compulsory Try Block for Catch: Whenever we write a catch block, a try block must be present; catch without try is invalid.
  4. Compulsory Try Block for Finally: Whenever we write a finally block, a try block should be present; finally without try is invalid.
  5. Nesting of Try-Catch-Finally: Inside try-catch-finally blocks, we can nest additional try-catch-finally blocks; nesting of try-catch-finally is allowed.
  6. Curly Braces Requirement: Curly braces ({}) are mandatory for try-catch-finally blocks.

Examples:

Valid:

Java
try {
  // code
} catch(X e) {
  // exception handling code
}

Valid:

Java
try {
  // code
} catch(X e) {
  // exception handling code
} catch(Y e) {
  // exception handling code
}

Invalid:

Java
try {
  // code
} catch(X e) {
  // exception handling code
} catch(X e) {
  // exception handling code
}

// CE: exception X has already been caught

Valid:

Java
try {
  // code
} catch(X e) {
  // exception handling code
} finally {
  // cleanup code
}

Valid:

Java
try {
  // code
} finally {
  // cleanup code
}

Valid:

Java
try {
  // code
} catch(X e) {
  // exception handling code
} try {
  // code
} catch(Y e) {
  // exception handling code
}

Valid:

Java
try {
  // code
} catch(X e) {
  // exception handling code
} try {
  // code
} finally {
  // cleanup code
}

Invalid:

Java
try {
  // code
}
// CE: try without catch (or) finally

Invalid:

Java
catch(X e) {
  // exception handling code
}
// CE: catch without try

Invalid:

Java
finally {
  // cleanup code
}
// CE: finally without try

Invalid:

Java
try {
  // code
} finally {
  // cleanup code
} catch(X e) {
  // exception handling code
}
// CE: catch without try 

Invalid:

Java
try {
  // code
} System.out.println("Hello");
catch(X e) {
  // exception handling code
}
// CE1: try without catch or finally 
// CE2: catch without try

Invalid:

Java
try {
  // code
} catch(X e) {
  // exception handling code
} System.out.println("Hello");
catch(Y e) {
  // exception handling code
}
// CE: catch without try

Invalid:

Java
try {
  // code
} catch(X e) {
  // exception handling code
} System.out.println("Hello");
finally {
  // cleanup code
}
// CE: finally without try

Valid:

Java
try {
  try {
    // code
  } catch(X e) {
    // exception handling code
  }
} catch(X e) {
}

Invalid:

Java
try {
  try {
   // code
  }
} catch(X e) {
  // exception handling code
}
// CE: try without catch or finally 

Valid:

Java
try {
  try {
    // code
  } finally {
    // cleanup code
  }
} catch(X e) {
  // exception handling code
}

Valid:

Java
try {
  // code
} catch(X e) {
  try {
  } finally {
  }
}

Invalid:

Java
try {
  // code
} catch(X e) {
 finally {
 }
}
// CE: finally without try

Valid:

Java
try {
  // code
} catch(X e) {
  // exception handling code
} finally {
  try {
  } catch(X e) {
  }
}

Invalid:

Java
try {
  // code
} catch(X e) {
  // exception handling code
} finally {
  finally {
  }
}
// CE: finally without try

Invalid:

Java
try {
  // code
} catch(X e) {
  // exception handling code
} finally {
}
finally {
}
// CE: finally without try

Invalid:

Java
try
 System.out.println("try");
catch(X e)
 System.out.println("catch");
finally
{
}
// CE: finally without try | Syntax error, insert "}" to complete Block

Invalid:

Java
try{
  // code
}
catch(X e)
System.out.println("catch");
finally{
  // cleanup code
}
// CE: finally without try | Syntax error, insert "{" to complete Block

Invalid:

Java
try {
  // code
} catch(X e) {
} finally
System.out.println("finally");

// CE: finally without try | Syntax error on token "System", invalid Expression

Valid:

Java
try {
    try {
        // code
    } catch(Exception e) {
        // inner catch block
    } finally {
        // inner finally block
    }
} catch(Exception e) {
    // outer catch block
} finally {
    // outer finally block
}

These examples demonstrate the various valid and invalid combinations of try-catch-finally blocks according to the specified rules.

Throw (throw keyword)

The throw keyword is used to manually create and throw an exception object. Sometimes, we need to generate exception objects explicitly and hand them over to the JVM

Java
throw new ArithmeticException("/ by zero");

Here, we are creating an ArithmeticException object explicitly and throwing it using the throw keyword.

The primary purpose of the throw keyword is to hand over our created exception object to the JVM manually.

The result of the following two programs is exactly the same:

Without throw keyword:

Java
class Test {
    public static void main(String[] args) {
        System.out.println(10/0);
    }
}

Output:

Java
Exception in thread "main" java.lang.ArithmeticException: / by zero
    at Test.main(Test.java:3)

In this case, the main method is responsible for causing the ArithmeticException and implicitly hands over the exception object to the JVM.

With throw keyword:

Java
class Test {
    public static void main(String[] args) {
        throw new ArithmeticException("/ by zero");
    }
}

Output:

Java
Exception in thread "main" java.lang.ArithmeticException: / by zero
    at Test.main(Test.java:3)

In this case, the programmer is explicitly creating an AE (ArithmeticException) object and throwing it using the throw keyword. This means the programmer is manually handing over the exception object to the JVM.

Note: The best use of the throw keyword is for user-defined or customized exceptions.

Java
class Test {
    static AE e = new AE();
    public static void main(String[] args) {
        throw e;
    }
}


//Output - Runtime Error: AE

In this case, the throw keyword is used to throw a reference to an exception object e (AE object)

Java
class Test {
    static AE e;
    public static void main(String[] args) {
        throw e;
    }
}


//Output - Runtime Error: NullPointerException

Here, e is a static member variable of type AE, but it is not initialized explicitly. Therefore, it defaults to null. When attempting to throw e, a NullPointerException occurs.

So, the throw keyword is used to explicitly throw exceptions, which can be either built-in or user-defined. However, it’s crucial to ensure that the exception object being thrown is properly initialized to avoid runtime errors like NullPointerException.

Unreachable Statement after throw:

After a throw statement, any code that follows it becomes unreachable because the exception is immediately thrown, and the program flow doesn’t continue beyond that point.

Java
class Test {
    public static void main(String[] args) {
        System.out.println(10/0);
        System.out.println("Hello");
    }
}


//Runtime Error: AE: / by zero

Exception in thread "main" java.lang.ArithmeticException: / by zero
    at Test.main(Test.java:3)

Unreachable Statement Error:

If there are statements after a throw statement, they are considered unreachable, and the compiler raises a compile-time error indicating “unreachable statement”.

Java
class Test {
    public static void main(String[] args) {
        throw new AE("/ by zero");
        System.out.println("Hello");
    }
}


//Compile Error: unreachable statement

Usage of throw with Non-Throwable Types:

The throw keyword is only applicable for throwable types (subclasses of Throwable). If we attempt to use it with non-throwable types, it results in a compile-time error due to incompatible types.

Java
class Test {
    public static void main(String[] args) {
        throw new Test();
    }
}


//Compile Error: incompatible types, found: Test, required: java.lang.Throwable

Compilation Error: incompatible types
    found:    Test
    required: java.lang.Throwable

Inheriting from RuntimeException:

If a class extends RuntimeException, it becomes a throwable type, and instances of this class can be thrown using the throw keyword.

Java
class Test extends RuntimeException {
    public static void main(String[] args) {
        throw new Test();
    }
}


// runtime exception

Exception in thread "main" Test
    at Test.main(Test.java:3)

In this example, Test is a subclass of RuntimeException, so it can be thrown without any compilation errors. When executed, it results in a runtime exception.

Throws (throws keyword)

In Java, if there is a possibility of a checked exception being thrown within a method, the method must either handle the exception using a try-catch block or declare that it throws the exception using the throws keyword in its method signature.

Java
import java.io.*;

class Test {
    public static void main(String[] args) {
        PrintWriter pw = new PrintWriter("abc.txt");
        pw.println("Hello");
    }
}


//Compilation Error: unreported exception java.io.FileNotFoundException; must be caught or declared to be thrown

Here, the PrintWriter constructor throws a checked exception FileNotFoundException, which is not handled. Hence, a compilation error occurs.

To handle this error, you can either use a try-catch block to handle the exception:

Java
import java.io.*;

class Test {
    public static void main(String[] args) {
        try {
            PrintWriter pw = new PrintWriter("abc.txt");
            pw.println("Hello");
        } catch (FileNotFoundException e) {
            // Handle the exception
            e.printStackTrace();
        }
    }
}

Or we can declare that the method throws the exception using the throws keyword:

Java
import java.io.*;

class Test {
    public static void main(String[] args) throws FileNotFoundException {
        PrintWriter pw = new PrintWriter("abc.txt");
        pw.println("Hello");
    }
}

Using the throws keyword delegates the responsibility of handling the exception to the caller of the method.

Note: It’s recommended to handle exceptions using try-catch blocks where possible, as using throws may propagate exceptions up the call stack without handling them properly.

Let’s take one more example,

Java
class Test {
    public static void main(String[] args) {
        Thread.sleep(10000);
    }
}

//CE: unreported exception java.lang.InterruptedException must be caught or declared to be thrown

The Thread.sleep() method may throw an InterruptedException, which is a checked exception. Since this exception is not handled within the main method, nor is it declared using the throws keyword in the method signature, the compiler raises a compilation error:

Here, we can declare that the method throws the exception using the throws keyword:

Java
class Test {
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(10000);
    }
}

Using the throws keyword delegates the responsibility of handling the exception to the caller of the method. However, it’s important to handle exceptions properly to prevent unexpected behavior in the program.

By Using throws Keyword

The throws keyword in Java is used in method declarations to indicate that a method might throw certain exceptions during its execution. This alerts the caller of the method that they need to handle or propagate these exceptions.

Java
class Test {
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(10000);
    }
}

In this example, the main method uses the throws keyword to declare that it might throw an InterruptedException during its execution. This means that if the sleep() method within main throws an InterruptedException, the caller of main (in this case, the JVM) must handle it.

Similarly, if a method calls another method that declares a checked exception using throws, the calling method must either handle that exception or re-throw it using throws.

Java
class Test {
    public static void main(String[] args) throws IE {
        doStuff();
    }

    public static void doStuff() throws IE {
        doMoreStuff();
    }

    public static void doMoreStuff() throws IE {
        Thread.sleep(10000);
    }
}

In this example, the doStuff method declares that it might throw an IE (hypothetical checked exception). Consequently, since main calls doStuff, it must either handle IE or declare that it throws it, as it propagates up the call stack from doMoreStuff().

However, it’s important to note that using throws doesn’t prevent abnormal termination of the program; it’s merely a way to indicate potential exceptions that might be thrown. Additionally, throws is only required for checked exceptions; there’s no impact or requirement for unchecked exceptions.

throws clause

  1. The throws clause can be used to delegate the responsibility of exception handling to the caller, whether it is a method or the JVM.
  2. It is required only for checked exceptions; the usage of the throws keyword for unchecked exceptions has no impact.
  3. It is required only to convince the compiler; the usage of throws does not prevent the abnormal termination of the program.

Note: It is recommended to use try-catch blocks over the throws keyword.

Case 1: We can use the throws keyword for methods and constructors, but not for classes.

Java
class Test throws Exception   //invalid 
{
    Test() throws Exception   // valid 
    {
    }
    public void m1() throws Exception  //valid  
    {
    }
}

Case 2: We can use the throws keyword only for throwable types. If we try to use it for normal Java classes, we will get a compile-time error saying ‘incompatible types.

Java
class Test
{
    public void m1() throws Test
    {
    }
}

// Compile Error: Incompatible types
// Found: Test
// Required: java.lang.Throwable

Case 3:

Java
class Test
{
    public static void main(String[] args)
    {
        throw new Exception(); // Exception is checked
    }
}

//Compile Error: Unreported exception java.lang.Exception; must be caught or declared to be thrown
Java
class Test
{
    public static void main(String[] args)
    {
        throw new Error(); // Error is unchecked
    }
}

//Runtime Error: 
Exception in thread "main" java.lang.Error
at Test.main(Test.java:lineNumber)

Case 4: Within a try block, if there is no chance of raising an exception, then we can’t write a catch block for that exception; otherwise, we will get a compilation error: ‘Exception XXX is never thrown in the body of the corresponding try statement.’ However, this rule is applicable only for fully checked exceptions.

Java
class Test
{
    public static void main(String[] args)
    {
        try 
        {
            System.out.println("Hello");
        }
        catch(AE e)   // AE is unchecked exception 
        {
        }
    }
}


Examples and Outputs

Java
class Test
{
    public static void main(String[] args)
    {
        try 
        {
            System.out.println("Hello");
        }
        catch(AE e)   // AE is unchecked exception 
        {
        }
    }
}

/////////////////////////////////////////////////

class Test 
{
    public static void main(String[] args)
    {
        try
        {
            System.out.println("hello");
        }
        catch(Exception e)   // partially checked 
        {
        }
    }
}

//////////////////////////////////////////////////////////////////

class Test
{
    public static void m(String[] args)
    {
        try
        {
            System.out.println("Hello");
        }
        catch(Error e)  // unchecked
        {
        }
    }
}

// Output - hello (in all three cases)

Java
import java.io.*;
class Test
{
    public static void main(String[] args)
    {
        try
        {
            System.out.println("Hello");
        }
        catch(IOException e)   // Fully checked
        {
        }
    }
}

// Compile Error: Exception java.io.IOException is never thrown in the body of the corresponding try statement


////////////////////////////////////////////////////////////////////////////////////////

class Test
{
    public static void main(String[] args)
    {
        try
        {
            System.out.println("Hello");
        }
        catch(InterruptedException e)  // fully checked
        {
        }
    }
}

// Compile Error: Exception java.lang.InterruptedException is never thrown in the body of the corresponding try statement

Customized or user-defined exception

Sometimes, to meet programming requirements, we can define our own exceptions. Such types of exceptions are called customized or user-defined exceptions, e.g., TooYoungException, TooOldException, InsufficientFundsException, etc.

Java
class TooYoungException extends RuntimeException {
    TooYoungException(String s) {
        super(s);
    }
}

class TooOldException extends RuntimeException {
    TooOldException(String s) {
        super(s);
    }
}

class CustomizedExceptionDemo {
    public static void main(String[] args) {
        int age = Integer.parseInt(args[0]);
        if (age > 60) {
            throw new TooYoungException("Please wait some more time; definitely, you will get the best match.");
        } else if (age < 18) {
            throw new TooOldException("Your age already crossed the marriage age, and there is no chance of getting married.");
        } else {
            System.out.println("You will get match details soon by email!");
        }
    }
}

Note:

  1. The throw keyword is best suitable for user-defined or customized exceptions but not for predefined exceptions.
  2. It is highly recommended to define customized exceptions as unchecked, i.e., we have to extend RuntimeException but not Exception.

In the example, why is super(s) required?

  • super(s) is required to pass the description message to the superclass constructor, making it available to the default exception handler.

Exception Handling CheatSheet

Exception handling keyword summary

  1. try –> Used to encapsulate risky code.
  2. catch –> Used to handle exceptions.
  3. finally –> Used to execute cleanup code.
  4. throw –> Used to manually pass our created exception object to the JVM.
  5. throws –> Used to delegate the responsibility of exception handling to the caller.

Various possible compiler errors in exception handling

  1. “Unreported exception XXX; must be caught or declared to be thrown.”
  2. “Exception XXX has already been caught.”
  3. “Exception XXX is never thrown in the body of the corresponding try statement.”
  4. “Unreachable statement.”
  5. “Incompatible types: found: Test, required: java.lang.Throwable.”
  6. “Try without catch or finally.”
  7. “Catch without try.”
  8. “Finally without try.”

Top 10 Exceptions in Java

Exceptions in Java are divided into two categories based on the entity raising them:

  1. JVM Exception: These exceptions are raised automatically by the JVM whenever particular events occur. Examples include ArithmeticException and NullPointerException.
  2. Programmatic Exception: These exceptions are raised explicitly, either by the programmer or by API developers, to indicate that something has gone wrong. Examples include TooOldException and IllegalArgumentException.

Top Ten Exceptions

1. ArrayIndexOutOfBoundsException: This exception is a subclass of RuntimeException and hence is unchecked. It is automatically raised by the JVM when attempting to access an array element with an index that is out of the array’s range. For example:

Java
int[] x = new int[4]; // Indices 0 to 3
System.out.println(x[0]); // 0
System.out.println(x[10]); // Throws ArrayIndexOutOfBoundsException
System.out.println(x[-10]); // Throws ArrayIndexOutOfBoundsException

2. NullPointerException: This exception is a subclass of RuntimeException and hence is unchecked. It is automatically raised by the JVM when attempting to perform any operation on a null object. For example:

Java
String s = null;
System.out.println(s.length()); // Throws NullPointerException

3. ClassCastException: This exception is a subclass of RuntimeException and hence is unchecked. It is automatically raised by the JVM when attempting to type cast a parent object to a child type.

Valid example:

Java
String s = new String("durga");
Object o = (Object)s;

Invalid example:

Java
Object o = new Object();
String s = (String)o;
// Throws ClassCastException

4. StackOverflowError: This error is a subclass of Error and hence is unchecked. It is automatically raised by the JVM when attempting to perform a recursive method call.

Java
class Test {
    public void m1() {
        m2();
    } 

    public void m2() {
        m1();
    }

    public static void main(String[] args) {
        Test t = new Test();
        t.m1();
    }
}

The recursive method calls in this code snippet would lead to a StackOverflowError:

Java
       .
       .
       .
       .
------------------
      m2()
------------------
      m1()
------------------
      m2()
------------------
      m1()
------------------
     main()
------------------



// RE : StackOverFlowError

5. NoClassDefFoundError: This error is a subclass of Error and hence is unchecked. It is automatically raised by the JVM when it is unable to find the required .class file. For example, if the Test.class file is not available, then attempting to run java Test will result in a runtime exception with the message: “NoClassDefFoundError: Test”.

6. ExceptionInInitializerError: This error is a subclass of Error and hence is unchecked. It is automatically raised by the JVM if any exception occurs while executing static variable assignments or static blocks.

Java
class Test {
    static int x = 10/0;
}

// ExceptionInInitializerError 
         caused by java.lang.ArithmeticException: / by zero

Another example:

Java
class Test {
    static {
        String s = null;
        System.out.println(s.length());
    }
}


// ExceptionInInitializerError caused by: java.lang.NullPointerException

7. IllegalArgumentException: This exception is a subclass of RuntimeException and hence is unchecked. It is raised explicitly either by the programmer or by API developers to indicate that a method has been invoked with an illegal argument. For example, suppose the valid range of thread priority is 1 to 10. If we attempt to set the priority with any other value, then we will get a RuntimeException with the message “IllegalArgumentException”

Java
Thread t = new Thread();
t.setPriority(7); // Valid
t.setPriority(15); // Throws IllegalArgumentException

8. NumberFormatException: This exception is a direct subclass of IllegalArgumentException and indirectly a subclass of RuntimeException, making it unchecked. It is raised explicitly either by the programmer or API developers to indicate that a conversion from a string to a number has been attempted, but the string is not properly formatted.

Java
int i = Integer.parseInt("10"); // Valid
int i = Integer.parseInt("ten"); // Throws NumberFormatException

9. IllegalStateException: This exception is a subclass of RuntimeException and hence is unchecked. It is raised explicitly either by the programmer or API developers to indicate that a method has been invoked at the wrong time. For example, after starting a thread, we are not allowed to restart the same thread once again; otherwise, we will get a RuntimeException with the message “IllegalThreadStateException”.

Java
// Example of IllegalThreadStateException
Thread t = new Thread();
t.start();
t.start(); // Throws IllegalStateException

10. AssertionError: This exception is a subclass of Error and hence is unchecked. It is raised explicitly by the programmer or by API developers to indicate that an assert statement fails. For example, if the condition x > 10 is not met, then we will get a RuntimeException with the message “AssertionError”.

Java
// Example of AssertionError
int x = 5;
assert(x > 10); // Throws AssertionError

Raised by:

Raised automatically by the JVM and hence these are JVM exceptions:
  • ArrayIndexOutOfBoundsException
  • NullPointerException
  • ClassCastException
  • StackOverflowError
  • NoClassDefFoundError
  • ExceptionInInitializerError
Raised explicitly either by the programmer or by API developer and hence these are programmatic exceptions:
  • IllegalArgumentException
  • NumberFormatException
  • IllegalStateException
  • AssertionError

Java 1.7v Enhancements in Exception Handling

As part of version 1.7, two concepts were introduced for exception handling:

  1. Try-with-Resources
  2. Multi-catch block

Until version 1.6, it was highly recommended to write a finally block to close resources that were opened as part of a try block:

Java
try {
    br = new BufferedReader(new FileReader("input.txt"));
    // Use br based on our requirement
} catch (IOException e) {
    // Handling code
} finally {
    if (br != null) {
        br.close();
    }
}

The problems with the above approach are:

  • The programmer is required to close resources inside the finally block, increasing the complexity of programming.
  • The finally block is mandatory, which increases the length of the code and reduces readability.

To overcome these problems, the developers introduced try-with-resources in version 1.7.

The main advantage of try-with-resources is that whatever resources we open as part of the try block will be closed automatically once control reaches the end of the try block, either normally or abnormally. Therefore, explicit closure is not required, reducing the complexity of programming. Additionally, there’s no need to write a finally block, which reduces the length of the code and improves readability.

1.7v Try with Resources

In version 1.7, the try-with-resources statement was introduced:

Java
try (BR br = new BR(new FR("input.txt"))) {
    // Use br based on our requirement 
    // br will be closed automatically once control reaches the end of the try block, either normally 
    // or abnormally, and we are not responsible for closing it explicitly
} catch (IOException e) {
    // Handling code
}

In this syntax, resources declared within the parentheses after the try keyword are automatically closed when the try block exits, whether normally or due to an exception. This feature simplifies resource management and improves code readability.

Try with Multiple Resources (1.7v)

In version 1.7, it became possible to declare and manage multiple resources in a single try-with-resources statement. These resources should be separated with semicolons:

Java
try (R1; R2; R3) {
    // Code block
}

For example:

Java
try (FileWriter fw = new FileWriter("Output.txt"); FileReader fr = new FileReader("input.txt")) {
    // Code block
}

All resources specified within the try-with-resources statement must implement the AutoCloseable interface. A resource is considered AutoCloseable if the corresponding class implements the java.lang.AutoCloseable interface.

It’s worth noting that many I/O-related, database-related, and network-related resources already implement the AutoCloseable interface. As programmers, we are not required to do anything specific other than being aware of this feature.

AutoCloseable Interface in Java 1.7v

The AutoCloseable interface was introduced in version 1.7 and contains only one method: close().

All resource variables declared within a try-with-resources statement are implicitly final. Therefore, within a try block, we cannot perform reassignment to these variables; otherwise, we will encounter a compilation error. For example:

Java
import java.io.*;

class TryWithResources {
    public static void main(String[] args) throws Exception {
        try (BufferedReader br = new BufferedReader(new FileReader("input.txt"))) {
            br = new BufferedReader(new FileReader("output.txt")); // Compilation Error
        }
    }
}

Compilation Error:

Java
AutoCloseable resource 'br' may not be assigned.
BufferedReader br = new BufferedReader(new FileReader("Output.txt"));

Until version 1.6, the try block should be associated with either catch or finally. However, from version 1.7 onward, we can use try-with-resources without explicitly specifying catch or finally blocks:

Java
try (R) {
    // Code block
}

The main advantage of try-with-resources is that we are not required to write a finally block explicitly because we do not need to close resources explicitly. Therefore, until version 1.6, the finally block was considered essential, but from version 1.7 onward, it becomes unnecessary.

Multi-Catch Block

Until version 1.6, even if multiple different exceptions required the same handling code, separate catch blocks had to be written for each exception type. This approach increased the length of the code and reduced readability:

Java
try {
    // Code block
} catch (AE e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
} catch (NPE e) {
    System.out.println(e.getMessage());
} catch (InterruptedException e) {
    System.out.println(e.getMessage());
}

To overcome this problem, the developers introduced the multi-catch block in version 1.7. With multi-catch blocks, a single catch block can handle multiple different types of exceptions:

Java
try {
    // Code block
} catch (AE | IOException e) {
    e.printStackTrace();
} catch (NPE | InterruptedException e) {
    System.out.println(e.getMessage());
}

The main advantage of this approach is that the length of the code is reduced, and readability is improved.

Java
class MultiCatchBlock {
    public static void main(String[] args) {
        try {
            System.out.println(10/0);
            String s = null;
            System.out.println(s.length());
        } catch (AE | NullPointerException e) {
            System.out.println(e);
        }
    }
}

In the above example, whether the raised exception is AE or NPE, the same catch block can handle it.

In a multi-catch block, there should not be any relation between exception types, such as child-to-parent, parent-to-child, or same type; otherwise, a compilation error will occur.

Java
try {
    // Code block
} catch (AE | Exception e) { 
    e.printStackTrace();
}

//Compilation Error: Alternatives in a multi-catch statement cannot be related by subclassing.

Exception Propagation: Inside a method, if an exception is raised and not handled, the handling will be propagated to the caller. Then, the caller method is responsible for handling the exception. This process is called exception propagation.

Re-throwing Exception: This approach is used to convert one exception to another exception.

Java
try {
    System.out.println(1 / 0);
} catch (AE e) {
    throw new NullPointerException();
}

Conclusion

Exception handling is an essential aspect of Java programming that enables developers to write robust and reliable code. By understanding the different types of exceptions, using try-catch blocks effectively, and following best practices for exception handling, you can create Java applications that gracefully handle errors and unexpected situations. Remember to handle exceptions appropriately, provide informative error messages, and clean up resources properly to ensure the reliability and maintainability of your code.

Multithreading Enhancements

Multithreading Enhancements: Supercharge Your Applications, Boosting Performance and Efficiency Part 5

Multithreading is a programming concept that enables simultaneous execution of multiple threads within a process, allowing for better resource utilization and improved performance. In recent years, advancements in hardware and software technologies have led to significant enhancements in multithreading techniques. In this blog, we’ll delve into the latest multithreading enhancements and their implications for software development.

Thread Group: First Multithreading Enhancements

Based on functionality, we can group a thread into a single unit, which is nothing but a thread group. That is, a thread group contains a group of threads. In addition to threads, a thread group also contains sub-thread groups.

The main advantage of maintaining threads in the form of thread groups is that we can perform common operations easily.

Every thread in Java belongs to some group. The main thread belongs to the main group. Every thread group in Java is a child group of the system group, either directly or indirectly. Hence, the system group acts as the root for all thread groups in Java. The system group contains several system-level threads like finalizer, reference handler, signal dispatcher, and attach listener.

Java
class Test {
  public static void main(String[] args) {
    System.out.println(Thread.currentThread().getThreadGroup().getName());  // Output: main 
    System.out.println(Thread.currentThread().getThreadGroup().getParent().getName());  // System
                         // main thread      main ThreadGroup  System TG System
  }
}

Here, “main thread” belongs to “main ThreadGroup,” and “System ThreadGroup” belongs to “System.”

Thread Group Constructors

ThreadGroup is a Java class present in the java.lang package and is a direct child class of Object.

Constructors:

Java
ThreadGroup g = new ThreadGroup(String gName);

// Example usage:
ThreadGroup g = new ThreadGroup("First Group");

This constructor creates a new thread group with the specified name. The parent of this new group is the thread group of the currently executing thread.

Java
ThreadGroup g = new ThreadGroup(ThreadGroup tg, String groupName);


// Example usage:
ThreadGroup g1 = new ThreadGroup("First Group")
System.out.println(g1.getParent().getName());  // main
ThreadGroup g2 = new ThreadGroup(g1, "Second Group");
System.out.println(g2.getParent().getName());  // First Group

This constructor creates a new thread group with the specified group name. The parent of this new thread group is the specified parent group.

In the above example, the parent group for “Second Group” is explicitly set to “First Group,” demonstrating the flexibility of this constructor.

Thread Group Methods

Various methods present in ThreadGroup:

  1. String getName(): Returns the name of the ThreadGroup.
  2. int getMaxPriority(): Retrieves the maximum priority of the thread group.
  3. void setMaxPriority(int priority): Sets the maximum priority of the thread group. The default maximum priority is 10. Threads in the thread group that already have a higher priority won’t be affected, but for newly added threads, this maximum priority is applicable.
Java
// Example usage:
ThreadGroup g1 = new ThreadGroup("tg");
Thread t1 = new Thread(g1, "Thread1");
Thread t2 = new Thread(g1, "Thread2");
g1.setMaxPriority(3);
Thread t3 = new Thread(g1, "Thread3");
System.out.println(t1.getPriority()); // 5
System.out.println(t2.getPriority()); // 5
System.out.println(t3.getPriority()); // 3
  1. ThreadGroup getParent(): Returns the parent group of the current thread group.
  2. void list(): Prints information about the thread group to the console.
  3. int activeCount(): Returns the number of active threads present in the thread group.
  4. int activeGroupCount(): Returns the number of active thread groups present within the current thread group.
  5. int enumerate(Thread[] t): Copies all active threads of this thread group into the provided Thread[] array. This includes threads from sub-thread groups.
  6. int enumerate(ThreadGroup[] g): Copies all active sub-thread groups into a ThreadGroup[] array.
  7. boolean isDaemon(): Checks whether the thread group is a Daemon or not.
  8. void setDaemon(boolean daemon): Sets the Daemon nature of the current thread group.
  9. void interrupt(): Interrupts all waiting or sleeping threads present in the thread group.
  10. void destroy(): Destroys the thread group and its sub-thread groups.

java.util.concurrent package

Problems with traditional synchronized keyword

Let’s first see the problems with the traditional synchronized keyword, then we will look at enhancements

The problems with the traditional synchronized keyword are:

  1. Lack of Flexibility: There is no flexibility to attempt for locks without waiting.
  2. Absence of Time Constraints: There is no way to specify the maximum waiting time for a thread to acquire a lock. Threads may wait indefinitely for a lock, potentially leading to performance problems or deadlock situations.
  3. Lack of Control Over Lock Acquisition: When a thread releases a lock, there is no control over which waiting thread will acquire that lock next.
  4. No API for Listing Waiting Threads: There is no API available to list out all waiting threads for a lock.
  5. Limitation in Usage Scope: The synchronized keyword must be used either at the method level or within a method, and it’s not possible to use it across multiple methods.

To address these issues, the creators introduced java.util.concurrent.locks in version 1.5. This package provides several enhancements to programmers, offering more control over concurrency.

The Lock interface

Lock object is similar to the implicit lock acquired by a thread to execute a synchronized method or synchronized block. Lock implementations provide more extensive operations than traditional implicit locks.

Important methods of the Lock interface:

Java
void lock()

We can use this method to acquire a lock. If the lock is already available, then the current thread will immediately get that lock. If the lock is not available, then it will wait until obtaining the lock. It exhibits the same behavior as the traditional synchronized keyword.

Java
boolean tryLock()

Attempts to acquire the lock without waiting. If the lock is available, the thread acquires the lock and returns true. If the lock is not available, the method returns false, and the thread can continue its execution without waiting. In this case, the thread never enters a waiting state.

Java
if (lock.tryLock()) {
    // Perform safe operations
} else {
    // Perform alternative operations
}

boolean tryLock(long time, TimeUnit unit)

Java
boolean tryLock(long time, TimeUnit unit)

If the lock is available, the thread will get the lock and continue its execution. If the lock is not available, then the thread will wait until the specified amount of time. If the lock is still not available after the specified time, the thread can continue its execution. TimeUnit: An enum present in the java.util.concurrent package.

Java
if (lock.tryLock(1000, TimeUnit.MILLISECONDS)) {
    // Perform safe operations
}

void lockInterruptibly(): Acquires the lock if it is available and returns immediately. If the lock is not available, it will wait. While waiting, if the thread is interrupted, it won’t get the lock.

void unlock(): To call this method, the current thread should be the owner of the lock; otherwise, a runtime exception IllegalMonitorStateException will be thrown.

ReentrantLock

It is an implementation class of the Lock interface and is a direct child class of Object.

Reentrant means a thread can acquire the same lock multiple times without any issue. Internally, ReentrantLock increments the thread’s personal count whenever we call lock methods and decrements the count value whenever the thread calls the unlock method. The lock will be released whenever the count reaches zero.

Constructors:

Java
ReentrantLock l = new ReentrantLock();  

Creates an instance of ReentrantLock.

Java
ReentrantLock l = new ReentrantLock(boolean fairness);
  • Creates a ReentrantLock with the given fairness policy. If fairness is set to true, then the longest waiting thread will get the lock if it is available, following a first-come-first-serve policy. If fairness is set to false, the selection of the waiting thread is not guaranteed.

Which of the following declarations are equal?

  • ReentrantLock l = new ReentrantLock();
  • ReentrantLock l = new ReentrantLock(true);
  • ReentrantLock l = new ReentrantLock(false);

The first and third declarations are equal.

Important methods of ReentrantLock:

(Comes from the Lock interface)

  • void lock()
  • boolean tryLock()
  • boolean tryLock(long l, TimeUnit t)
  • void lockInterruptibly()
  • void unlock()

Extra methods:

  • int getHoldCount(): Returns the number of holds on this lock by the current thread.
  • boolean isHeldByCurrentThread(): Returns true if the lock is held by the current thread.
  • int getQueueLength(): Returns the number of threads waiting for the lock.
  • Collection getQueuedThreads(): Returns a collection of threads that are waiting to acquire the lock.
  • boolean hasQueuedThreads(): Returns true if any thread is ready to acquire the lock.
  • boolean isLocked(): Returns true if the lock is acquired by the same thread.
  • boolean isFair(): Returns true if the fairness policy is set to true.
  • Thread getOwner(): Returns the thread that acquired the lock.

Thread Pools (Executor Frameworks)

Creating a new thread for every job may lead to performance and memory problems. To overcome this, we should use a thread pool. A thread pool is a pool of already created threads ready to execute our jobs. Java 1.5 introduced the Executor Framework to implement thread pools.

We can create a thread pool as follows:

Java
ExecutorService service = Executors.newFixedThreadPool(3);

We can submit a Runnable job using the submit method, e.g., service.submit(job);.

Java
service.submit(job);.

We can shutdown the ExecutorService by using shutdown(), e.g., service.shutdown();.

Java
service.shutdown();.

Callable and Future

In the case of a Runnable job, the thread won’t return anything after completing its job. If a thread is required to return some result after execution, then we should use Callable. The Callable interface contains only one method, call(). If we submit a Callable object to the executor, then after completing the job, the thread returns an object of type Future. The Future object can be used to retrieve the result from the Callable job.

Runnable vs Callable

Runnable:

  1. If a thread is not required to return anything after completing a job, then we should use Runnable.
  2. The Runnable interface contains only one method, run().
  3. A Runnable job does not return anything; hence, the return type of run() is void.
  4. Within the run method, if there is any chance of a raised checked exception, we must handle it using try-catch because we can’t use the throws keyword with the run method.
  5. The Runnable interface is present in the java.lang package.
  6. Introduced in version 1.0.

Callable:

  1. If a thread is required to return something after completing a job, then we should use Callable.
  2. The Callable interface contains only one method, call().
  3. A Callable job is required to return something, and hence the return type of the call method is object.
  4. Inside the call method, if there is any chance of raising a checked exception, we are not required to handle it using try-catch because the call method already throws exceptions.
  5. The Callable interface is present in the java.util.concurrent package.
  6. Introduced in version 1.5.

ThreadLocal

The ThreadLocal class provides thread-local variables. The ThreadLocal class maintains values on a per-thread basis. Each ThreadLocal object maintains a separate value for each thread that accesses that object. Threads can access their local value, manipulate its value, and even remove its value. In every part of the code executed by a thread, we can access its variable.

For example, consider a servlet that invokes some business methods. We have a requirement to generate a unique transaction ID for every request, and we have to pass this transaction ID to the business methods. For this requirement, we can use ThreadLocal to maintain a separate transaction ID for every request, i.e., for every thread.

Once a thread enters into a dead state, all its local variables are by default eligible for garbage collection.

This allows for safe, efficient handling of thread-specific data without worrying about synchronization issues or interference from other threads. It ensures that each thread operates with its own set of data, isolated from other threads.

Conclusion

Multithreading enhancements are ongoing, driven by the need for faster, more responsive, and scalable software. These advancements improve concurrency, performance, debugging, and security, making multithreading a powerful and versatile tool for modern software development. As hardware and software continue to evolve, we can expect even more innovative and efficient multithreading techniques to emerge in the future.

inter-thread communication in java

Mastering Inter-Thread Communication in Java: A Comprehensive Guide Part 4

In multi-threaded programming, communication between threads is essential for coordinating their activities and sharing data. Inter-thread communication in Java refers to the mechanisms through which threads communicate with each other to synchronize their actions or exchange data. This communication enables threads to work cooperatively towards achieving a common goal. In this blog, we will explore the concepts of inter-thread communication in Java, including synchronization, shared memory, and the wait-notify mechanism.

Why Inter-Thread Communication?

Consider a scenario where multiple threads are executing concurrently and need to coordinate their tasks or exchange information. For instance, one thread may produce data, while another consumes it. In such cases, inter-thread communication becomes necessary to ensure the correct execution and synchronization of threads.

Java provides several mechanisms for inter-thread communication, including synchronized blocks, wait-notify, and locks. These mechanisms allow threads to coordinate their activities effectively and avoid issues such as race conditions and deadlock.

Inter-thread communication

Two threads can communicate with each other by using wait(), notify(), and notifyAll(). The thread that is expecting an update is responsible for calling wait(), after which the thread enters a waiting state. The thread responsible for performing the update should call notify() after completing the update, allowing the waiting thread to receive the notification and continue its execution with the updated items.

Why are wait(), notify(), and notifyAll() present in the Object class but not in the Thread class? wait(), notify(), and notifyAll() are present in the Object class rather than the Thread class because a thread can call these methods on any Java object.

To call wait(), notify(), and notifyAll() methods on any object, the thread must be the owner of that object, meaning it must hold the lock of that object. Therefore, these methods can only be called from within a synchronized area; otherwise, a IllegalMonitorStateException will be thrown.

If a thread calls wait() on any object, it immediately releases the lock of that particular object and enters a waiting state.

If a thread calls notify() on any object, it releases the lock of that object, but it may not happen immediately. Unlike wait(), notify(), and notifyAll(), there are no other methods where a thread releases a lock.

Hence, in the cases of yield(), join(), and sleep(), the thread does not release the lock, but it does release the lock in wait(), notify(), and notifyAll().

Prototypes of methods:

Java
public final void wait() throws InterruptedException
public final native void wait(long ms) throws InterruptedException 
public final void wait(long ms, int ns) throws InterruptedException 

public final native void notify()
public final native void notifyAll()

Note: Every wait() method throws InterruptedException, which is a checked exception. Therefore, whenever we use the wait() method, we must handle this InterruptedException, either with a try-catch block or by using the throws keyword; otherwise, a compile-time error will occur.

What is impact of these methods on thread lifecycle?

The wait(), notify(), and notifyAll() methods in Java have a significant impact on the lifecycle of threads. These methods are primarily used for inter-thread communication and synchronization. Let’s explore their impact on the lifecycle of threads:

wait()

  • Impact on Thread Lifecycle: When a thread calls the wait() method, it voluntarily gives up its hold on the object’s monitor (lock) and enters into the “waiting” state. This means that the thread is temporarily suspended and does not consume CPU resources until one of the following conditions occurs:
    • Another thread calls notify() or notifyAll() on the same object.
    • The specified timeout period elapses (if wait(long timeout) is used).
  • Transition in Thread Lifecycle: From the “waiting” state, a thread can transition back to the “runnable” state (ready to run) once it is notified or the timeout expires.
  • Example Use Case: Typically used to wait for a specific condition to be met or for another thread to complete its task before proceeding.

notify()

  • Impact on Thread Lifecycle: The notify() method is used to wake up a single thread that is waiting on the object’s monitor. If multiple threads are waiting, the choice of which thread to wake up is arbitrary and depends on the JVM’s implementation.
  • Transition in Thread Lifecycle: When notify() is called, the awakened thread transitions from the “waiting” state to the “runnable” state. However, it does not immediately acquire the object’s monitor. It competes with other threads to obtain the lock once it becomes available.
  • Example Use Case: Often used to signal that a condition has changed and other threads waiting on that condition can proceed.

notifyAll()

  • Impact on Thread Lifecycle: Similar to notify(), but notifyAll() wakes up all threads that are waiting on the object’s monitor. All awakened threads transition to the “runnable” state and compete for the object’s monitor.
  • Transition in Thread Lifecycle: Threads waiting on the object’s monitor are awakened and transitioned to the “runnable” state. They compete for the lock once it becomes available.
  • Example Use Case: Useful when multiple threads are waiting for a shared resource to become available or when a significant change occurs that affects multiple threads.

Example(Wait,Notify,NotifyAll)

Java
class ThreadA {
    public static void main(String[] args) throws Exception {
        ThreadB b = new ThreadB();
        b.start();
        synchronized (b) {
            System.out.println("Main thread calling wait method"); // -----------------------------> 1
            b.wait();
            System.out.println("Main thread got notification"); //-----------------------------> 4
            System.out.println(b.total); // -----------------------------> 5
        }
    }
}

class ThreadB extends Thread {
    int total = 0;

    public void run() {
        System.out.println("Child thread starts calculation"); // ----------------------------->2
        for (int i = 1; i <= 100; i++) {
            total = total + i;
        }
        System.out.println("Child thread giving notification"); // ----------------------------->3
        this.notify();
    }
}
Java
Main thread calling wait method
Child thread starts calculation 
Child thread giving notification 
Main thread got notification 
5050

Here,

  • The main thread starts and creates an instance of ThreadB and starts it.
  • Main thread enters a synchronized block with object b.
  • The main thread calls wait() on b, releasing the lock and going into a waiting state.
  • Meanwhile, ThreadB starts execution and calculates the sum.
  • After calculation, ThreadB calls notify(), waking up the waiting main thread.
  • The main thread resumes execution, prints “Main thread got notification”, and then prints the total sum calculated by ThreadB, which is 5050.

Producer-Consumer Problem

In the Producer-Consumer problem, the Producer thread is responsible for producing items to the queue, while the Consumer thread is responsible for consuming items from the queue. If the queue is empty, the Consumer thread calls the wait() method and enters a waiting state. After the Producer thread produces an item and adds it to the queue, it is responsible for calling notify(). This notification allows the waiting Consumer thread to resume execution and continue processing the updated items.

Java
// Producer thread is responsible for producing items to the queue
class ProducerThread {
    void produce() {
        synchronized(q) {
            // Produce items to the queue
            q.notify();
        }
    }
}

// Consumer thread is responsible for consuming items from the queue
class ConsumerThread {
    void consume() {
        synchronized(q) {
            if (q.isEmpty())
                q.wait(); // If the queue is empty, consumer thread enters waiting state
            else
                consumeItems(); // Consume items from the queue
        }
    }
}
  • In the Producer-Consumer Problem, there are two threads: ProducerThread and ConsumerThread.
  • ProducerThread is responsible for producing items and ConsumerThread is responsible for consuming items.
  • When ProducerThread produces an item, it notifies any waiting ConsumerThread by calling notify() after synchronizing on the queue object.
  • ConsumerThread, after synchronizing on the queue object, checks if the queue is empty.
  • If the queue is empty, ConsumerThread calls wait(), entering a waiting state until notified by the ProducerThread.
  • Once ProducerThread produces an item and notifies, the waiting ConsumerThread gets the notification and continues its execution, consuming the items from the queue.

This process ensures that the ProducerThread and ConsumerThread synchronize their actions properly to avoid issues such as producing when the queue is full or consuming when the queue is empty.

Difference between notify() and notifyAll()

The notify() method is used to provide a notification to only one waiting thread. If multiple threads are waiting, only one thread will be notified, and the remaining threads have to wait for further notifications. Which thread will be notified cannot be predicted, as it depends on the JVM.

On the other hand, the notifyAll() method is used to provide notification to all waiting threads of a particular object. Even if multiple threads are notified, execution will be performed one by one because threads require a lock, and only one lock is available.

Using notify() and notifyAll() allows for controlling the synchronization and communication between threads effectively, depending on the specific requirements of the application.

Understanding Lock Acquisition in Synchronized Blocks with wait()

To unserstand this concept better, let’s first see blow eamples and then we will discuss it further.

Java
Stack s1 = new Stack();
Stack s2 = new Stack();

synchronized(s1) {
    // ...
    s2.wait(); // This will result in an IllegalMonitorStateException
    // ...
}

We will encounter an IllegalMonitorStateException in the above case. This is because the wait() method is being called on object s2, but the synchronization block is held on object s1. Therefore, the thread does not have the lock for s2, leading to the illegal state exception.

However, in the second example:

Java
synchronized(s1) {
    // ...
    s1.wait(); // This is a valid usage
    // ...
}

The above example is perfectly valid. Here, the wait() method is called on the same object s1 on which the synchronization block is held. Hence, the thread has the lock for s1, ensuring that the call to wait() is valid.

Note: When calling the wait() method on an object, the thread requires the lock of that particular object. For instance, if wait() is called on s1, the thread acquires the lock of s1 but not s2.

Deadlocks

Deadlocks occur primarily due to the use of the synchronized keyword, hence special care must be taken when using it. There are no resolution techniques for deadlocks, but several prevention techniques are available.

Java
class A {
    public synchronized void d1(B b) {
        System.out.println("Thread 1 starts execution of d1() method");
        try {
            Thread.sleep(6000);
        } catch(InterruptedException e) {}
        System.out.println("Thread 1 trying to call B's last()");
        b.last();
    }

    public synchronized void last() {
        System.out.println("Inside A, this is last() method");
    } 
}

class B {
    public synchronized void d2(A a) {
        System.out.println("Thread 2 starts execution of d2() method");
        try {
            Thread.sleep(6000);
        } catch(InterruptedException e) {}
        System.out.println("Thread 2 trying to call A's last()");
        a.last();
    }

    public synchronized void last() {
        System.out.println("Inside B, this is last() method");
    }
}

class DeadLock1 extends Thread {
    A a = new A();
    B b = new B();

    public void m1() {
        this.start();
        a.d1(b);  // This line executed by main thread
    }

    public void run() {
        b.d2(a); // this line executed by child thread 
    }

    public static void main(String[] args) {
        DeadLock1 d = new DeadLock1();
        d.m1();
    }
}

Output-

Java
Thread 1 starts execution of d1() method
Thread 2 starts execution of d2() method
Thread 2 trying to call A's last()
Thread 1 trying to call B's last()

In the above program, if we remove any single synchronized keyword, then the program won’t enter into a deadlock. Therefore, the synchronized keyword is the only reason for the deadlock situation. Due to this, special care must be taken when using synchronized keywords.

Deadlock Versus Starvation

Deadlock occurs when threads are blocked forever because they are each waiting for a resource that the other thread holds. It results in a situation where no progress can be made.

On the other hand, starvation refers to a situation where a thread is unable to gain regular access to shared resources and is unable to make progress. However, the waiting of the thread in starvation eventually ends at certain points.

For example, if a low priority thread has to wait until completing all high priority threads, it will experience long waiting but will eventually get a chance to execute, which is a form of starvation.

Daemon Threads

Daemon threads are those executing in the background without interfering with the termination of the main application. Examples of daemon threads include the Garbage Collector, Signal Dispatcher, and Attach Listener.

The main objective of daemon threads is to provide support for non-daemon threads (such as the main thread). For instance, if the main thread is running with low memory, the JVM may run the garbage collector to reclaim memory from unused objects. This action improves the amount of free memory, allowing the main thread to continue its execution smoothly.

Usually, daemon threads have low priority, but depending on our requirements, they can run with high priority as well.

We can check if a thread is a daemon thread by using the isDaemon() method of the Thread class:

Java
public boolean isDaemon()

Similarly, we can change the daemon nature of a thread using the setDaemon() method of the Thread class, but this change is only possible before starting the thread. If we attempt to change the daemon nature after starting the thread, we will encounter an IllegalThreadStateException.

Default Nature of Thread

The default nature of thread is such that the main thread is always non-daemon, while for all other threads, the daemon nature is inherited from their parent thread. This means that if the parent thread is a daemon, then the child thread will also be a daemon, and if the parent thread is non-daemon, then the child thread will also be non-daemon.

It is impossible to change the daemon nature of the main thread because it is already started by the JVM at the beginning.

For example:

Java
class MyThread extends Thread {
}

class Test {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().isDaemon()); // false
        // Thread.currentThread().setDaemon(true);  // This will result in IllegalThreadStateException
        MyThread t = new MyThread();
        System.out.println(t.isDaemon()); // false
        t.setDaemon(true);
        System.out.println(t.isDaemon()); // true
    }
}

When the last non-daemon thread terminates, all daemon threads are automatically terminated regardless of their position.

For example:

Java
class MyThread extends Thread {
    public void run() {
        for(int i = 0; i < 10; i++) {
            System.out.println("Child thread");
            try {
                Thread.sleep(2000);
            } catch(InterruptedException e) {}
        }
    }
}

class DaemonThreadDemo {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.setDaemon(true); // This makes the child thread a daemon
        t.start();
        System.out.println("End of main thread");
    }
}

If we comment out line 1 (t.setDaemon(true);), then both the main and child threads are non-daemon, and both threads will execute until their completion. However, if line 1 is not commented, the main thread is non-daemon, and the child thread is daemon. In this case, when the main thread terminates, the child thread will also be terminated. The possible outputs in this case are:

Java
//output 1

End of main thread
Child Thread


//output 2

End of main thread


//output 3

Child Thread
End of main thread
```"

Multithreading Models

Java multithreading concepts are implemented using the following two models:

  1. Green Thread Model: Threads managed entirely by the JVM without relying on underlying OS support are called green threads. Very few operating systems, such as SUN Solaris, provide support for the green thread model. However, the green thread model is deprecated and not recommended for use.
  2. Native OS Model: Threads managed by the JVM with the assistance of the underlying operating system are referred to as the native OS model. All Windows-based operating systems provide support for the native OS model.

How to stop a thread?

If the stop() method is called, the thread immediately enters the dead state. However, the stop() method is deprecated and not recommended for use.

How to suspend and resume a thread?

The suspend() and resume() methods are used to suspend and resume threads, respectively. However, these methods are deprecated and not recommended for use.

When the suspend() method is called, the thread immediately enters the suspended state. A suspended thread can be resumed using the resume() method of the Thread class, allowing the suspended thread to continue its execution.

Part 5:

Conclusion

Inter-thread communication is essential in Java multi-threaded programming for coordinating the activities of concurrent threads. By using synchronization and mechanisms like the wait-notify protocol, threads can communicate effectively and synchronize their actions to avoid issues such as race conditions and deadlock. Understanding these concepts is crucial for writing thread-safe and efficient concurrent programs in Java.

synchronization in java multithreading

Mastering Synchronization in Java Threads: A Comprehensive Guide Part 3

In the realm of concurrent programming, synchronization plays a crucial role in ensuring thread safety and preventing race conditions. In Java, where multithreading is a fundamental feature, understanding synchronization mechanisms is essential for writing robust and efficient concurrent applications. In this comprehensive guide, we will delve into the intricacies of synchronization in Java threads, exploring its concepts, techniques, and best practices.

Synchronization in Java

Synchronization in Java is a critical concept for managing concurrent access to shared resources, preventing data inconsistency issues that can arise when multiple threads operate on the same data simultaneously.

“Synchronized”

The ‘synchronized’ keyword is the modifier applicable only to methods and blocks, but not to classes and variables. If multiple threads are trying to operate simultaneously, there may be a chance of a data inconsistency problem. To overcome this problem, we should use the “synchronized” keyword. If a method or block is declared as synchronized, then at any given time, only one thread is allowed to execute that method or block on the given object, resolving the data inconsistency problem.

The main advantage of the “synchronized” keyword is that we can resolve data inconsistency problems, but the main disadvantage is that it increases the waiting time of threads and creates performance problems. Hence, if there is no specific requirement, it is not recommended to use the “synchronized” keyword.

If a thread wants to execute a synchronized method on the given object, it first has to obtain the lock of that object. Once the thread has obtained the lock, it is allowed to execute any synchronized method on that object. Once the method execution is complete, the thread automatically releases the lock. Acquiring and releasing the lock are internally taken care of by the JVM, and the programmer is not responsible for this activity.

While a thread is executing a synchronized method on a given object, the remaining threads are not allowed to execute any synchronized methods simultaneously on the same object, but they are allowed to execute non-synchronized methods simultaneously.

Java
class X {
    synchronized m1() {
    }
    
    synchronized m2() {
    }
    
    m3() {
    }
}

If thread t1 comes to execute m1() on object x, then t1 only acquires the lock of object x and starts executing m1(). Now, if t2 comes to execute m1() or t3 comes to execute m2(), then in both situations, a waiting state will occur. However, if t4 comes to execute m3(), it will get a chance immediately.

The lock concept is implemented based on the object, not on the method. So, every Java object has two areas:

  1. Non-synchronized area: This area can be accessed by any number of threads simultaneously, for example, wherever the object’s state won’t be changed, like a read() operation.
  2. Synchronized area: This area can be accessed by only one thread at a time, for example, wherever we are performing update operations (Add/Remove/Delete/Replace), i.e., where the state of the object is changing.

Real-time Example:

Java
class ReservationSystem {
    CheckAvailability() {           // Non-synchronized 
        // Just read operation
    }
    
    synchronized bookTicket() {       //Synchronized 
        // Update
    }
}

Synchronized Demo

Java
class Display {
  public synchronized void wish(String name) { 
    for(int i=0; i<10; i++) {
      System.out.print("Good Morning:");
      try {
         Thread.sleep(2000);
      } catch(InterruptedException e) {
      }
      System.out.println(name);
    }
  }
}

class MyThread extends Thread {
 Display d;
 String name;
 MyThread(Display d, String name) {
   this.d = d;
   this.name =  name;
 }
 public void run() {
   d.wish(name);
 }
}

class SynchronizedDemo {
 public static void main(String[] args) {
   Display d = new Display();
   MyThread t1 = new MyThread(d, "Krushna");
   MyThread t2 = new MyThread(d, "Arjuna");
   t1.start();
   t2.start();
 }
}

Here, (d object).wish(“Krushna”) by t1 | .wish(“Arjuna”) by t2

In the SynchronizedDemo class, two threads t1 and t2 are created, both sharing the same Display object d. The wish() method of the Display class is synchronized, ensuring that only one thread can execute it at a time. This prevents data inconsistency issues.

Suppose,

If we do not declare the wish() method as synchronized, then both threads will be executed simultaneously, resulting in irregular output like below:

Java
Good Morning:Good Morning:Arjuna
Good Morning:Krushna
Good Morning:Arjuna
Good Morning:Krushna
Good Morning:Good Morning:Arjuna
Good Morning:Krushna
Good Morning:Arjuna
Good Morning:Krushna
Good Morning:Good Morning:Arjuna
Good Morning:Krushna
Good Morning:Arjuna
Good Morning:Krushna

But, if we declare wish() as synchronized, then at a time only one thread is allowed to execute wish() on the given Display object, ensuring regular output.

Java
Good Morning:Krushna
Good Morning:Krushna
Good Morning:Krushna
Good Morning:Krushna
Good Morning:Krushna
Good Morning:Krushna
Good Morning:Krushna
Good Morning:Krushna
Good Morning:Krushna
Good Morning:Krushna
Good Morning:Arjuna
Good Morning:Arjuna
Good Morning:Arjuna
Good Morning:Arjuna
Good Morning:Arjuna
Good Morning:Arjuna
Good Morning:Arjuna
Good Morning:Arjuna
Good Morning:Arjuna
Good Morning:Arjuna

As expected, each thread prints its name (“Krushna” for t1 and “Arjuna” for t2) ten times alternately, resulting in regular output due to the synchronization of the wish() method.

Case Study

Java
Display d1 = new Display();
Display d2 = new Display();

MyThread t1 = new MyThread(d1, "Krushna");
MyThread t2 = new MyThread(d2, "Arjuna");

t1.start();
t2.start();

In this case study, eventhough the wish() method is synchronized, we will get irregular output because threads are operating on different Java objects.

Conclusion: If multiple threads are operating on the same Java object, then synchronization is required. If multiple threads are operating on multiple Java objects, then synchronization is not required.

Every class in Java has a unique lock, which is known as the class-level lock. If a thread wants to execute a static synchronized method, it requires the class-level lock. Once the thread acquires the class-level lock, it is allowed to execute any static synchronized method of that class. Once the method execution completes, the thread automatically releases the lock.

While a thread is executing a static synchronized method, the remaining threads are not allowed to execute any static synchronized method of that class simultaneously. However, the remaining threads are allowed to execute the following methods simultaneously:

  • static m3() (normal static method)
  • synchronized m4() (normal synchronized instance method)
  • m5() (normal instance methods)
Java
class X {
    static synchronized m1() {
    }
    
    static synchronized m2() {
    }
    
    static m3() {
    }
    
    synchronized m4() {
    }
    
    m5() {
    }
}
Java
(object X).m1()

If thread t1 comes to execute m1(), then it acquires the class-level lock: t1 --> CL(X)

If t2 comes to execute m1(), it will go into a waiting state.

If t3 comes to execute m2(), it will go into a waiting state.

If t4 comes to execute m3(), it will execute it.

If t5 comes to execute m4(), it will execute it.

If t6 comes to execute m5(), it will execute it.

Synchronized Block

If only a few lines of code require synchronization, it is not recommended to declare the entire method as synchronized. Instead, we can enclose those few lines of code using a synchronized block.

The main advantage of a synchronized block over a synchronized method is that it reduces the waiting time of threads and improves the performance of the system.

We can declare synchronized blocks as follows:

To get the lock of the current object:

Java
synchronized(this) {
    // If a thread gets the lock of the current object, then only it is allowed to execute this area
}

To get the lock of a particular object ‘b’:

Java
synchronized(b) {
    // If a thread gets the lock of the particular object 'b', then only it is allowed to execute this area
}

To get the class-level lock:

Java
synchronized(Display.class) {
    // If a thread gets the class-level lock of the "Display" class, then only it is allowed to execute this area
}

Using synchronized blocks allows for finer-grained control over synchronization, focusing only on the critical sections of code that require it, thus improving the overall performance of the system.

Example

Java
class Display {
    public void wish(String name) {
        ;;;;;;;;;;;;;;;;;;;;;;;;;;;; // 1 lakh lines of code

        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                System.out.println("Good Morning:");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                }
                System.out.println(name);
            }
        }
        ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; // 1 lakh lines of code 
    }
}

class MyThread extends Thread {
    Display d;
    String name;

    MyThread(Display d, String name) {
        this.d = d;
        this.name = name;
    }

    public void run() {
        d.wish(name);
    }
}

class SynchronizedDemo {
    public static void main(String[] args) {
        Display d = new Display();
        MyThread t1 = new MyThread(d, "Krushna");
        MyThread t2 = new MyThread(d, "Arjuna");
        t1.start();
        t2.start();
    }
}

Let’s see code section wise.

In the Display class, the wish() method is defined with extensive code execution before and after the synchronized block. Inside the synchronized block, a loop prints “Good Morning” along with the provided name, ensuring thread-safe execution of this critical section of code.

In the MyThread class, threads are created to execute the wish() method of the Display object with provided names.

In the SynchronizedDemo class, Display object d is created, and two threads (t1 and t2) are instantiated to execute the wish() method on d with different names concurrently.

Lock Concept

Lock concept is applicable for object type and class type, but not for primitives. Hence, we can’t pass a primitive type as an argument to a synchronized block; otherwise, we will get a compilation error: “unexpected type found: int required: reference.”

Can a thread acquire multiple locks simultaneously?

Yes, of course, from different objects. For example:

Java
class X {
    public synchronized void m1() {
        // Here, the thread has the lock of the 'X' object

        Y y = new Y();

        synchronized (y) {
            // Here, the thread has locks of x and y

            Z z = new Z();
            synchronized (z) {
                // Here, the thread has locks of x, y, and z
            }
        }
    }
}

X x = new X();
x.m1();

In this example, while executing method m1() of object x of class X, the thread acquires locks from objects of classes X, Y, and Z simultaneously.

Synchronized Statement

A synchronized statement refers to the set of statements enclosed within a synchronized method or synchronized block. These statements are termed synchronized statements because they are executed under the protection of synchronization, ensuring thread-safe access to critical sections of code.

In a synchronized method, the entire method body is considered a synchronized statement, as all statements within it are executed atomically, allowing only one thread to execute the method at any given time.

Similarly, in a synchronized block, the statements enclosed within the block are synchronized statements. These statements are executed under the lock associated with the specified object or class, ensuring mutual exclusion among threads attempting to access the synchronized block concurrently.

Overall, synchronized statements play a crucial role in achieving thread safety by ensuring that critical sections of code are executed in a mutually exclusive manner, thereby preventing data races and maintaining program correctness.

Part 4: Mastering Inter-Thread Communication in Java

Conclusion

Synchronization is a fundamental aspect of Java concurrency, enabling safe and efficient coordination among multiple threads. By mastering synchronization techniques and understanding the underlying principles, developers can build robust and scalable concurrent applications. With this comprehensive guide, you’re equipped to navigate the complexities of synchronization in Java threads and harness the power of concurrent programming effectively.

multithreading in java

Mastering Multithreading in Java: A Comprehensive Guide Part 1

In the realm of software development, multithreading emerges as a powerful tool for building applications that can execute multiple tasks seemingly concurrently, enhancing responsiveness and performance. Java, a widely used language, offers robust support for multithreading through its Thread class and Runnable interface. Multithreading in Java is a powerful feature that allows concurrent execution of multiple threads within the same process. It’s crucial for building scalable, responsive, and efficient applications, especially in today’s world where multi-core processors are prevalent. In this comprehensive guide, we will delve into the depths of Java multithreading, covering everything from the basics to advanced concepts.

What is Multithreading in Java?

Before understanding multithreading, we first need to understand multitasking. Let’s see what it is.

Multitasking: Executing several tasks simultaneously is the concept of multitasking. There are two types of multitasking:

  1. Process-based: Executing several tasks simultaneously, where each task is a separate independent program (Process), is called process-based multitasking. For example, while typing a Java program in an editor, we can listen to audio songs from the same system, simultaneously download a file from the internet. All these tasks will be executed simultaneously and independently of each other; hence, it is process-based multitasking. Process-based multitasking is best suited at the OS level.
  2. Thread-based: Executing several tasks simultaneously, where each task is a separate independent part of the same program, is called thread-based multitasking. Each independent part is called a thread. Thread-based multitasking is best suited at the program level. For example, developing multimedia graphics, animation, video games, web server, and application server, etc.

Whether it is process-based or thread-based, the main objective of multitasking is to reduce the response time of the system and improve performance.

Now, What is Multithreading?

Multithreading refers to the concurrent execution of multiple threads within a single process. Each thread represents an independent flow of control, allowing programs to perform multiple tasks simultaneously.

BTW, What is Concurrency: The ability of tasks to appear to execute simultaneously, even on single-core processors.

Why Multithreading?

Multithreading enables applications to utilize the available CPU resources efficiently, especially on multi-core processors. It improves responsiveness by allowing tasks to run in the background while the main application thread remains responsive.

Threads vs. Processes

A thread is a lightweight process, and multiple threads can exist within a single process. Threads share the same memory space, making communication between them more efficient compared to processes, which have separate memory spaces.

Defining a Thread

We can define a thread by:

  1. By extending the Thread class
  2. By implementing the Runnable Interface

Let’s take a closer look at each one.

By Extending the Thread class

To define a thread by extending the Thread class, a new class can be created that extends the Thread class. Here’s an example:

Java
class MyThread extends Thread   // This way we can define a thread by extending the Thread class 
{
   public void run()
   {
     for(int i=0; i<10; i++)   // Inside the run method, whatever is there is the job of the thread. Here, this for loop is the job of this child thread and it is executed independently.
     {
       System.out.println("child thread");
     }
   }
}            

In this method, the class MyThread extends the Thread class, enabling it to define a thread. The run() method within this class signifies the task to be executed by the thread. In this example, the loop within the run() method represents the specific job to be performed by the child thread.

Thread Execution and Thread Scheduler

Let’s see below example first.

Java
class ThreadDemo {
  public static void main(String[] args) {
    // At this point, only one thread is executing, and that is the main thread.
  
    MyThread t = new MyThread();  // Thread instantiation
    t.start();   // Starting the thread
   
    // At this point, two threads are executing simultaneously: one is the main thread, and the other is the child thread (t - MyThread).
    for(int i=0; i<10; i++)   // This for loop is executed by the main thread and not by its child thread.
    {
      System.out.println("main thread");
    }
  }
}

The behavior of thread execution is influenced by the thread scheduler, a component of the JVM responsible for managing and scheduling threads.

Thread Scheduler

It is a part of JVM and is responsible for scheduling threads. If multiple threads are waiting to get a chance for execution, the order in which threads will be executed is decided by the thread scheduler. We can’t expect the exact algorithm followed by the thread scheduler as it varies from JVM to JVM. Hence, we can’t expect the thread execution order and exact output. Whenever the situation involves multithreading, there is no guarantee of exact output, but we can provide several possible outputs. The following are various possible outputs for the above program:

Possible Outputs:

Java
Output 1:

Main thread
Main thread
...
Child thread
Child thread
...
Java
Output 2:

Child Thread
Child Thread
...
Main Thread
Main Thread
...
Java
Output 3:

Main Thread
Child Thread
Main Thread
Child Thread
...
Java
Output 4:

Child Thread
Main Thread
Child Thread
...

Due to the unpredictable nature of thread scheduling, any of these scenarios (or variations thereof) could occur during program execution. It’s important to note that while we cannot guarantee a specific output, understanding the behavior of thread scheduling enables us to anticipate various possible outcomes in multithreading scenarios.

Difference between t.start() and t.run()

In the case of t.start(), a new thread will be created which is responsible for the execution of the run method. However, in the case of t.run(), a new thread won’t be created, and the run method will be executed just like a normal method call by the main thread. Hence, in the above program, if we replace t.start() with t.run(), then the output is:

Java
Child Thread 
Child Thread 
.
.
.
Main Thread 
Main Thread 
Main Thread

This total output is produced only by the main thread.

Importance of Thread class start method

The start method of the Thread class is responsible for registering the thread with the thread scheduler and performing all other mandatory activities. Without executing the start method of the Thread class, there is no chance of starting a new thread in Java. Due to this, the start method of the Thread class is considered the heart of multithreading.

Java
start()
{
  1. Register this thread with the Thread Scheduler
  2. Perform all other mandatory activities
  3. Invoke run()
}

Each step in the start() method plays a critical role in setting up and launching a new thread, making it indispensable for effective multithreading in Java.

Overloading of the run method

Overloading of the run method is always possible, but the start method of the Thread class will always invoke the no-argument run method. The other overloaded methods must be called explicitly like a normal method call.

For instance:

Java
class MyThread extends Thread {
    public void run() {
        System.out.println("no arg run");
    }
 
    public void run(int i) {
        System.out.println("int arg run");
    }
}

class ThreadDemo {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start(); // This will invoke the no argument run method implicitly.
    }
}



Output: no arg run

Here, although the MyThread class has an overloaded run method that accepts an integer argument, it won’t be invoked implicitly when the thread starts. To execute the overloaded run method with an integer argument, it must be called explicitly like any other method.

Without overriding the run method

If we are not overriding the run method, then the Thread class’s run method will be executed, which has an empty implementation. Hence, we won’t get any output.

Means, If the run method is not overridden in a subclass of Thread, then the default run method provided by the Thread class will be executed. This default run method has an empty implementation, resulting in no output.

Java
class MyThread extends Thread {
    // No run method overridden
}

class Test {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
    }
}


//No output

Note: It is highly recommended to override the run method; otherwise, don’t proceed with the multithreading concept.

Overriding start method

If we override the start method, then our overridden start method will be executed just like a normal method call, and a new thread won’t be created.

Java
class MyThread extends Thread {
  public void start() {
    System.out.println("start method");
  }
  
  public void run() {
    System.out.println("run method");
  }
}

class Test {
  public static void main(String[] args) {
    MyThread t = new MyThread();
    t.start();
    System.out.println("main method");
  }
}


////////////////////////////////

output

start method
main method


// Note: This output is produced only by the main thread.

Here, the output is produced solely by the main thread since no new thread is created. The overridden start method in the MyThread class behaves like a regular method call, printing “start method” before executing the main method. It’s important to note that overriding the start method is not recommended in general, especially when dealing with multithreading concepts.

Note: It is not recommended to override the start method; otherwise, don’t proceed with the multithreading concept.

If we made one small change in the above program, then the output will change as follows:
Java
class MyThread extends Thread {
  public void start() {
    super.start();           // due to this line two threads run simultaneously that is child and main thread so o/p also vary
    System.out.println("Start method");
  }

  public void run() {
    System.out.println("run method");
  }
}


class Test {
  public static void main(String[] args) {
    MyThread t = new MyThread();
    t.start();
    System.out.println("main Thread");
  }
}

Here, due to the invocation of super.start() within the overridden start method of the MyThread class, two threads, the child thread, and the main thread, run simultaneously, thereby causing variations in the output. Here are the possible outputs:

Output 1

Java
run method 
Start method 
main Thread

In this case, the child thread executes its run method first, followed by the completion of the start method in the child thread, and then the main thread executes its main method.

Output 2

Java
Start method 
main Thread
run method

Here, the start method in the child thread executes first, followed by the completion of the main method in the main thread, and then the run method in the child thread executes.

Output 3

Java
Start method
run method
main Thread 

In this case, the start method in the child thread executes first, followed by the execution of the run method in the child thread, and finally, the main method in the main thread executes.

Thread Lifecycle

Threads in Java go through various states during their lifecycle:

  • New: When a thread is created but not yet started.
  • Runnable: When a thread is ready to run but waiting for CPU time.
  • Blocked: When a thread is waiting for a monitor lock to enter a synchronized block or waiting on I/O operations.
  • Waiting: When a thread is waiting indefinitely for another thread to perform a particular action.
  • Timed Waiting: Similar to waiting, but with a specified timeout period.
  • Terminated: When a thread completes its execution or is terminated prematurely.

The life cycle of a thread in Java can be outlined as follows:

Java
[New/Born] -------- t.start() --------> [Ready/Runnable] 

                                            -------- If Thread Scheduler allocates processor -------->
                                
[Running] -------- If run() method completes --------> [Dead]
  1. New/Born: The thread is created but not yet started. For instance, when you create a new instance of a thread class using MyThread t = new MyThread(), the thread is in the “New” or “Born” state.
  2. Ready/Runnable: After invoking the start() method on the thread object (t.start()), the thread becomes eligible to run, but it’s up to the thread scheduler to allocate processor time. The thread is considered “Ready” or “Runnable” at this stage.
  3. Running: When the thread scheduler allocates processor time to the thread, it enters the “Running” state, and its run() method starts executing. The thread remains in this state until its run() method completes or is paused by the scheduler to allow other threads to run.
  4. Dead: Once the run() method completes execution or the thread is explicitly terminated, the thread enters the “Dead” state. A thread is considered “Dead” when its execution is finished, and it cannot be started again.

What happens if we try to restart an already started thread?

After starting a thread, if we try to restart the same thread once again, then we will get IllegalThreadStateException.

Java
Thread t = new Thread();
t.start();
// Some other code...
t.start(); // This will throw IllegalThreadStateException
  1. You create a new thread object t.
  2. You start the thread using t.start(), which transitions the thread from the new state to the runnable state and invokes its run() method.
  3. If you attempt to start the same thread again using t.start(), you’ll encounter the IllegalThreadStateException because the thread is already in the runnable or running state and cannot be restarted.

Note: Attempting to restart a thread that has already been started violates the threading rules in Java, hence the exception. This exception occurs because a thread cannot be restarted once it has already started running or has been terminated.

Defining a Thread by Implementing the Runnable Interface (I)

When defining a thread by implementing the Runnable interface, you create a class that implements the run() method defined in the Runnable interface. The Runnable interface is present in the java.lang package and it contains only one method: public void run(). Then, you pass an instance of this class to the Thread constructor, and invoke the start() method on the Thread object. This approach allows for better flexibility in Java’s multithreading model.

Java
class MyRunnable implements Runnable { // defining a thread using the second approach, which is implementing the Runnable interface
   public void run() {
     for(int i=0; i<10; i++) { // whatever code inside the run method is the job of the thread and it is executed by the child thread  
       System.out.println("child thread");
     }
   }
}

class ThreadDemo {
  public static void main(String[] args) {
    MyRunnable r = new MyRunnable();
    Thread t = new Thread(r); // r is the target runnable
    t.start();
    
    // At this point, two threads started executing simultaneously: the child and main threads 
    for(int i=0; i<10; i++) { // executed by the main thread 
      System.out.println("main thread");
    }
  }
}

We will get mixed output, and we can’t determine the exact output.

Output

Java
main thread
main thread
child thread
child thread
main thread
main thread
child thread
child thread
main thread
main thread
child thread
child thread
main thread
child thread
main thread
child thread
main thread
child thread
main thread
child thread
  • Here, we defined a class MyRunnable that implements the Runnable interface and overrides its run() method to define the task of the thread.
  • You create an instance of MyRunnable.
  • You create a Thread object t and pass the MyRunnable instance to its constructor.
  • You start the thread using t.start(), which initiates the execution of the thread’s run() method concurrently with the main thread’s execution.
  • Both the main thread and the child thread execute simultaneously, leading to a mixed output.

Due to the concurrent execution of the main thread and the child thread, the output may vary, resulting in a mixed sequence of messages from both threads.

Case Study

Let’s dive into the different outcomes depending on the situation. consider following example:

Java
MyRunnable r = new MyRunnable();
Thread t1 = new Thread();
Thread t2 = new Thread(r);

Case 1:

Java
t1.start(); // A new thread will be created responsible for the Thread class's run method, which has empty implementation.

In this case , a new thread will be created, but since t1 is not associated with any Runnable object, the new thread will execute the run() method of the Thread class, which has an empty implementation.

Case 2:

Java
t1.run(); // No new thread will be created, and the Thread class's run method will be executed like a normal method call.

Here, No new thread will be created, and the run() method of the Thread class will be executed just like a normal method call.

Case 3:

Java
t2.start(); // A new thread will be created responsible for the execution of the MyRunnable class's run method.

A new thread will be created, and it will be responsible for executing the run() method of the MyRunnable class, as t2 is associated with the MyRunnable instance r.

Case 4:

Java
t2.run(); // No new thread will be created, and the MyRunnable class's run method will be executed like a normal method call.

No new thread will be created, and the run() method of the MyRunnable class will be executed just like a normal method call.

Case 5:

Java
r.start(); // Compile-time error: MyRunnable class doesn't have start capability (CE: cannot find symbol | symbol: method start() | location: class MyRunnable)

This will result in a compile-time error because the MyRunnable class does not have a start() method. It’s the Thread class that has the start() method.

Case 6:

Java
r.run(); // No new thread will be created, and the MyRunnable run method will be executed like a normal method call.

No new thread will be created, and the run() method of the MyRunnable class will be executed like a normal method call.

Which approach is best to define a thread?

Among the two ways of defining a thread, the “implements Runnable” approach is recommended.

In the first approach, our class always extends the Thread class, leaving no chance of extending any other class. Hence, we are missing inheritance benefits.

However, in the second approach, by implementing the Runnable interface, we can extend any other class. Therefore, we won’t miss any inheritance benefits.

Because of the above reasons, implementing the Runnable approach is recommended over extending the Thread class.

Another Way of Defining a Thread (Not Recommended)

Here’s another way of defining a thread in Java, valid but not recommended. Let’s see why.

Java
class MyThread extends Thread {
   public void run() {
     System.out.println("Child Thread");
   }
}

class ThreadDemo {
  public static void main(String[] args) {
    MyThread t = new MyThread();
    Thread t1 = new Thread(t);
    t1.start();
    System.out.println("main thread");
  }
}

Output (One of the possible outputs):

Java
Child Thread
main Thread

Or

Java
main Thread
Child Thread

Eample given above is valid but not recommended approach due to several reasons, including inflexibility and violation of good object-oriented design principles.

However, is it valid to pass a reference to another thread to the Thread constructor like this (new Thread(t))?

Java
                       +-------------------+
                       |     java.lang     |
                       |      Object       |
                       +---------+---------+
                                 ^
                                 |
               +-----------------+-----------------+
               |                                   |
   +-----------+-----------+           +-----------+-----------+
   |                       |           |                       |
+--+-------------------+   |       +---+-------------------+   |
|     java.lang      |   |       |     java.lang      |   |
|      Thread       |   |       |      Runnable     |   |
+-----------+-------+   |       +-------------------+   |
            ^           |                   ^           |
            |           |                   |           |
            |           |                   |           |
+-----------+-----------+-----------+       |       +---+-------------------+
|                                   |       |       |                       |
|           MyThread               |<------+       |      YourThread      |
|                                   |               |                       |
+-----------------------------------+               +-----------------------+

Here,

  • java.lang.Thread is shown as extending java.lang.Object and implementing the Runnable interface.
  • java.lang.Runnable is an interface implemented by the Thread class, represented by the dashed line between them.
  • Custom classes like MyThread can extend Thread and, therefore, indirectly implement the Runnable interface.

So, passing either Runnable interface or implementing class reference is acceptable and completely valid in Java, but this hybrid approach is not recommended.

Part 2: Mastering Thread Constructors, Thread Priority, yield(), join(), and sleep() Methods for Concurrent Efficiency

Conclusion

In this comprehensive guide, we’ve covered various aspects of Java multithreading, from the basics of creating and synchronizing threads to advanced concepts like thread safety, concurrency issues, and performance tuning. By mastering multithreading in Java and following best practices, you can develop efficient, scalable, and responsive applications that leverage the full power of modern multicore processors. Keep experimenting, learning, and refining your multithreading skills to build robust and high-performance software.

Thread Constructors in java

Mastering Thread Constructors, Thread Priority, yield(), join(), and sleep() Methods for Concurrent Efficiency: Java Multithreading A Comprehensive Guide Part 2

Threads are the lifeblood of concurrent programming in Java, enabling multiple tasks to execute seemingly simultaneously, enhancing the performance of applications by utilizing the available processing power efficiently. Understanding thread constructors and methods is crucial for effectively creating, managing, and synchronizing threads in your applications. In this blog post, we’ll delve into the various constructors and methods provided by the Thread class in Java. Specially three fundamental methods: yield(), join(), and sleep(). Each of these methods serves a specific purpose in controlling thread execution, offering developers flexibility and control over concurrent processes.

Thread Constructors

Java
Thread t = new Thread();
Thread t = new Thread(Runnable r);
Thread t = new Thread(String name);
Thread t = new Thread(Runnable r, String name);
Thread t = new Thread(ThreadGroup g, String name);
Thread t = new Thread(ThreadGroup g, Runnable r);
Thread t = new Thread(ThreadGroup g, Runnable r, String name);
Thread t = new Thread(ThreadGroup g, Runnable r, String name, long stackSize);

let’s break down each constructor of the Thread class:

  1. Thread t = new Thread();: This constructor creates a new thread object t but does not specify the task (runnable) for the thread to execute. In this case, the thread is considered to be in a “new” state, and calling t.start() will cause the thread to execute the run() method of the Thread class, which has an empty implementation.
  2. Thread t = new Thread(Runnable r);: This constructor creates a new thread object t and associates it with the specified Runnable object r. When t.start() is called, the run() method of the Runnable object r will be executed by the new thread.
  3. Thread t = new Thread(String name);: This constructor creates a new thread object t with the specified name. The thread is considered to be in a “new” state, and calling t.start() will cause the thread to execute the run() method of the Thread class, which has an empty implementation.
  4. Thread t = new Thread(Runnable r, String name);: This constructor creates a new thread object t associated with the specified Runnable object r and with the specified name.
  5. Thread t = new Thread(ThreadGroup g, String name);: This constructor creates a new thread object t within the specified ThreadGroup g and with the specified name.
  6. Thread t = new Thread(ThreadGroup g, Runnable r);: This constructor creates a new thread object t within the specified ThreadGroup g and associates it with the specified Runnable object r.
  7. Thread t = new Thread(ThreadGroup g, Runnable r, String name);: This constructor creates a new thread object t within the specified ThreadGroup g, associates it with the specified Runnable object r, and gives it the specified name.
  8. Thread t = new Thread(ThreadGroup g, Runnable r, String name, long stackSize);: This constructor creates a new thread object t within the specified ThreadGroup g, associates it with the specified Runnable object r, gives it the specified name, and specifies the size of the thread’s stack.

Getting and Setting the Name of a Thread

Every thread in Java has a name, which may be a default name generated by the JVM or a customized name provided by the programmer. We can get and set the name of a thread using the following two methods of the Thread class:

  • public final String getName(): This method returns the name of the thread as a String.
  • public final void setName(String name): This method sets the name of the thread to the specified name.
Java
class MyThread extends Thread {
}

class Test {
  public static void main(String[] args) {
    System.out.println(Thread.currentThread().getName());  // main
    MyThread t = new MyThread();
    System.out.println(t.getName()); // Thread-0
    Thread.currentThread().setName("softAai Apps");
    System.out.println(Thread.currentThread().getName()); // softAai Apps
  }
}

Here,

  • Thread.currentThread().getName() returns the name of the current thread, which is “main” by default.
  • t.getName() returns the name of the thread t, which is “Thread-0” by default since it’s created without a specific name.
  • Thread.currentThread().setName("softAai Apps") sets the name of the current thread to “softAai Apps”.
  • Thread.currentThread().getName() returns the updated name of the current thread, which is now “softAai Apps”.

These methods provide a way to manage and identify threads in Java programs.

Getting the Current Executing Thread Object

We can get the current executing thread object by using Thread.currentThread().

Java
class MyThread extends Thread {
  public void run() {
    System.out.println("run method executed by Thread: " + Thread.currentThread().getName());
  }
}

class Test {
  public static void main(String[] args) {
    MyThread t = new MyThread();
    t.start()
    System.out.println("main method executed by Thread: " + Thread.currentThread().getName());
  }
}

Output:

Java
main method executed by Thread: main
run method executed by Thread: Thread-0
  • In the main() method, Thread.currentThread().getName() returns the name of the current thread, which is “main”. This is because the main() method is executed by the main thread.
  • In the run() method of MyThread, Thread.currentThread().getName() returns the name of the current thread, which is “Thread-0”. This is because the run() method is executed by the thread represented by the MyThread object t, which has the default name “Thread-0”.

Using Thread.currentThread() allows you to obtain information about the thread currently executing a particular block of code, which can be useful for debugging and logging purposes.

Thread Priority

Every thread in Java has some priority, which may be a default thread priority generated by the JVM or a customized priority provided by the programmer. The valid range of thread priorities is from 1 to 10, where 1 is the minimum thread priority and 10 is the maximum thread priority. The Thread class defines the following constants to represent some standard priorities:

  • Thread.MIN_PRIORITY –> 1
  • Thread.NORM_PRIORITY –> 5
  • Thread.MAX_PRIORITY –> 10

Identifying valid thread priorities:

  • 0: Invalid
  • 1: Valid
  • 10: Valid
  • Thread.LOW_PRIORITY: Invalid (not a standard constant in the Thread class)
  • Thread.HIGH_PRIORITY: Invalid (not a standard constant in the Thread class)
  • Thread.MIN_PRIORITY: Valid (constant defined in the Thread class)
  • Thread.NORM_PRIORITY: Valid (constant defined in the Thread class)

If two threads have the same priority, then we can’t expect the exact execution order; it depends on the thread scheduler.

The Thread class defines the following methods to get and set the priority of a thread:

  • public final int getPriority()
  • public final void setPriority(int p) (allowed values range: 1 to 10; otherwise, IllegalArgumentException is thrown)
Java
t.setPriority(7);  // valid
t.setPriority(17); // IllegalArgumentException

In this example, setting the priority to 7 is valid, but setting it to 17 would result in an IllegalArgumentException because it’s outside the valid range.

Default Thread Priority

The default thread priority for the main thread is 5, but for all other threads, the default thread priority will be inherited from the parent thread. This means that whatever priority the parent thread has, the same priority will be inherited by the child thread.

Java
class MyThread extends Thread {
}

class Test {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getPriority());  // 5 (default priority for main thread)
        
        // Thread.currentThread().setPriority(15);  // Throws IllegalArgumentException
        // Thread.currentThread().setPriority(7);   // --> line 1 
        
        // line 1 allowed and sets 7 as priority, but it's redundant as the default priority is 5
        
        MyThread t = new MyThread();
        System.out.println(t.getPriority());  //initial output 7 if line 1 is not commented,  otherwise 5 (as inherited from the parent thread, which is the main thread)
    }
}

If we comment line 1, then the child thread priority will become 5, inherited from the parent thread.

MyThread t = new MyThread();
Here, the parent class is Thread class, and its parent thread is the Main Thread.
Parent thread and parent class are different entities.

Thread Priority Example

Let’s discuss the scenario of setting thread priorities and its implications:

Java
class MyThread extends Thread {
  public void run() {
    for(int i=0; i<10; i++) {
      System.out.println("child thread");
    } 
  }
}

class ThreadPriorityDemo {
  public static void main(String[] args) {
    MyThread t = new MyThread();
    //t.setPriority(10);   --------->  line 1
    t.start();
   
    for(int i=0; i<10; i++) { 
      System.out.println("main thread");
    }
  }
}

If we comment out line 1, then both the child thread and the main thread will have the same priority, which is 5. Hence, we can’t expect the execution order and the exact output, as we don’t know which thread will get the first chance.

If we don’t comment out line 1, then the main thread has priority 5 and the child thread has priority 10. In this case, the child thread will get the chance first, followed by the main thread. The output will be:

Java
child thread
child thread
(child thread repeated 10 times)
main thread
(main thread repeated 10 times)

Note: Some platforms may not provide proper support for thread priorities. In such cases, even if we set high priority, we may not get the exact output. This is a system problem, and there’s nothing we can do about it.

Ways to Temporarily Prevent Thread Execution

We can prevent a thread’s execution temporarily by using the following methods:

  1. yield()
  2. join()
  3. sleep()

yield() Method

yield() causes the current executing thread to pause and gives a chance for waiting threads of the same priority. If there are no waiting threads or all waiting threads have a lower priority, then the same thread can continue its execution. If multiple threads are waiting with the same priority, we can’t predict which waiting thread will get the chance; it depends on the thread scheduler. When the thread that yielded will get a chance again also depends on the thread scheduler, and we can’t predict it precisely.

Complete prototype of yield():

Java
public static native void yield();

Impact of yield() method on thread life cycle:

Java
class MyThread extends Thread {
  public void run() {
    for(int i=0; i<10; i++) {
      System.out.println("Child Thread");
      Thread.yield();  // Commented line 1
    }   
  }
}

class ThreadYieldDemo {
  public static void main(String[] args) {
    MyThread t = new MyThread();
    t.start();
    for(int i=0; i<10; i++) {
      System.out.println("main Thread");
    }
  }
}

In the above program, if we comment out line 1, then both threads will be executed simultaneously, and we can’t predict which thread will complete first. If we don’t comment out line 1, then the child thread will always call yield(), causing the main thread to get more chances to execute, increasing the likelihood of the main thread completing first.

Note: Some platforms may not provide proper support for yield().

join() Method

This method waits for a thread to complete its execution. When a thread calls join() on another thread, it will wait until the specified thread completes its execution before continuing its own execution. There are several overloaded versions of the join() method that allow you to specify a timeout duration for the waiting period.

  • Prototype of join() method:
Java
public final void join() throws InterruptedException
public final void join(long ms) throws InterruptedException
public final void join(long ms, int ns) throws InterruptedException

Note: Every join() method throws InterruptedException, which is a checked exception. Hence, we must handle it using either try-catch or by using the throws keyword, otherwise, we will get a compile-time error.

Usage of join() Method

For example, if a thread t1 wants to wait until another thread t2 completes, then t1 has to call t2.join(). When t1 executes t2.join(), it immediately enters a waiting state until t2 completes. Once t2 completes, t1 can continue its execution.

Example Scenario:

Venue Fixing activity (t1)
Wedding card printing (t2) —- t1.join() —-
Wedding card distribution (t3) —– t2.join() —-

The wedding card printing thread has to wait until the venue fixing thread, hence t2 needs to call t1.join(). Similarly, the wedding card distribution thread has to wait until the wedding card printing thread, hence t3 needs to call t2.join().

Impact of join() on thread life cycle:

Java
class MyThread extends Thread {
  public void run() {
    for(int i=0; i<10; i++) {
      System.out.println("Seetha Thread");
      try {
        Thread.sleep(2000);
      } catch(InterruptedException e) {
        // Handle the exception
      }
    }
  }
}
 
class ThreadJoinDemo {
  public static void main(String[] args) throws InterruptedException {
    MyThread t = new MyThread();
    t.start();
  
    t.join();     // Commented line 1   (Main thread waits for the child thread to complete)
   
    for(int i=0; i<10; i++) {
      System.out.println("Rama Thread");
    }
  }
}

In the above program, if we comment out line 1, then both the main and child threads will execute simultaneously, and we can’t predict the exact output. If we don’t comment out line 1, then the main thread calls join() on the child thread object, causing the main thread to wait until the child thread completes. In this case, the output will be:

Java
Seetha Thread
Seetha Thread
.. (repeated 10 times)

Rama Thread
Rama Thread 
.. (repeated 10 times)

Waiting of Child Thread Until Completing Main Thread

Let’s discuss the scenario where the child thread waits until the completion of the main thread using the join() method:

Java
class MyThread extends Thread {
    static Thread mt; //this line holds the main thread object in the main method

    public void run() {
        try {
            mt.join(); // Child thread waits until the main thread completes
        } catch (InterruptedException e) {
            // Handle the exception
        }

        for (int i = 0; i < 10; i++) {
            System.out.println("child thread");
        }
    }
}

class ThreadJoinDemo {
    public static void main(String[] args) throws InterruptedException {
        MyThread.mt = Thread.currentThread();
        MyThread t = new MyThread();
        t.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("main thread");
            Thread.sleep(2000);
        }
    }
}

In the above example, the child thread calls join method on the main thread object, hence the child thread has to wait until the main thread completes. In this case, the output is:

Java
main thread
main thread 
.
.
10 times
.
child thread 
.
.
10 times 
.

Explanation

  • In this example, the child thread (MyThread) calls the join() method on the main thread object (mt). This causes the child thread to wait until the main thread completes its execution.
  • The main thread is identified by Thread.currentThread(), and its reference is stored in the mt static variable of the MyThread class.
  • After starting the child thread, the main thread continues its execution. It prints “main thread” ten times with a 2-second delay between each print statement.
  • Meanwhile, the child thread is in the waiting state due to the join() method. Once the main thread completes its execution, the child thread resumes and prints “child thread” ten times.
  • This ensures that the child thread waits for the main thread to finish before continuing its own execution.

Deadlock Situation in case of join() method

Let’s discuss two scenarios involving the join() method that can lead to program deadlock:

Case 1: Deadlock between Main Thread and Child Thread:

If the main thread calls the join method on the child thread object, and the child thread also calls join() on the main thread object, then both threads will wait indefinitely, resulting in a deadlock. This situation resembles a deadlock scenario where neither thread can proceed.

Java
class MyThread extends Thread {
    static Thread mainThread;

    public void run() {
        try {
            mainThread.join(); // Child thread waits for main thread
        } catch (InterruptedException e) {
            // Handle the exception
        }

        System.out.println("Child thread finished.");
    }
}

class ThreadDeadlockDemo {
    public static void main(String[] args) throws InterruptedException {
        MyThread.mainThread = Thread.currentThread(); // Save reference to main thread
        MyThread t = new MyThread();
        t.start();

        try {
            t.join(); // Main thread waits for child thread
        } catch (InterruptedException e) {
            // Handle the exception
        }

        System.out.println("Main thread finished.");
    }
}

Case 2: Deadlock within the Same Thread:

If a thread calls the join method on itself, then the program will be stuck, similar to a deadlock situation. In this case, the thread has to wait indefinitely for itself to complete, which cannot happen.

Java
class Test {
    public static void main(String[] args) throws InterruptedException {
        Thread.currentThread().join(); // Main thread waits for itself
    }
}

In the above example, the main thread is calling join() on itself, causing the program to get stuck indefinitely.

Note: In both cases, the program will be stuck indefinitely, unable to proceed further, due to deadlock situations. These scenarios illustrate the importance of using join() method with caution to avoid deadlocks.

sleep() Method

If a thread doesn’t need to perform any operation for a specific amount of time, then it should use the sleep method. This method causes the currently executing thread to sleep (temporarily suspend execution) for the specified duration.

Prototypes:

Java
public static native void sleep(long ms) throws InterruptedException
public static void sleep(long ms, int ns) throws InterruptedException

Note:
Every sleep() method throws InterruptedException, which is a checked exception. Therefore, whenever we use the sleep method, we must handle InterruptedException either by using try-catch or by using the throws keyword, otherwise, we will get a compile-time error.

Impact of sleep method on Thread life cycle

When a thread calls the sleep() method, it enters the TIMED_WAITING state for the specified duration. During this time, the thread does not consume CPU resources and is not eligible for execution. After the specified duration elapses, the thread transitions back to the RUNNABLE state and continues its execution.

Java
class SlideRotator {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 1; i <= 10; i++) {
            System.out.println("Slide-" + i);
            Thread.sleep(5000); // Sleep for 5 seconds
        }
    }
}
  • In this example, the main method prints the slides from 1 to 10 and then sleeps for 5 seconds (Thread.sleep(5000)) before printing the next slide.
  • After printing each slide, the thread sleeps for 5 seconds before proceeding to print the next slide.
  • During the sleep period, the thread transitions to the TIMED_WAITING state, where it remains until the specified duration (5 seconds) elapses.
  • Once the sleep duration is over, the thread transitions back to the RUNNABLE state and continues its execution by printing the next slide.

So, in the above example, the thread pauses for 5000 milliseconds (5 seconds) after printing each slide. This simulates a slide rotation process where each slide is displayed for 5 seconds before moving to the next one.

interrupt() Method

A thread can interrupt a sleeping or waiting thread by using the interrupt() method of the Thread class. This method interrupts the execution of the target thread, causing it to throw an InterruptedException if the target thread is in a sleeping or waiting state.

Prototype:

Java
public void interrupt();

When a thread calls interrupt() on another thread, it sets the interrupt flag for that thread. If the target thread is in a blocking method such as sleep() or wait(), it will throw an InterruptedException immediately, or the next time it enters a blocking operation.

Java
class MyThread extends Thread {
    public void run() {
        try {
            for (int i = 0; i < 10; i++) {
                System.out.println("I am lazy thread");
                Thread.sleep(2000);
            }
        } catch (InterruptedException e) {
            System.out.println("I got interrupted");
        }
    }
}

class ThreadInterruptedDemo {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
        t.interrupt(); // Commented line 1 (Interrupt the child thread)
        System.out.println("End of main thread");
    }
}

If we comment out line 1, then the main thread won’t interrupt the child thread. In this case, the child thread will execute the for loop 10 times.

If we don’t comment out line 1, then the main thread interrupts the child thread. In this case, the output is:

Java
End of main thread 
I am lazy thread
I got interrupted
  • In this example, the main thread interrupts the MyThread thread by calling the interrupt() method on it.
  • If line 1 is commented out, the main thread won’t interrupt the child thread. In this case, the child thread executes the for loop 10 times without interruption.
  • If line 1 is not commented out, the main thread interrupts the child thread. As a result, the child thread throws an InterruptedException, and the message “I got interrupted” is printed.
  • Regardless of whether the child thread is interrupted or not, the message “End of main thread” is always printed at the end of the main thread’s execution.

Note: Whenever we call the interrupt method, if a target thread is not in a sleeping state or waiting state, then there is no immediate impact of the interrupt call. The interrupt call will be queued until the target thread enters a sleeping or waiting state. If the target thread is in a sleeping or waiting state, then the interrupt call will immediately interrupt the target thread.

Java
class MyThread extends Thread {
  public void run() {
    for(int i=0; i<=10000; i++) {
      System.out.println("I am lazy thread-"+i);
    }
    System.out.println("I am entering into sleeping state");
    try {
      Thread.sleep(10000);
    } catch(InterruptedException e) {
      System.out.println("I got interrupted");
    }
  }
}

class ThreadSleepDemo1 {
  public static void main(String[] args) {
    MyThread t = new MyThread();
    t.start();
    t.interrupt();  // Interrupt call is made immediately
    System.out.println("End of main thread");
  }
}

In the above example, the target thread t is interrupted immediately after the start method is called. However, since the target thread is not in a sleeping or waiting state yet, the interrupt call will not have any immediate effect. The interrupt call will be queued until the target thread enters the sleeping state. Therefore, in this case, the output will be:

Java
End of main thread
I am lazy thread-0
I am lazy thread-1
...
I am lazy thread-9999
I am entering into sleeping state
I got interrupted
  • In this example, the main thread starts a MyThread thread and immediately calls the interrupt() method on it.
  • However, since the MyThread thread is executing a loop and not in a sleeping or waiting state, the interrupt call has no immediate effect.
  • The interrupt call will be queued until the MyThread thread enters a sleeping or waiting state.
  • In this case, the MyThread thread eventually enters a sleeping state using Thread.sleep(10000), and the interrupt call immediately interrupts it, causing it to throw an InterruptedException.
  • If the MyThread thread never enters a sleeping or waiting state, the interrupt call would have no effect throughout its execution.

Quick Revision: yield(), join(), and sleep() methods

Let’s discuss yield(), join(), and sleep() methods in Java, along with their characteristics:

yield()

Purpose: If a thread wants to pause its execution to give a chance for the remaining threads of the same priority, then we should use yield().

  • Is it Overloaded? No,
  • Is it final? No
  • Does it throw InterruptedException? No
  • Is it native? Yes
  • Is it static? Yes

join()

Purpose: If a thread wants to wait until completing some other thread, then we should use join().

  • Is it Overloaded? Yes
  • Is it final? Yes
  • Does it throw InterruptedException? Yes
  • Is it native? No
  • Is it static? No

sleep()

Purpose: If a thread doesn’t want to perform any operation for a particular amount of time, then we should use the sleep method.

  • Is it Overloaded? Yes
  • Is it final? No
  • Does it throw InterruptedException? Yes
  • Is it native? sleep(long ms) is native, sleep(long ms, int ms) is non-native
  • Is it static? Yes

Part 3: Mastering Synchronization in Java Threads

Conclusion

Thread management is a crucial aspect of developing robust and efficient multithreaded Java applications. The yield(), join(), and sleep() methods provide valuable tools for controlling thread execution, synchronization, and timing.

By leveraging these methods effectively, developers can orchestrate concurrent activities, coordinate thread interactions, and manage resource contention, thereby enhancing the performance and reliability of their applications in a concurrent environment.

Understanding the nuances of these thread-related methods empowers developers to design and implement multithreaded applications that exhibit optimal concurrency behavior, scalability, and responsiveness, contributing to the development of high-performance Java software systems.

String Interning in Java

String Interning: A Memory-Saving Optimization for Efficient String Handling

In the realm of Java programming, efficient memory management is crucial for optimizing performance. One of the lesser-known yet powerful techniques employed by Java to conserve memory is string interning. String interning is a mechanism that allows multiple string objects with the same value to share the same memory location, thus reducing memory footprint and improving performance. In this blog post, we’ll explore the concept of string interning, its implementation in Java, its benefits, and when to use it.

What is String Interning?

String interning is the process of storing only one copy of each distinct string value in memory, rather than creating multiple copies. When a string is interned, it is added to a pool of strings called the “string intern pool” or “string constant pool.” Subsequent occurrences of the same string literal or identical string values are then referenced to the same string instance in the pool, rather than creating a new object. This ensures that memory is conserved by eliminating redundancy.

Interning of String object in Java

We can use the intern() method to get the corresponding SCP (String Constant Pool) object reference by using a heap object reference (or by using a heap object reference if we want to get the corresponding SCP object reference, then we should go for the intern method).

Java
String s1 = new String("softAai");
String s2 = s1.intern();
System.out.println(s1 == s2); // false
String s3 = "softAai";
System.out.println(s2 == s3); // true

In the heap area: s1 points to "softAai".

In the SCP area: s2 and s3 point to "softAai".

If the corresponding SCP object is not available, then the intern method itself will create the corresponding SCP object.

Java
String s1 = new String("softAai");
String s2 = s1.concat("Apps");
String s3 = s2.intern();
System.out.println(s2 == s3); // false
String s4 = "softAaiApps";
System.out.println(s3 == s4);

In the heap area:

s1 points to "softAai".

s2 points to "softAaiApps".

In the SCP area:

"softAai", "Apps" are stored individually,

Both s3, and s4 point to "softAaiApps".

Practical Proof of Where Objects Get Created: Heap Area or SCP Area

To illustrate the practical proof of where objects are created, let’s consider the following code:

Java
String s1 = new String("you cannot change me!");
String s2 = new String("you cannot change me!");

System.out.println(s1 == s2); // false

String s3 = "you cannot change me!";

System.out.println(s1 == s3); // false

String s4 = "you cannot change me!";
System.out.println(s3 == s4); // true

String s5 = "you cannot" + "change me!"; // ----> 1
System.out.println(s3 == s5); // true  

String s6 = "you cannot";
String s7 = s6 + "change me!"; // ----> 2

System.out.println(s3 == s7); // false

final String s8 = "you cannot";
String s9 = s8 + "change me!"; // -----> 3

System.out.println(s3 == s9);   // true
System.out.println(s6 == s8);    // true 

In the heap area:

  • s1 points to “you cannot change me!”
  • s2 points to “you cannot change me!”
  • s7 points to “you cannot change me!”

In the SCP area:

  • s3, s4, s5, s9 point to “you cannot change me!”
  • s6, s8 point to “you cannot”
  • “change me!” is stored individually
Note

In the above code, see lines marked as 1, 2, and 3,

Now, let’s analyze the lines marked as —->1, —->2, and —->3:

  1. This operation will be performed at compile time only because both arguments are compile-time constants.
  2. This operation will be performed at runtime only because at least one argument is a normal variable.
  3. This operation will be performed at compile time only because both arguments are compile-time constants.

Importance of String Constant Pool (SCP)

In our program, if a string object is required to be repeated, it is not recommended to create separate objects because it creates performance and memory problems. Instead of creating separate objects for every requirement, we have to create only one object, and we can reuse the same object for every requirement. This approach improves performance and memory utilization. This efficiency is possible because of SCP. Hence, the main advantages of SCP are improved memory utilization and performance.

However, the main problem with SCP is that if several references point to the same object, using one reference to change the content will affect the remaining references. To overcome this problem, Sun implemented string objects as immutable. This means that once we create a string object, we can’t perform any changes to the existing object. If we attempt to make any changes, a new object will be created. Hence, SCP is the primary reason for the immutability of string objects.

Conclusion

String interning is a powerful memory optimization technique in Java that allows for efficient storage and retrieval of string objects. By intelligently managing memory and reducing redundancy, string interning contributes to improved performance and resource utilization in Java applications. Understanding the principles and benefits of string interning empowers Java developers to leverage this technique effectively to optimize their applications for memory and performance efficiency.

Object Cloning in Java

Demystifying Object Cloning in Java: A Detailed Guide

Java, being an object-oriented programming language, allows developers to create and manipulate objects. In many cases, you might encounter a situation where you need to create a copy of an existing object. This process of creating an exact copy of an object is called object cloning. Understanding object cloning in Java is essential for various programming scenarios, such as creating copies of complex objects, implementing prototype patterns, and more. In this comprehensive guide, we’ll delve into the concept of object cloning in Java, explore different techniques, and discuss best practices.

What is Object Cloning?

The process of creating an exactly duplicate object is called cloning. The main purpose of cloning is to maintain a backup copy and preserve the state of the object. We can perform cloning by using the clone method of the Object class:

Java
protected native Object clone() throws CloneNotSupportedException

In Java, to enable cloning for objects, the corresponding class must implement the Cloneable interface. This interface, found in the java.lang package, doesn’t contain any methods; it serves solely as a marker interface. Attempting to clone an object that doesn’t implement Cloneable will result in a CloneNotSupportedException.

Java
class Test implements Cloneable {
    int i = 10;
    int j = 20;

    public static void main(String[] args) throws CloneNotSupportedException {
        Test t1 = new Test();
        Test t2 = (Test) t1.clone();
        t2.i = 888;
        t2.j = 999;
        System.out.println(t1.i + "......" + t1.j); //10...20
    }
}

Here, Test class implements the Cloneable interface, allowing its objects to be cloned using the clone() method.

What are different approaches to cloning objects in Java

Shallow cloning and deep cloning are two different approaches to cloning objects in Java.

Shallow Cloning

Shallow cloning refers to the process of creating a bitwise copy of an object. If the main object contains primitive variables, then exactly duplicate copies will be created in the cloned object. However, if the main object contains any reference variables, then corresponding objects won’t be created; instead, duplicate reference variables will be created, pointing to the old content object. The Object class’s clone method is meant for shallow cloning.

Java
class Cat {
  int j;
  Cat(int j) {
    this.j = j;
  }
}

class Dog implements Cloneable {
  Cat c;
  int i;
  Dog(Cat c, int i) {
    this.c = c;
    this.i = i;
  }

  public Object clone() throws CloneNotSupportedException {
    return super.clone();
  }
}

class ShallowCloning {
  public static void main(String[] args) throws CloneNotSupportedException {
    Cat c = new Cat(20);
    Dog d1 = new Dog(c, 10);
    System.out.println(d1.i + "..............." + d1.c.j);
    Dog d2 = (Dog)d1.clone();
    d2.i = 888;
    d2.c.j = 999;
    System.out.println(d1.i + "........." + d1.c.j); // Output: 10 .... 999
  }
}

In shallow cloning, if changes are made to the content object through the cloned object reference, those changes will be reflected in the main object. To overcome this problem, we should use deep cloning.

Deep Cloning

Deep cloning refers to the process of creating an exactly duplicate independent copy, including the content object. In deep cloning, if the main object contains any primitive variables, then duplicate values will be created in the cloned object. If the main object contains any reference variables, then corresponding cloned objects will be created in the cloned copy. By default, the Object class’s clone method is meant for shallow cloning, but we can implement deep cloning explicitly by overriding the clone method in our class.

Java
class Cat {
  int j;
  Cat(int j) { 
    this.j = j; 
  } 
} 

class Dog implements Cloneable {
  Cat c;
  int i;
   
  Dog(Cat c, int i) { 
    this.c = c;
    this.i = i;
  }

  public Object clone() throws CloneNotSupportedException {
    Cat c1 = new Cat(c.j);
    Dog d  = new Dog(c1, i);
    return d;
  } 
}

class DeepCloning {
  public static void main(String[] args) throws CloneNotSupportedException {
    Cat c = new Cat(20);
    Dog d1 = new Dog(c, 10);
    System.out.println(d1.i + "......." + d1.c.j);
    Dog d2 = (Dog) d1.clone();
    d2.i = 888;
    d2.c.j = 999;
    System.out.println(d1.i + "..........." + d1.c.j);
  }
}

When using a cloned object reference, if we perform any change to the contained object, those changes won’t be reflected in the main object.

Which cloning method is the best?

While shallow cloning creates a new object with copies of the original object’s fields, including references to the same content objects, deep cloning creates a completely independent duplicate copy, including new instances of all referenced objects. The choice between shallow and deep cloning depends on the specific requirements of the cloning operation and the desired behavior of the cloned objects.

If the object contains only primitive variables, then shallow cloning is the best choice. If the object contains reference variables, then deep cloning is the best choice.

Best Practices for Object Cloning

When implementing object cloning in Java, consider the following best practices:

  1. Override clone() method: Always override the clone() method to ensure proper cloning behavior.
  2. Implement Cloneable interface: If you choose to use the clone() method, implement the Cloneable interface to indicate that your class supports cloning.
  3. Handle CloneNotSupportedException: Handle the CloneNotSupportedException appropriately when cloning an object.
  4. Deep cloning when necessary: For complex objects containing reference types, implement deep cloning to ensure that all referenced objects are also cloned.
  5. Immutable classes: If possible, design your classes to be immutable to avoid the need for cloning.

Conclusion

Object cloning in Java is a powerful mechanism for creating copies of objects. By understanding the concepts of shallow and deep cloning and implementing the clone() method or Cloneable interface, you can effectively clone objects in your Java applications. However, it’s essential to be cautious while cloning objects, especially when dealing with complex data structures and mutable objects. Following best practices and considering the implications of cloning ensures that your Java applications perform as expected while maintaining the integrity of your data.

== Operator and .equals() Method in Java

Comprehensive Analysis: == Operator versus .equals() Methods in Java

In Java, developers often encounter situations where they need to compare objects for equality. However, understanding the nuances between using the == operator and the .equals() method is crucial for writing correct and efficient code. This article delves into the relationship between these two approaches in the java.lang package, providing insights into their differences, similarities, and best practices.

Understanding the == Operator

The == operator in Java is primarily used for comparing primitive data types and object references. When applied to primitive types such as int, double, or char, it compares their values directly. For example:

Java
int a = 5;
int b = 5;
boolean result = (a == b); // result will be true

However, when it comes to objects, the == operator behaves differently. It compares object references rather than their actual contents. Two object references are considered equal if they point to the same memory location, i.e., if they refer to the same object instance. For instance:

Java
String str1 = new String("softAai");
String str2 = new String("softAai");
boolean result = (str1 == str2); // result will be false

Here, str1 and str2 point to different memory locations, even though their contents are the same. Therefore, the == operator returns false.

Understanding the .equals() Method

In Java, the .equals() method is used to compare the contents of objects for equality. The java.lang.Object class, which is the root class for all Java classes, provides a default implementation of the .equals() method. This default implementation compares object references similar to the == operator.

However, many classes in Java override the .equals() method to provide custom equality comparison based on the contents of objects rather than their references. For example, the String class overrides the .equals() method to compare the actual string contents:

Java
String str1 = new String("Hello");
String str2 = new String("Hello");
boolean result = str1.equals(str2); // result will be true

In this case, the .equals() method compares the contents of str1 and str2, resulting in true since they contain the same string.

Relation between == Operator and .equals() Method

The relationship between the == operator and the .equals() method can be summarized as follows:

  • If two objects are determined to be equal by the == operator, it’s guaranteed that they will also be equal when compared using the .equals() method.
    • This means that if r1 == r2 evaluates to true, then r1.equals(r2) will always be true as well.
  • However, if two objects are not deemed equal by the == operator, we cannot make any assumptions about the outcome of the .equals() method. It may return either true or false.
    • In other words, if r1 == r2 evaluates to false, r1.equals(r2) may return true or false, and we cannot predict the exact result.
  • Similarly, if two objects are determined to be equal by the .equals() method, we cannot infer anything about the == operator. It may return true or false.
    • So, if r1.equals(r2) is true, then r1 == r2 may return true or false, and we cannot determine the exact outcome.
  • Conversely, if two objects are not equal according to the .equals() method, they will always not be equal when compared using the == operator.
    • Therefore, if r1.equals(r2) evaluates to false, then r1 == r2 will always be false as well.

Example

Java
String s1 = new String("softAai");
String s2 = new String("softAai");
StringBuffer sb1 = new StringBuffer("softAai");
StringBuffer sb2 = new StringBuffer("softAai");

System.out.println(s1 == s2);  //false
System.out.println(s1.equals(s2)); //true
System.out.println(sb1 == sb2);  //false
System.out.println(sb1.equals(sb2)); //false
// System.out.println(s1 == sb1);  //CE: incomparable types: java.lang.String and java.lang.StringBuffer
System.out.println(s1.equals(sb1)); //false

Note: To use the == operator, there must be some relation between argument types (either child to parent, parent to child, or the same type); otherwise, we will get a compilation error saying “incomparable types.” If there is no relation between argument types, then the .equals() method won’t raise any runtime or compile-time errors; it simply returns false.

Differences between the == operator and .equals() method

The differences between the == operator and .equals() method can be summarized as follows:

== operator

  • It is an operator applicable for both primitives and object types
  • In the case of object references, the == operator is meant for reference comparison, which entails comparing memory addresses.
  • We can’t override the == operator for content comparison
  • To use the == operator, there must be a relationship between the argument types (either child to parent, parent to child, or the same type); otherwise, we will receive a compile-time error indicating “incomparable types.”

.equals() method

  • It is a method applicable only for object types and not for primitives.
  • By default, the .equals() method present in the object class is also intended for reference comparison.
  • We can override the .equals() method for content comparison.
  • If there is no relation between argument types, then .equals() won’t raise any compile-time or runtime errors and simply returns false.

In general, use the == operator for reference comparison and .equals() for content comparison;

For any object reference “r,”

both “r == null” and “r.equals(null)” always return false.

Java
// Example:
Thread t = new Thread();
System.out.println(t == null); // false
System.out.println(t.equals(null)); // false

Also, one more thing,

Hashing-related data structures follow the following fundamental rules:

Two equivalent objects should be placed in the same bucket, but not all objects present in the same bucket need to be equal.

Contract between .equals() and hashCode()

If two objects are equal by the .equals() method, then their hashcodes must be equal; that is, two equivalent objects should have the same hashcode. Therefore, if “r1.equals(r2)” is true, then “r1.hashCode() == r2.hashCode()” is always true. The Object class’s .equals() and hashCode() methods follow these contracts; hence, whenever we override .equals(), it is compulsory to override hashCode() to satisfy these contracts. This ensures that two equivalent objects have the same hashcode.

If two objects are not equal by .equals(), then there are no restrictions on their hashcodes; they may be equal or not equal. If the hashcodes of two objects are equal, we cannot conclude anything about .equals(); it may return true or false. However, if the hashcodes of two objects are not equal, then these objects are always not equal by the .equals() method.

Note: To satisfy the contract between equals and hashCode methods, whenever we are overriding .equals(), it is compulsory to override the hashCode method; otherwise, we won’t get any compile-time or runtime error, but it is not a good programming practice.

In the String class, .equals() is overridden for content comparison, hence the hashCode method is also overridden to generate a hashcode based on content. For example:

Java
String s1 = new String("softAai");
String s2 = new String("softAai");
System.out.println(s1.equals(s2)); // true
System.out.println(s1.hashCode()); // 95950491
System.out.println(s2.hashCode()); // 95950491

In the StringBuffer class, .equals() is not overridden for content comparison, and hence the hashCode method is also not overridden. For example:

Java
StringBuffer sb1 = new StringBuffer("softAai");
StringBuffer sb2 = new StringBuffer("softAai");
System.out.println(sb1.equals(sb2)); // false
System.out.println(sb1.hashCode()); // 19621457
System.out.println(sb2.hashCode()); // 4872882

It is highly recommended to override the hashCode() method. In all collection classes, in all wrapper classes, and in the String class, .equals() is overridden for content comparison; hence, it is highly recommended to override .equals() as well for content comparison.

Best Practices

To ensure correctness and efficiency in your Java code, consider the following best practices:

  1. Use the == operator for primitive types and object reference comparison.
  2. Use the .equals() method for comparing the contents of objects, especially when dealing with strings, arrays, and custom classes.
  3. Override the .equals() method in custom classes to provide meaningful content comparison.
  4. Be cautious when comparing objects of different types, as the behavior of == and .equals() may vary depending on the implementation.

Conclusion

Understanding the relationship between the == operator and the .equals() method is essential for writing robust and efficient Java code. By knowing when to use each approach and following best practices, developers can ensure correct object comparison and avoid common pitfalls. Whether comparing object references or their