Process and Thread
Process
A process is an instance of a program that is being executed. It contains the program code and its current activity. Each process has a separate memory space, which means that a process runs independently and is isolated from other processes. It cannot directly access shared data in other processes. Switching from one process to another requires some time for saving and loading registers, memory maps, and other administrative information.
In Java, when you start the main function, you're starting a Java Virtual Machine (JVM) process. The main function runs in a thread of its own, which is often referred to as the "main" thread. This is the entry point of any Java code.
Thread
A thread is a subset of the process. A process can have multiple threads. All threads within the same process share the same memory space, which means they can communicate with each other more easily than if they were separate processes. Threads are sometimes called lightweight processes, and they do not require much overhead to create and exist in the system. Multiple threads within a process share the same data space with the main thread and can therefore share information or communicate with each other more easily than if they were separate processes.
Thread in JVM
The JVM allows an application to have multiple threads of execution running concurrently. Each thread has its own path of execution but the threads in the same process share the same memory space, which allows them to communicate with each other more easily.
It's also worth noting that the JVM itself uses multiple threads, such as the garbage collection thread.
In the context of Java 8 and later, all threads within the same JVM process share the following:
- Heap Memory: This is where all class instances and arrays are allocated. Since this memory is shared, any variable that is not local to the thread (for example, an instance or static variable) can be accessed and modified by all threads.
- Metaspace: This is a native memory space that replaced the older "Method Area". It holds class definitions and other meta-information. It is shared among all threads.
However, each thread in a JVM has its own:
- Program Counter (PC) Register: Each thread has its own PC Register to hold the address of the currently executing JVM instruction. If the method being executed is not native, the PC register contains the address of the JVM instruction currently being executed.
- JVM Stack: Each JVM thread has a private JVM stack, created at the same time as the thread. A JVM stack stores frames. It holds local variables and partial results, and plays a part in method invocation and return. Because the JVM stack is never manipulated directly except to push and pop frames, frames may be heap allocated.
- Native Method Stack: It contains all the native methods used in the application.
Why multithreading
- Hardware Perspective: Threads are lightweight processes and switching between threads is less costly than between processes. In the era of multi-core CPUs, multiple threads can run simultaneously, reducing the overhead of context switching.
- Software Perspective: Modern systems often require handling millions of concurrent requests, and multithreading is the foundation of developing high-concurrency systems. Proper use of multithreading can significantly improve the system's overall concurrency and performance.
- Single-Core CPU Era: In the single-core era, multithreading was mainly used to improve the efficiency of a single process utilizing the CPU and I/O system. When a thread is blocked by I/O, other threads can continue to use the CPU, thereby improving the overall efficiency of the process in utilizing system resources.
- Multi-Core CPU Era: In the multi-core era, multithreading is mainly used to improve the ability of a process to utilize multiple CPU cores. If a complex task is divided among multiple threads, these threads can be mapped to multiple CPUs for execution. As long as there is no resource contention among the threads in the task, the efficiency of task execution will be significantly improved.
Switching between threads in Java is less costly than switching between processes due to the following reasons:
- Shared Memory Space: Threads within the same process share the same memory space, which means they can access the same variables and objects. This allows for easy and fast communication between threads. On the other hand, processes have separate memory spaces. Sharing data between processes requires inter-process communication (IPC) mechanisms, such as pipes or sockets, which are more complex and slower.
- Context Switching Overhead: When the operating system switches from executing one thread to another, this is known as a context switch. For threads within the same process, the context switch involves storing and restoring fewer attributes (mainly the program counter, stack pointer, and registers), so it's faster. When switching between processes, the operating system needs to store and restore a much larger context (including the entire memory map), which takes more time.
- Creation and Termination Overhead: Creating a new thread or terminating an existing one is generally faster than creating or terminating a process, because it requires fewer system resources.
However, it's important to note that while thread switching is less costly, it's not without its challenges. Threads in the same process share the same memory space, so care must be taken to synchronize access to shared data to avoid race conditions and other concurrency-related bugs.
How to start thread
Method 1: Extending the Thread
class
class MyThread extends Thread {
public void run(){
System.out.println("Thread is running...");
}
}
public class Main {
public static void main(String args[]){
MyThread myThread = new MyThread();
myThread.start(); // Starting the thread
}
}
Method 2: Implementing the Runnable
interface
class MyRunnable implements Runnable {
public void run(){
System.out.println("Thread is running...");
}
}
public class Main {
public static void main(String args[]){
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start(); // Starting the thread
}
}
In both examples, the run() method contains the code that will be executed when the thread is started. The start() method is used to begin the execution of the thread. It's important to note that you should always override the run() method to define the task that the thread should perform.
Thread pool
Why thread pool
- Resource Management: Creating and destroying threads takes time and resources. If a program creates a new thread for every task, it can spend more time and resources on thread management than on executing the actual tasks. Thread pools manage the lifecycle of threads, which can significantly reduce the overhead of thread lifecycle management.
- Control Over System Load: By limiting the number of concurrent threads, thread pools can help ensure that the system doesn't become overloaded with threads. This can be particularly important on systems with a large number of CPUs, where a large number of threads could be created without a thread pool.
- Improved Performance: Thread pools usually offer improved performance when executing large numbers of asynchronous tasks. This is due to reduced per-task invocation overhead and more efficient execution due to the active use of available processing resources.
- Resource Reuse: In a thread pool, after a thread has finished executing a task, it can be reused for another task, avoiding the overhead of thread creation and destruction.
- Simplified Error Handling: Thread pools often provide built-in error handling and recovery mechanisms.
- Task Queueing: Thread pools typically maintain a queue of tasks waiting to be executed. This allows the system to handle bursts of tasks in an efficient manner.
In summary, using a thread pool can provide better system stability and improved performance when executing large numbers of tasks.
Thread pool run task
The principle of a thread pool is to have a limited number of threads that can be reused to handle tasks. When a new task is submitted, the thread pool tries to use an idle thread to execute this task. If all threads are busy and the number of threads is less than the maximum pool size, a new thread is created. If the number of threads has reached the maximum, the task is placed into a queue until a thread becomes available.
In a Java thread pool, if one thread encounters an error and throws an uncaught exception, that thread will terminate immediately. However, this does not affect the other threads in the pool; they will continue to execute their tasks. The thread pool will create a new thread to replace the terminated one when needed.
The main thread (i.e., the thread that created and started the thread pool) typically won't be affected by exceptions in the threads of the pool, unless you explicitly get and handle these exceptions in the main thread. For example, if you use the Future.get()
method to get the result of a thread, this method will throw an ExecutionException
if the task in the thread threw an exception.
As for the thread that encountered an error and terminated, if its exception was not caught and handled, the exception will be handled by the thread's UncaughtExceptionHandler
. If no UncaughtExceptionHandler
is set, the default behavior is to print the stack trace to System.err.
This is why you should try to catch and handle all possible exceptions in the tasks when using a thread pool, to prevent threads from terminating unexpectedly.
import java.util.concurrent.*;
class MyRunnable implements Runnable {
private final String taskName;
MyRunnable(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
System.out.println(taskName + " is running...");
}
}
public class Main {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // corePoolSize
10, // maximumPoolSize
60, // keepAliveTime
TimeUnit.SECONDS, // time unit for keepAliveTime
new LinkedBlockingQueue<>() // workQueue
);
for (int i = 0; i < 15; i++) {
Runnable worker = new MyRunnable("Task " + i);
executor.execute(worker);
}
executor.shutdown();
}
}
In this example, we create a ThreadPoolExecutor with a core pool size of 5 and a maximum pool size of 10. This means that there will always be 5 threads in the pool, but it can grow up to 10 threads if necessary. If there are more than 10 tasks, they will be placed into the LinkedBlockingQueue until a thread becomes available. The keepAliveTime parameter is set to 60 seconds, which means that idle threads will be terminated after 60 seconds if the number of threads is greater than the core pool size.
- corePoolSize: The number of threads to keep in the pool, even if they are idle.
- maximumPoolSize: The maximum number of threads to allow in the pool.
- keepAliveTime: When the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating.
- unit: The time unit for the
keepAliveTime
argument. - workQueue: The queue to use for holding tasks before they are executed. This queue will hold only the
Runnable
tasks submitted by theexecute
method.
Executors
- Executors: This is a utility class that provides several factory methods for creating different kinds of executor services, such as single-threaded executor (
Executors.newSingleThreadExecutor()
), fixed thread pool (Executors.newFixedThreadPool(int n)
), and cached thread pool (Executors.newCachedThreadPool()
). These methods are convenient for simple use cases where you don't need to fine-tune the parameters of the thread pool. Under the hood, these methods useThreadPoolExecutor
to create the executor services. - ThreadPoolExecutor: This is a class that provides a powerful and flexible implementation of the
ExecutorService
interface. It allows you to specify the core and maximum pool sizes, keep-alive time, work queue, and more. This gives you more control over the behavior of the thread pool, which can be useful in more complex scenarios where you need to optimize the performance of the thread pool.
// Executors
ExecutorService executor = Executors.newFixedThreadPool(5);
// ThreadPoolExecutor
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // corePoolSize
10, // maximumPoolSize
60, // keepAliveTime
TimeUnit.SECONDS, // time unit for keepAliveTime
new LinkedBlockingQueue<>() // workQueue
);
Executors is not recommended
The factory methods provided by the Executors class are convenient, but in certain scenarios, they may not provide enough control and could lead to resource overuse, such as OutOfMemoryError (OOM).
Executors.newFixedThreadPool
andExecutors.newSingleThreadExecutor
: Both of these methods use an unboundedLinkedBlockingQueue
. If the number of tasks submitted exceeds the maximum size of the thread pool, tasks will pile up in the queue, potentially leading to an OutOfMemoryError.Executors.newCachedThreadPool
: This method uses aSynchronousQueue
. If the number of tasks submitted far exceeds what the thread pool can handle, the thread pool may create a large number of threads, potentially leading to an OutOfMemoryError.Executors.newScheduledThreadPool
andExecutors.newSingleThreadScheduledExecutor
: Both of these methods use an unboundedDelayedWorkQueue
. If the number of tasks submitted exceeds the maximum size of the thread pool, tasks will pile up in the queue, potentially leading to an OutOfMemoryError.
/*
* These methods are part of the java.util.concurrent.Executors class in the Java standard library.
* They provide a simple way to create thread pools with different behaviors.
* However, as mentioned earlier, they may not provide enough control for more complex scenarios.
*/
// Fixed Thread Pool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
// Single Thread Executor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
// Cached Thread Pool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
// Scheduled Thread Pool
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
// Single Thread Scheduled Executor
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}
Use LinkedBlockingQueue means that the queue can theoretically hold up to Integer.MAX_VALUE tasks. If tasks are submitted faster than they can be processed, and the maximum pool size is reached, additional tasks will be queued in the LinkedBlockingQueue. If the number of queued tasks grows without limit, this could potentially lead to an OutOfMemoryError. If you want to limit the number of tasks that can be queued, you can use a bounded queue instead, such as ArrayBlockingQueue.
new ArrayBlockingQueue<>(100) // queue size is limited to 100
Thread pools
- Fixed Thread Pool (
Executors.newFixedThreadPool(int nThreads)
) : This creates a thread pool that reuses a fixed number of threads operating off a shared unbounded queue. At any point, at mostnThreads
threads will be active processing tasks. If additional tasks are submitted when all threads are active, they will wait in the queue until a thread is available. - Single Thread Executor (
Executors.newSingleThreadExecutor()
) : This creates an executor that uses a single worker thread operating off an unbounded queue. If this single thread terminates due to a failure during execution prior to shutdown, a new one will take its place if needed to execute subsequent tasks. - Cached Thread Pool (
Executors.newCachedThreadPool()
) : This creates a thread pool that creates new threads as needed but will reuse previously constructed threads when they are available. This can improve performance for programs that execute many short-lived asynchronous tasks. Threads that have not been used for sixty seconds are terminated and removed from the cache. - Scheduled Thread Pool (
Executors.newScheduledThreadPool(int corePoolSize)
) : This creates a thread pool that can schedule commands to run after a given delay, or to execute periodically. - Single Thread Scheduled Executor (
Executors.newSingleThreadScheduledExecutor()
) : This creates a single-threaded executor that can schedule commands to run after a given delay, or to execute periodically. - Work Stealing Pool (
Executors.newWorkStealingPool()
) : This creates a work-stealing thread pool using all available processors as its target parallelism level. A work-stealing pool makes no guarantees about the order in which submitted tasks are executed.
Each type of thread pool is suited to a different type of task and workload. The choice of which to use depends on the details of the task being performed.
Thread state
- NEW: The thread has been created but has not yet started (i.e., the
start()
method has not been called). - RUNNABLE: The thread is currently executing in the JVM. It may be waiting for the OS to assign CPU time to it.
- BLOCKED: The thread is currently blocked waiting for a lock to enter a synchronized block/method.
- WAITING: The thread is waiting indefinitely for another thread to perform a particular action. For example, it has called
Object.wait()
on an object and is waiting for another thread to callObject.notify()
orObject.notifyAll()
on that object. - TIMED_WAITING: The thread is waiting for another thread to perform an action for up to a specified waiting time. For example, it has called
Thread.sleep(long millis)
orObject.wait(long millis)
. - TERMINATED: The thread has completed execution or has been stopped because of an exception.
After a thread is created, it is in the NEW state. When the start() method is called, the thread begins to run and enters the READY state. A thread in the READY state that has been allocated CPU time is in the RUNNING state.
At the operating system level, the states of a thread include READY and RUNNING. However, at the JVM level, these two states are both categorized as RUNNABLE. This is because in modern time-sharing, multi-tasking operating systems, the switching of threads is very fast. The time a thread runs on the CPU (i.e., the time it is in the RUNNING state) is typically only a few tens of milliseconds. When a thread's time slice is used up, it is switched out of the CPU and placed in the scheduling queue to wait to be scheduled again (at this time, it is in the READY state). Because this process is so fast, from the JVM's perspective, the thread is almost always in the running state. Therefore, the JVM does not distinguish between the READY and RUNNING states, but categorizes them both as RUNNABLE. This simplifies the JVM's thread state model and is sufficient to meet the needs of most Java programs.
When a thread executes the wait() method, it enters the WAITING state. A thread in the WAITING state relies on notifications from other threads to return to the RUNNING state. The TIMED_WAITING state is essentially the WAITING state with a timeout limit. For example, the sleep(long millis) method or wait(long millis) method can put a thread in the TIMED_WAITING state. When the timeout period ends, the thread will return to the RUNNABLE state.
When a thread enters a synchronized method/block or calls wait() and then re-enters a synchronized method/block (after being notified), but the lock is occupied by other threads, the thread enters the BLOCKED state.
After a thread has finished executing the run() method, it enters the TERMINATED state.
Thread method
- The join() method in Java is used to pause the current thread until the specified thread is dead. When a thread calls join() on another thread, the calling thread goes into a waiting state. It remains in the waiting state until the referenced thread terminates.
/*
* In this example, the main thread starts t1 and then calls t1.join().
* This causes the main thread to pause and wait until t1 finishes its execution.
* Only after t1 is finished does the main thread continue and print "Main thread continues".
*
* So, if join() is called from the main thread,
* it will block the main thread,
* because the main thread is waiting for the completion of the thread on which join() was called.
*/
Thread t1 = new Thread(() -> {
System.out.println("Thread t1 is running");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread t1 is finished");
});
t1.start();
try {
t1.join(); // main thread waits for t1 to finish
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread continues");
- The interrupt() method in Java is used to interrupt a thread's execution. When a thread is interrupted, it doesn't stop immediately. Instead, it's given an "interrupted status" (a boolean flag) which indicates that it's been interrupted.
/*
* In this example, the thread keeps running until it's interrupted.
* When thread.interrupt() is called, the thread's interrupted status is set to true.
* The thread checks this status in its loop condition with Thread.currentThread().isInterrupted(), and stops running when it's interrupted.
*
* Note that if the thread is currently in a state where it's waiting for another thread to complete (like when it's inside a sleep() or join() method),
* it will throw an InterruptedException, and its interrupted status will be cleared.
* If you want to stop the thread at this point, you need to re-interrupt the thread by calling Thread.currentThread().interrupt() inside the catch block.
*/
Thread thread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("Thread is running...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// The interrupted status of the thread is cleared when an InterruptedException is thrown.
// If you want to stop the thread at this point, you need to re-interrupt the thread.
Thread.currentThread().interrupt();
}
}
});
thread.start();
// After 5 seconds, interrupt the thread
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
- The isAlive() method in Java is used to test if a thread is alive. A thread is considered alive if it has been started and has not yet died.
Thread thread = new Thread(() -> {
try {
System.out.println("Thread is running...");
Thread.sleep(5000);
System.out.println("Thread has finished running");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("Before start(): " + thread.isAlive()); // prints: Before start(): false
thread.start();
System.out.println("After start(): " + thread.isAlive()); // prints: After start(): true
// Wait for the thread to finish
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("After join(): " + thread.isAlive()); // prints: After join(): false
ThreadLocal
How to use
ThreadLocal in Java is a class that provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or transaction ID).
/*
* In this example, the ThreadLocal variable threadLocalVar is set to 100 in the main thread and 200 in the new thread.
* When we print the value of threadLocalVar in the main thread, it prints 100, and when we print it in the new thread, it prints 200.
* This shows that the ThreadLocal variable has a separate value for each thread.
*/
public class Example {
// Create a ThreadLocal variable
private static ThreadLocal<Integer> threadLocalVar = new ThreadLocal<>();
public static void main(String[] args) {
// Set the ThreadLocal variable
threadLocalVar.set(100);
Thread thread = new Thread(() -> {
// This will print "null" because the ThreadLocal variable is not set in this thread
System.out.println("ThreadLocal variable in new thread: " + threadLocalVar.get());
// Set the ThreadLocal variable in this thread
threadLocalVar.set(200);
System.out.println("ThreadLocal variable in new thread after setting: " + threadLocalVar.get());
});
thread.start();
// This will print "100" because the ThreadLocal variable was set in the main thread
System.out.println("ThreadLocal variable in main thread: " + threadLocalVar.get());
}
}
How ThreadLocal works
Set
/*
* ThreadLocal class
* The ThreadLocal class in Java uses a ThreadLocalMap to store variables that are specific to the current thread.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
// Use the current threadLocal object as the key, the object you want to save as the value, and store it in the map.
map.set(this, value);
else
createMap(t, value);
}
/*
* ThreadLocal class
* This map is actually bound to Thread
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/*
* Thread class
* ThreadLocal values pertaining to this thread.
* This map is maintained by the ThreadLocal class.
*/
ThreadLocal.ThreadLocalMap threadLocals = null;
Get
/*
* ThreadLocal class
* This method retrieves the value of the thread-local variable for the current thread.
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
Usage
In most cases, ThreadLocal is used to store context information related to a specific thread, which is shared within the same thread. This makes it easy for different methods within the same thread to access this information without having to pass it around as method parameters.
For example, you might use ThreadLocal to store a user ID or transaction ID that needs to be accessed by several methods within the same thread. By storing this information in a ThreadLocal, you can avoid having to pass the user ID or transaction ID to each method. Instead, each method can simply retrieve the value from the ThreadLocal.
This can greatly simplify your code, especially in complex multithreaded applications where passing around context information as method parameters can become cumbersome and error-prone.
Conclusion
The design of ThreadLocal is indeed very clever. It might seem like the ThreadLocal object is storing a copy of each thread's variable, but in reality, these copies are stored within their respective thread objects. This means that each thread has its own copy of the variable, effectively achieving thread isolation.
This design avoids issues with shared variables in a multithreaded environment. Since each thread has its own copy of the ThreadLocal variable, they don't interfere with each other. This is particularly useful in scenarios where thread state needs to be maintained, such as session management in web applications.
Also, since these variable copies are stored within thread objects, they are automatically cleaned up when the thread terminates, which helps prevent memory leaks.