Brief Overview of Java Multithreading
Java multithreading is a core feature of the Java programming language that allows multiple threads of execution to run concurrently in the same program. A thread is essentially a lightweight, independent unit of execution that consists of its own register set, program counter, and stack. By leveraging Java multithreading, developers can perform multiple operations simultaneously to make the most of modern multi-core processors.
In a single-threaded environment, tasks are executed sequentially, one after the other. This can be inefficient and can lead to performance bottlenecks, especially in applications that require real-time processing or high levels of user interactivity. On the other hand, Java multithreading allows tasks to be divided into smaller sub-tasks and executed in parallel, which can lead to faster execution times and more responsive applications.
Java provides built-in support for multithreaded programming through its java.lang.Thread
class and java.lang.Runnable
interface. Additionally, the language offers a rich set of APIs and frameworks, like the Executor framework, Fork/Join framework, and various concurrent collections, to facilitate more advanced multithreading capabilities.
Basics of Threads in Java
In Java multithreading, a thread is the smallest unit of a CPU's execution in a program. It's a separate path of execution that runs independently but can share common resources like memory with other threads in the same application. Multithreading allows you to perform multiple tasks concurrently, making efficient use of system resources.
1. The Main Thread
Every Java application starts with a single thread known as the main thread. This is the entry point for your application and is responsible for executing the main()
method. It's possible to manipulate the main thread using the Thread
class, though it's generally best to leave it to manage the application's startup and shutdown processes.
public class MainThreadExample {
public static void main(String[] args) {
Thread mainThread = Thread.currentThread();
// Display the main thread
System.out.println("Current Thread: " + mainThread);
}
}
2. Creating Threads by Extending the Thread
Class
One way to create a new thread in Java is by extending the Thread class and overriding its run() method. The run()
method contains the code that constitutes the new thread.
class MyThread extends Thread {
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getId() + " Value " + i);
}
}
}
public class Example {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start();
MyThread t2 = new MyThread();
t2.start();
}
}
3. Creating Threads by Implementing the Runnable
Interface
An alternative approach to creating threads is to implement the Runnable
interface and override its run()
method. Unlike extending the Thread
class, implementing Runnable
allows your class to extend other classes as well, since Java doesn't support multiple inheritance.
class MyRunnable implements Runnable {
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getId() + " Value " + i);
}
}
}
public class RunnableExample {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable());
t1.start();
Thread t2 = new Thread(new MyRunnable());
t2.start();
}
}
Thread Life Cycle in Java
Understanding the life cycle of a thread is crucial for effective Java multithreading. A thread goes through various states during its lifetime, from its creation to its termination. Below are the primary states in a thread's life cycle:
1. New
When a thread is created using the new
keyword, it's in the "New" state. At this point, the thread is not yet alive, and its start()
method has not been called.
Thread t1 = new Thread(); // New state
2. Runnable
After the start()
method is invoked, the thread transitions to the "Runnable" state. In this state, the thread becomes eligible to be picked up by the scheduler for execution. Note that being in the runnable state doesn't necessarily mean the thread is currently executing.
t1.start(); // Runnable state
3. Running
When the thread scheduler picks the thread from the runnable pool, the thread starts executing its run()
method. At this point, it's in the "Running" state. Only one thread can be in this state per CPU core, as it's actually using CPU resources.
// Inside run() method
public void run() {
// Code here is in the Running state
}
4. Blocked
A thread enters the "Blocked" state when it is temporarily unable to proceed with its execution due to external factors, like waiting for a resource to become available or for a lock to be released. It will move back to the "Runnable" state as soon as the blocking condition is resolved.
// Thread is blocked when waiting for a lock
synchronized(myObject) {
// Code here
}
5. Terminated
Once the thread completes the execution of its run()
method, or if it is explicitly stopped via any other means (though forcefully stopping threads is generally discouraged), it transitions to the "Terminated" state. A terminated thread cannot be restarted.
// Thread will be terminated after run() method finishes
public void run() {
// Some code
}
Thread Priority
Thread priority is a fundamental concept in Java multithreading that allows you to control the order in which threads are scheduled for execution. The thread scheduler uses these priorities as hints to decide which thread to execute first. However, it's essential to note that thread priorities are not guaranteed to work as expected on all JVMs or operating systems.
1. Priority Range in Java
In Java, thread priorities are integer values that range from 1 to 10, with three predefined constants in the Thread
class:
Thread.MIN_PRIORITY
: 1Thread.NORM_PRIORITY
: 5 (default priority)Thread.MAX_PRIORITY
: 10
2. How to Set and Get Thread Priorities
You can set a thread's priority using the setPriority(int priority)
method and retrieve it using the getPriority()
method.
Thread t1 = new Thread();
t1.setPriority(Thread.MAX_PRIORITY); // Set priority to 10
int p = t1.getPriority(); // Get priority
3. Impact of Thread Priorities on Execution
Thread priority can influence the scheduler's behavior, but it's not a guarantee. Threads with higher priority are generally more likely to be chosen for execution before lower-priority threads, but this can vary depending on the underlying operating system and JVM.
class MyRunnable implements Runnable {
public void run() {
System.out.println(Thread.currentThread().getName() + " Priority: " + Thread.currentThread().getPriority());
}
}
public class PriorityExample {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable(), "Thread-1");
Thread t2 = new Thread(new MyRunnable(), "Thread-2");
// Setting priorities
t1.setPriority(Thread.MIN_PRIORITY); // 1
t2.setPriority(Thread.MAX_PRIORITY); // 10
// Starting threads
t1.start();
t2.start();
}
}
In the above example, it's more likely that Thread-2
with a higher priority will be executed before Thread-1
, but it's not a guarantee.
Thread Synchronization
Thread synchronization is a pivotal concept in Java multithreading that ensures that threads access shared resources in a manner that maintains data integrity and consistency. It prevents issues like race conditions, deadlocks, and thread interference, which can cause unexpected behavior and bugs.
In a multithreaded environment, multiple threads often share resources like variables, data structures, or external systems. Without proper synchronization, one thread might modify a resource while another is reading it, leading to inconsistencies and errors.
1. Synchronized Method
You can declare a method as synchronized by using the synchronized
keyword. When a method is synchronized, only one thread can access it at a time for a given object instance.
public synchronized void mySynchronizedMethod() {
// Critical section
}
To demonstrate, consider a counter class:
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
2. Synchronized Block
In cases where you only need to synchronize a part of a method, you can use a synchronized block instead of synchronizing the entire method. This reduces the scope of synchronization, thus potentially improving performance.
public void myMethod() {
// Some non-critical section code
synchronized(this) {
// Critical section
}
// More non-critical section code
}
3. Static Synchronization
When a synchronized method is static, the lock is held on the class object, not on an individual instance. This is useful when you want to synchronize access to class-level variables.
public class MyStaticClass {
private static int count = 0;
public static synchronized void increment() {
count++;
}
}
Or using a synchronized block for a static method:
public class MyStaticClass {
private static int count = 0;
public static void increment() {
synchronized(MyStaticClass.class) {
count++;
}
}
}
Inter-thread Communication
Inter-thread communication is crucial for coordinating the actions of multiple threads in a Java multithreading environment. Java provides built-in mechanisms such as wait()
, notify()
, and notifyAll()
methods to facilitate this communication.
1. wait()
, notify()
, and notifyAll()
Methods
wait()
: Makes the current thread wait until another thread invokesnotify()
ornotifyAll()
for the same object.notify()
: Wakes up a single waiting thread for the given object.notifyAll()
: Wakes up all waiting threads for the given object.
class SharedResource {
private int data;
public synchronized void produce(int value) {
data = value;
System.out.println("Produced: " + data);
notify();
}
public synchronized void consume() {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Consumed: " + data);
}
}
2. Producer-Consumer Problem
The producer-consumer problem is a classic example that demonstrates inter-thread communication. Producers create data and consumers consume it, both operating on a shared resource.
public class Main {
public static void main(String[] args) {
SharedResource resource = new SharedResource();
Thread producer = new Thread(() -> {
for (int i = 0; i < 5; i++) {
resource.produce(i);
}
});
Thread consumer = new Thread(() -> {
for (int i = 0; i < 5; i++) {
resource.consume();
}
});
producer.start();
consumer.start();
}
}
3. Deadlock Situation and How to Prevent It
A deadlock in Java multithreading occurs when two or more threads wait for a set of resources that are held by each other, creating a cycle of dependencies that can never be broken.
How to Prevent Deadlock:
- Lock Ordering: Always acquire the locks in the same order.
- Timeout: Try to acquire the lock, but give up if it takes too long.
- Deadlock Detection: Continuously monitor and analyze resource allocation graphs to detect cycles.
public class DeadlockExample {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void method1() {
synchronized (lock1) {
synchronized (lock2) {
// Critical section
}
}
}
public void method2() {
synchronized (lock2) {
synchronized (lock1) {
// Critical section
}
}
}
}
In this example, if method1
and method2
are invoked by two different threads simultaneously, a deadlock can occur.
Thread Pooling
Thread pooling is a technique used in Java multithreading to reuse existing threads instead of creating new ones for every task. It's an efficient way to manage resources, especially for systems that execute numerous short-lived asynchronous tasks.
In a thread pool, a fixed or dynamic number of threads are created and stored in a pool, waiting to be assigned tasks. Once a task is complete, the thread returns to the pool, making it available for future tasks. This approach minimizes the overhead of thread creation and destruction.
Java provides the Executor framework, which abstracts thread pool management through classes like ThreadPoolExecutor and interfaces like Executor and ExecutorService.
Here is a simple example that demonstrates how to use the ExecutorService
to manage a thread pool.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5); // Pool of 5 threads
for (int i = 0; i < 10; i++) {
Runnable worker = new MyRunnable(i);
executorService.execute(worker);
}
executorService.shutdown();
while (!executorService.isTerminated()) {
}
System.out.println("Finished all threads");
}
}
class MyRunnable implements Runnable {
private int taskId;
public MyRunnable(int id) {
this.taskId = id;
}
@Override
public void run() {
System.out.println("Task ID : " + this.taskId + " performed by " + Thread.currentThread().getName());
}
}
Concurrent Collections
Java multithreading has made concurrent programming more accessible and more efficient through the use of Concurrent Collections. These collections are designed to allow multiple threads to read and write data simultaneously, while maintaining thread safety.
1. ConcurrentHashMap
ConcurrentHashMap
is a thread-safe alternative to HashMap
. It allows multiple threads to read and modify the map concurrently, without compromising data integrity or consistency.
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// Adding elements
map.put("one", 1);
map.put("two", 2);
// Retrieving an element
int value = map.get("one");
}
}
2. CopyOnWriteArrayList
CopyOnWriteArrayList
is a thread-safe variant of ArrayList
. When the list is modified, a new copy is created, making it ideal for scenarios where read operations far outnumber write operations.
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteArrayListExample {
public static void main(String[] args) {
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
// Adding elements
list.add(1);
list.add(2);
// Reading elements
for (int item : list) {
System.out.println(item);
}
}
}
3. BlockingQueue
BlockingQueue
is an interface that represents a thread-safe queue with blocking operations. It is useful for implementing producer-consumer problems.
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueExample {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
// Adding elements
try {
queue.put(1);
queue.put(2);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// Retrieving elements
try {
int item = queue.take();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
The Fork/Join Framework
The Fork/Join Framework is an integral part of Java's multithreading capabilities, introduced in Java 7. It provides a way to break down complex tasks into smaller sub-tasks that can be processed in parallel, improving performance and resource utilization.
The Fork/Join Framework is designed to help take advantage of multi-core processors. It employs a divide-and-conquer algorithm to break complex tasks into smaller and more manageable tasks, which are then executed by worker threads from a thread pool. This results in faster computation by fully leveraging the multi-threading capabilities of modern CPUs.
The Fork/Join Framework is primarily composed of the following classes:
ForkJoinPool
: Manages the worker threads that asynchronously execute the tasks.ForkJoinTask
: Represents a task that can be executed within aForkJoinPool
.RecursiveTask
: ExtendsForkJoinTask
for tasks returning a result.RecursiveAction
: ExtendsForkJoinTask
for tasks that do not return a result.
Here is an example of calculating a Fibonacci number using the RecursiveTask
class.
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
public class Fibonacci extends RecursiveTask<Integer> {
final int n;
public Fibonacci(int n) {
this.n = n;
}
@Override
protected Integer compute() {
if (n <= 1) {
return n;
}
Fibonacci f1 = new Fibonacci(n - 1);
f1.fork();
Fibonacci f2 = new Fibonacci(n - 2);
return f2.compute() + f1.join();
}
public static void main(String[] args) {
ForkJoinPool forkJoinPool = new ForkJoinPool();
Fibonacci fibonacci = new Fibonacci(10);
int result = forkJoinPool.invoke(fibonacci);
System.out.println("Fibonacci number at position 10 is: " + result);
}
}
In this example, the Fibonacci series is calculated using recursive tasks. The ForkJoinPool
is used to execute these tasks concurrently.
Modern Concurrency with Java Multithreading
Java has evolved to offer modern concurrency mechanisms that make it easier for developers to write efficient and maintainable multi-threaded code. These include features like CompletableFuture
, CountDownLatch
, CyclicBarrier
, and Semaphore
.
1. CompletableFuture
CompletableFuture
is a class introduced in Java 8 that represents a future result of an asynchronous computation, allowing you to write asynchronous, non-blocking, and more readable code.
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExample {
public static void main(String[] args) {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 5)
.thenApplyAsync(result -> result * 2);
future.thenAccept(result -> System.out.println("Result is: " + result));
}
}
2. CountDownLatch
CountDownLatch
is a synchronization aid that allows threads to wait until a set of operations is completed.
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
new Thread(() -> { latch.countDown(); }).start();
new Thread(() -> { latch.countDown(); }).start();
new Thread(() -> { latch.countDown(); }).start();
latch.await(); // Wait until count reaches zero
System.out.println("All threads have finished.");
}
}
3. CyclicBarrier
CyclicBarrier
is similar to CountDownLatch
but it can be reused, making it suitable for scenarios where a group of threads must wait for each other multiple times.
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("Barrier action executed"));
new Thread(() -> {
try { barrier.await(); } catch (Exception e) { Thread.currentThread().interrupt(); }
}).start();
new Thread(() -> {
try { barrier.await(); } catch (Exception e) { Thread.currentThread().interrupt(); }
}).start();
new Thread(() -> {
try { barrier.await(); } catch (Exception e) { Thread.currentThread().interrupt(); }
}).start();
}
}
4. Semaphore
Semaphore
is used to control the number of threads that can access a particular resource at a given time.
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(2); // Allow two threads to access the resource at once
new Thread(() -> {
try {
semaphore.acquire();
// Critical section
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release();
}
}).start();
}
}
Advanced Topics
Advanced users often encounter complex problems that require specialized solutions. Java multithreading provides several features to deal with such scenarios, such as ThreadLocal
variables, daemon threads, thread dump analysis, and thread safety in singleton classes.
1. ThreadLocal Variables
ThreadLocal
allows you to create variables that can only be read and written by the same thread, thus making them thread-safe.
public class ThreadLocalExample {
private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
threadLocal.set(42);
System.out.println("Value: " + threadLocal.get()); // Outputs 42
}
}
2. Daemon Threads
Daemon threads are threads that run in the background and do not prevent the JVM from exiting.
public class DaemonThreadExample {
public static void main(String[] args) {
Thread daemonThread = new Thread(() -> {
while (true) {
// Background work
}
});
daemonThread.setDaemon(true);
daemonThread.start();
}
}
3. Thread Dump Analysis
Thread dumps are crucial for debugging and understanding the state of threads at any given point in time. Various tools like jstack
can generate thread dumps for analysis.
To generate a thread dump, you can run:
jstack <pid>
Replace <pid>
with the process ID of your Java application.
4. Thread Safety in Singleton Classes
Ensuring thread safety in singleton classes is critical for avoiding inconsistencies.
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
Performance Considerations in Java Multithreading
Multithreading in Java is a powerful tool for boosting application performance by allowing multiple threads to execute simultaneously. However, this doesn't mean that adding more threads will always make your application faster. Below, let's consider some of the performance implications of using multithreading.
1. CPU and Memory Overhead
Example 1: Using multiple threads to write to a shared resource without synchronization.
public class SharedResourceExample {
private static int sharedResource = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
sharedResource++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
sharedResource++;
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Shared resource value: " + sharedResource);
}
}
Output:
Shared resource value: 13749
The shared resource value will likely not be 20000 because both threads are incrementing the shared resource simultaneously without any synchronization, causing a race condition. This results in poor performance and incorrect output.
2. Scalability Concerns
Example 2: Creating too many threads that perform a small task.
public class ScalabilityExample {
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
new Thread(() -> System.out.print("")).start();
}
}
}
Creating too many threads for trivial tasks can be counter-productive. Each thread comes with overhead for context switching, and excessive threads could lead to poor scalability and resource exhaustion.
3. Avoiding Deadlocks and Starvation
Example 3: Using synchronized methods but in a way that leads to deadlock.
public class DeadlockExample {
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock1) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Thread 1 Acquired Locks");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock2) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("Thread 2 Acquired Locks");
}
}
});
t1.start();
t2.start();
}
}
Both threads t1
and t2
are stuck waiting for each other to release locks. This is a classic example of a deadlock scenario, which negatively impacts performance and could halt your application.
Frequently Asked Questions on Java Multithreading
What is multithreading in Java?
Multithreading in Java is a feature that allows multiple threads of execution to run concurrently in the same program. It helps improve the efficiency and performance of applications by better utilizing system resources.
How do I create a thread in Java?
You can create a thread in Java by either extending the Thread
class or implementing the Runnable
interface. Once you have a class that extends Thread
or implements Runnable
, you can create an object of that class and call its start()
method to execute the thread.
What is the life cycle of a thread in Java?
The life cycle of a thread in Java consists of several states: New, Runnable, Running, Blocked, and Terminated.
How do thread priorities affect execution?
Thread priorities in Java range from 1 to 10, with 1 being the lowest and 10 being the highest. Threads with higher priority are more likely to be scheduled for execution by the Java Virtual Machine, but it's not a guarantee.
What is thread synchronization?
Thread synchronization ensures that threads do not interfere with each other while sharing resources. It can be achieved using synchronized methods, synchronized blocks, and static synchronization.
How can threads communicate with each other?
Inter-thread communication can be achieved using methods like wait()
, notify()
, and notifyAll()
. These methods allow threads to coordinate and share resources efficiently.
What is a thread pool?
A thread pool is a set of pre-initialized threads that can be used to execute tasks. The Executor
framework in Java provides built-in support for creating and managing thread pools.
What are concurrent collections?
Concurrent collections like ConcurrentHashMap
, CopyOnWriteArrayList
, and BlockingQueue
are thread-safe variants of traditional collections, designed to be used in multi-threaded environments.
What is the Fork/Join Framework?
The Fork/Join Framework is a feature in Java that helps in parallelizing tasks to take advantage of multi-core processors. It employs a divide-and-conquer approach to break complex tasks into smaller sub-tasks.
What are modern concurrency utilities in Java?
Modern concurrency utilities include features like CompletableFuture
, CountDownLatch
, CyclicBarrier
, and Semaphore
, which offer more robust and maintainable ways to handle concurrent programming challenges.
How can I analyze a thread dump?
You can analyze a thread dump using tools like jstack
. The thread dump provides detailed information on the state of all the threads at a particular moment, helping in debugging and performance tuning.
What are daemon threads and how are they different from user threads?
Daemon threads are background threads that do not prevent the JVM from exiting. User threads, on the other hand, can keep the JVM running even if all daemon threads have completed execution.
How can I make a singleton class thread-safe?
To make a singleton class thread-safe, you can use mechanisms like lazy initialization with double-checked locking, using the volatile
keyword, or by using the Bill Pugh Singleton design using a private static inner class to hold the singleton instance.
Summary
Java multithreading offers a comprehensive set of features to manage concurrent programming effectively, ranging from basic thread creation and synchronization to modern concurrency utilities and advanced topics. Whether you're a beginner or an experienced professional, understanding these features can aid in writing efficient, robust, and maintainable code.
- Basics of Threads: Learn how to create and manage threads using the
Thread
class andRunnable
interface. - Thread Life Cycle: Understand the various states a thread can be in and how the Java Virtual Machine manages them.
- Thread Priority: Explore how to set and retrieve thread priorities.
- Thread Synchronization: Master synchronization techniques like synchronized methods, blocks, and static synchronization.
- Inter-Thread Communication: Get to grips with methods like
wait()
,notify()
, andnotifyAll()
. - Thread Pooling: Utilize the
Executor
framework for efficient thread management. - Concurrent Collections: Use thread-safe collections like
ConcurrentHashMap
,CopyOnWriteArrayList
, andBlockingQueue
. - Fork/Join Framework: Understand the divide-and-conquer approach for parallel processing.
- Modern Concurrency Utilities: Learn about
CompletableFuture
,CountDownLatch
,CyclicBarrier
, andSemaphore
. - Advanced Topics: Delve into
ThreadLocal
, daemon threads, thread dump analysis, and thread safety in singleton classes. - FAQs: A handy section answering common questions related to Java multithreading.
Additional Resources
- Oracle's Java Tutorials: Oracle provides official tutorials that cover various aspects of Java, including multithreading. Oracle Java Tutorial on Concurrency
- Java API Documentation: The API documentation provides detailed information about the Thread class and the Runnable interface, among other things related to multithreading. Java API Documentation for Thread
- Java Language Specification: The Java Language Specification will provide the most detailed and formal explanation of how threading works in Java. Java Language Specification - Threads and Locks
- OpenJDK: OpenJDK is the official reference implementation of the Java Platform, Standard Edition. It's a good resource for looking at the actual code that implements Java's multithreading capabilities. OpenJDK Source Code