Multithreading in Java: Benefits, Costs, and Concurrency Models — Part 1
Multithreading is a powerful technique that can improve the performance of Java applications by allowing multiple threads to run concurrently. In this article, we will explore the benefits and costs of multithreading, as well as different concurrency models that can be used to implement it in Java.
Multithreading Benefits
The main benefit of multithreading is improved performance. By allowing multiple threads to execute concurrently, a program can make better use of available resources and reduce the overall execution time. In addition, multithreading can also make programs more responsive by allowing the user interface to remain active while time-consuming tasks are being executed in the background.
Multithreading Costs
Multithreading also has some costs that must be taken into account. One of the main costs is increased complexity. Multithreaded programs are more difficult to write and debug than single-threaded programs, and they require careful synchronization to avoid problems such as race conditions and deadlocks.
Another cost of multithreading is increased resource usage. Each thread requires its own stack and context, which can consume significant amounts of memory. In addition, threads also require synchronization mechanisms, such as locks and semaphores, which can add additional overhead.
Concurrency Models
There are several different concurrency models that can be used to implement multithreading in Java. Some of the most common models include same-threading, single-threaded concurrency, and parallelism.
Same-Threading
Same-threading is a concurrency model where all tasks are executed on the same thread. This model is simple to implement and can be useful for tasks that do not require a lot of computation or I/O. Here is an example:
public class SameThreadExample {
public static void main(String[] args) {
System.out.println("Start of program");
// Perform some task on the main thread
System.out.println("Performing task on main thread");
System.out.println("End of program");
}
}
In this example, all tasks are executed on the main thread, which is the default thread for Java programs.
Single-threaded Concurrency
Single-threaded concurrency is a concurrency model where tasks are executed on a single thread, but the thread can switch between tasks in a non-deterministic way. This model can be useful for tasks that require some degree of concurrency but do not need to execute in parallel. Here is an example:
public class SingleThreadedExample {
public static void main(String[] args) {
System.out.println("Start of program");
// Create a single thread to execute tasks
Thread thread = new Thread(() -> {
System.out.println("Performing task on separate thread");
});
thread.start();
// Perform some task on the main thread
System.out.println("Performing task on main thread");
System.out.println("End of program");
}
}
In this example, a separate thread is created to execute a task, but the main thread continues to execute other tasks while the separate thread is running.
Concurrency vs. Parallelism
It is important to note that concurrency and parallelism are not the same thing. Concurrency refers to the ability to execute multiple tasks at the same time, while parallelism refers to the ability to execute multiple tasks simultaneously on different processors or cores.
Creating and Starting Java Threads
In Java, threads can be created and started using the Thread
class. To create a new thread, you can either subclass the Thread
class and override its run
method, or you can create a new thread by passing a Runnable
object to the Thread
constructor.
public class ThreadExample {
public static void main(String[] args) {
System.out.println("Start of program");
// Create a new thread by subclassing the Thread class
Thread thread1 = new Thread() {
@Override
public void run() {
System.out.println("Thread 1 is running");
}
};
thread1.start();
// Create a new thread by passing a Runnable object to the Thread constructor
Thread thread2 = new Thread(() -> {
System.out.println("Thread 2 is running");
});
thread2.start();
System.out.println("End of program");
}
}
In this example, two threads are created and started, one by subclassing the Thread
class and one by passing a Runnable
object to the Thread
constructor.
Java Virtual Threads
Java Virtual Threads is a new feature introduced in Java 16 that allows for lightweight, user-mode threads to be created and managed by the JVM. These threads are not tied to any OS-level thread and can be scheduled and managed more efficiently than traditional threads.
Example:
public class VirtualThreadExample {
public static void main(String[] args) {
System.out.println("Start of program");
// Create a virtual thread
Executor executor = Executors.newVirtualThreadExecutor();
executor.execute(() -> {
System.out.println("Virtual thread is running");
});
System.out.println("End of program");
}
}
In this example, a virtual thread is created and executed using the newVirtualThreadExecutor
method provided by the Executors
class.
Race Conditions and Critical Sections
Race conditions occur when two or more threads access a shared resource at the same time, resulting in unexpected behavior. To avoid race conditions, critical sections can be used to ensure that only one thread can access a shared resource at a time.
Example:
public class RaceConditionExample {
private static int count = 0;
public static void main(String[] args) {
System.out.println("Start of program");
// Create two threads that increment the count variable
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000000; i++) {
synchronized (RaceConditionExample.class) {
count++;
}
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000000; i++) {
synchronized (RaceConditionExample.class) {
count++;
}
}
});
thread1.start();
thread2.start();
// Wait for the threads to finish
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("End of program, count=" + count);
}
}
In this example, two threads increment a shared variable count
using a synchronized block to ensure that only one thread can access it at a time.
Thread Safety and Shared Resources
Thread safety is the property of a program that ensures correct behavior even when multiple threads are accessing shared resources. To achieve thread safety, shared resources must be accessed in a way that avoids race conditions and other concurrency problems.
Example:
public class ThreadSafetyExample {
private static final List < Integer > list = new ArrayList < > ();
public static void main(String[] args) {
System.out.println("Start of program");
// Create two threads that add elements to the list
Thread thread1 = new Thread(() - > {
for (int i = 0; i < 1000; i++) {
synchronized(ThreadSafetyExample.class) {
list.add(i);
}
}
});
Thread thread2 = new Thread(() - > {
for (int i = 1000; i < 2000; i++) {
synchronized(ThreadSafetyExample.class) {
list.add(i);
}
}
});
thread1.start();
thread2.start();
// Wait for the threads to finish
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// Print the contents of the list
System.out.println("End of program, list=" + list);
}
}
In this example, two threads add elements to a shared list using a synchronized block to ensure that only one thread can access it at a time.
Java Memory Model
The Java Memory Model (JMM) specifies the rules that govern how threads interact with memory in a Java program. The JMM defines a set of happens-before relationships that establish a partial ordering of events in a program, which helps to ensure that threads can access shared resources safely and correctly.
Java Happens Before Guarantee
The Java Happens Before Guarantee is a fundamental principle of the Java Memory Model. It states that if one event happens before another event in a program, then the effects of the first event are guaranteed to be visible to the second event.
Example:
public class HappensBeforeExample {
private static int x = 0;
private static boolean flag = false;
public static void main(String[] args) {
System.out.println("Start of program");
// Thread 1 sets x=1 and then sets flag=true
Thread thread1 = new Thread(() - > {
x = 1;
flag = true;
});
// Thread 2 waits for flag to be set to true and then reads x
Thread thread2 = new Thread(() - > {
while (!flag) {
// Wait for flag to be set
}
System.out.println("x=" + x);
});
thread1.start();
thread2.start();
// Wait for the threads to finish
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("End of program");
}
}
In this example, thread 1 sets a variable x
and a flag flag
, and thread 2 waits for the flag to be set to true before reading x
. The happens-before relationship between the two events ensures that thread 2 sees the correct value of x
.