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

Table of Contents

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.

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!