Multithreading and Concurrency In Java

Java provides built-in support for multithreaded programming. A thread is an independent path of execution within a program. Multithreading allows you to take advantage of multiple CPU cores by executing multiple threads in parallel. Concurrency refers to dealing with lots of things at once. In Java, concurrency is achieved through multithreading.

Why Use Multithreading?

There are several reasons to use multithreading in your Java programs:

  • Improved performance: By executing tasks concurrently on separate threads, you can improve performance and responsiveness. This is especially true when threads are running on separate CPU cores.
  • Better resource utilization: Threads allow multiple tasks to share scarce resources like memory and CPU cycles without conflict.
  • Simplified modeling: Multithreading can simplify modeling and implementation of concurrent activities that may be encountered in real-world applications.
  • Convenient handling of asynchronous events: Multithreading makes it easy to handle asynchronous events like user input concurrently with main program execution.

Thread Lifecycle

The lifecycle of a thread in Java consists of several discrete stages:

  • New: A new thread instance is created using the Thread class constructor. The thread at this point has no runtime behavior associated with it.
  • Runnable: The newly born thread calls the start() method, which makes it eligible to run and be scheduled by the thread scheduler. The thread may now run at anytime.
  • Running: The JVM thread scheduler picks the thread to execute its target run() method. A running thread may be suspended or halted by the scheduler at anytime.

Blocked/Waiting: The thread is still alive but is currently not eligible to run. This may happen if it is waiting on a monitor lock or some I/O operation.

  • Terminated: The run() method completes execution. Alternatively, stop() may have been called. The thread is dead and cannot be restarted.

Thread States

During its lifetime, a thread transitions through various states. The main thread states are:

  • New: Just instantiated using Thread constructor
  • Runnable: Eligible to run on CPU when scheduled
  • Running: Currently being executed by a CPU core
  • Waiting: Waiting on some monitor lock or I/O operation
  • Timed Waiting: Waiting with specified waiting time
  • Blocked: Waiting to acquire a monitor lock
  • Terminated: Execution completed

Creating and Starting Threads

There are two ways to create a new thread in Java:

  • By extending the Thread class
  • By implementing the Runnable interface

Extending Thread class

You can create a new thread by extending the Thread class and overriding its run() method:

public class MyThread extends Thread {

  @Override
  public void run() {
    // thread task
  }

}
JavaScript

To start the thread, create an instance and call its start() method:

MyThread thread = new MyThread(); 
thread.start();
JavaScript

Implementing Runnable Interface

The Runnable interface defines a single run() method:

public interface Runnable {
  public void run();
}
JavaScript

You can create a runnable task like so:

public class MyTask implements Runnable {

  @Override
  public void run() {
    // thread task    
  }
  
}
JavaScript

Then pass an instance to a Thread to execute the task:

Runnable task = new MyTask();
new Thread(task).start();
JavaScript

Runnable is generally preferred over extending Thread since Java does not support multiple inheritance.

Thread Priority

Thread priority indicates to the scheduler the relative desirability of scheduling a thread. Java provides ten priority levels from MIN_PRIORITY (1) to MAX_PRIORITY (10).

By default, every thread gets priority NORM_PRIORITY (5). To change the priority:

thread.setPriority(Thread.MAX_PRIORITY);
JavaScript

Higher priority threads get executed in preference to those with lower priority. However, thread priorities cannot guarantee ordering. They are just hints to the scheduler.

Daemon Threads

A daemon thread only runs in the background to provide services to user threads. Its defining feature is that the JVM exits when only daemon threads remain.

To mark a thread as daemon, simply call setDaemon(true) before starting it:

thread.setDaemon(true); 
thread.start();
JavaScript

Daemon threads are useful for services like garbage collection and monitoring.

Thread Synchronization

Thread synchronization refers to controlling the timing and order of thread execution to avoid unpredictable concurrent access problems. This is achieved through synchronized blocks and methods.

Synchronized Block

To create a synchronized block, simply add the synchronized keyword:

synchronized(lock) {
  // access shared data
}
JavaScript

This block will execute after acquiring the intrinsic lock on lock. Other threads trying to access the block must wait.

Synchronized Method

Marking a method as synchronized makes it acquire a lock on the owning instance before execution:

synchronized void add(int value) {
  // update shared data 
}
JavaScript

Static synchronized methods lock on the class instance instead.

Reentrant Locks

ReentrantLock is a more flexible alternative to implicit monitor locking. Key features include:

  • Ability to hold lock over multiple method calls.
  • Multiple condition variables for finer notification control.
  • Non-blocking tryLock() method.

Basic usage:

Lock lock = new ReentrantLock();

lock.lock();
try {
  // access resource
}
finally {  
  lock.unlock();
}
JavaScript

Volatile Variables

The volatile keyword indicates that a variable’s value will be modified from concurrent threads. Its key behaviors include:

  • Updates to volatile variables are immediately visible across threads.
  • Volatile reads do not cache or re-order writes from other threads.

Volatile variables provide happens-before relationship and synchronization visibility guarantees. But they do not provide atomicity or mutual exclusion semantics.

Thread Cooperation

Threads need ways to safely communicate results and coordinate execution. Java provides several mechanisms for thread cooperation.

Wait and Notify

Objects can call wait() to pause thread execution and notify()/notifyAll() to wake waiting threads. This provides basic signaling between threads:

// Waiting thread
syncObject.wait(); 

// Notifying thread  
syncObject.notifyAll();
JavaScript

Joining Threads

The join() method allows a thread to wait for completion of another:

Thread t2 = new Thread(...);
t2.start();

t2.join(); // wait for t2 to finish
JavaScript

This enables coordinating the execution order of interdependent threads.

ThreadLocal

A ThreadLocal variable creates a separate instance accessible only to the owning thread. This provides simple thread-safe sharing without explicit locking.

private ThreadLocal counter = new ThreadLocal();

counter.set(1); // per-thread instance
JavaScript

Thread Pools

Managing a large number of concurrent threads can be error-prone and resource intensive. The solution is to use a thread pool backed by a queue.

Java provides ExecutorService for creating and using thread pools:

// single background thread 
ExecutorService pool = Executors.newSingleThreadExecutor();

// reusabled cached thread pool
ExecutorService pool = Executors.newCachedThreadPool(); 

// start tasks
for(Task task : tasks) 
  pool.execute(task);

// shutdown gracefully  
pool.shutdown();
JavaScript

The pool handles queueing and reuse, providing better control over concurrency.

Thread Safety Tips

Here are some tips for writing thread-safe code in Java:

  • Use synchronized blocks/methods judiciously after identifying critical sections.
  • Prefer concurrent collections and atomic variables over synchronization.
  • Avoid shared mutable state by using ThreadLocal and immutable objects.
  • Use ExecutorService/thread pools instead of creating threads manually.
  • Leverage copy-on-write collections like ConcurrentHashMap.
  • Declare methods as synchronized only if needed.
  • Catch ConcurrentModificationException to detect thread interference.
  • Document thread safety guarantees for custom classes and methods.

Conclusion

Multithreading enables efficient utilization of multiprocessor architectures prevalent today. Java provides extensive inbuilt support through a rich concurrency framework. Understanding thread lifecycle, synchronization, locks, thread pools and high level concurrency APIs is key to harnessing the power of Java multithreading. Used judiciously, multithreading can simplify development of robust real-world applications.

Leave a Comment