Have you ever thought about why some Java applications are able to handle and execute multiple tasks at the same time without any lag or crash? Well, the answer lies in the multithreading concept of Java. It is a technique that typically enables concurrent execution, which leads to better performance of the Java applications. But there are also problems like incorrect thread synchronization, which can lead to deadlocks, and race conditions, so how would you avoid them?
With this Java Multithreading Tutorial, we will discuss everything about multithreading, from thread creations, lifecycle of threads, and synchronizations, to advanced concepts like deadlocks, interthread communication, performance optimization, and many more.
So without any further delay, let’s learn multithreading in Java
Table of Contents:
What is Multithreading in Java?
Multithreading in Java is a programming technique where multiple threads are capable of running concurrently in a single process. It generally allows the programs to perform multiple tasks at the same time, which leads to improved performance, better CPU utilization, and enhanced responsiveness.
Generally, Java divides the program into multiple threads that simply enable the parallel execution of the program, which can handle tasks like handling user input, background computations, and managing I/O operations.
Key Benefits of Multithreading
- Better CPU Utilization: Multiple Java threads share CPU time, which reduces the idle time.
- Faster Execution: Tasks in Multithreading run concurrently rather than sequentially.
- Improvement in Responsiveness: It also prevents UI freezing in application programs.
- Shared Memory Resource Economy: Threads share space that helps to minimize overhead.
Multiprocessing vs Multithreading in Java
Multithreading and multiprocessing are used for parallel execution in Java but are very different in how they use system resources. It is good to be aware of them in order to use them for performance improvement in a suitable manner.
Multiprocessing means when multiple processes (individual programs) are executed simultaneously. Each process consists of a distinct space for memory, time for CPU, and execution flow and hence are completely independent processes, whereas Multithreading allows for multiple threads running in a particular process and accessing a shared space for memory. It can be used in parallel programming for handling multiple user requests in a web server.
Example of Multiprocessing in Java:
- Creates a new independent process (notepad.exe) outside of the Java program.
- Each process has its own memory and resources.
Example of Multithreading in Java:
Output:

- Creates multiple threads in one process, and they use shared memory.
- Faster and more efficient for light tasks.
What is a Thread?
The smallest unit of execution in a particular process is termed a thread. A thread in Java is independent but can share resources and space in memory with another thread in a given process. Some key characteristics of threads are:
- Low-memory usage: Threads typically consume much less memory than a regular process.
- Independent Execution: Every thread has its own execution paths.
- Shared Resources: Threads share space in heap but have independent space in stack memory.
Single-threaded vs Multi-threaded Programs
- Single-threaded Program: Executes any tasks in a sequential manner, that leads to slower performance.
- Multithreaded Program: Executes multiple threads simultaneously, making it memory efficient.
Java typically has built-in support for using threads and is consequently a highly efficient concurrent programming tool.
Thread Creation in Java
Java provides multiple mechanisms for creating and running threads for efficient concurrent execution and multithreading. Three primary techniques are:
1. Using the Thread Class
The easiest way to start a thread is to use the Thread class and override the run() method. It generally provides direct access to the Thread methods but is limited in flexibility as there is no support for multiple inheritance in Java.
Code Example:
Output:

- MyThread is a class extending Thread: MyThread thus becomes a separate thread.
- Override run method: It is used for specifying the activity to be executed in another thread.
- start() method: It internally calls run() but runs it in another call stack, thus allowing for parallel execution.
2. Using the Runnable Interface Implementation
A second option is to use the Runnable interface. It is a more flexible alternative than inheriting a Thread class since it allows for inheriting other classes and still being able to use multiple threads. It also promotes better reusability and separation of concerns.
Code Example:
Output:

- Uses Runnable instead of inheriting from Thread because it provides more flexibility.
- Passing Runnable object in Thread constructor: The given Runnable object is executed in the run() method.
- Invokes start() method: Causes the thread to execute independently from the main program execution flow.
3. Using Callable and Future (For Returning Values)
Unlike Runnable, in which no result comes out, the Callable interface generally allows a thread to return a value and throw checked exceptions. It is useful when we require a result after we execute a thread, such as in parallel computation, asynchronous programming, or when making API requests.
Code Example:
Output:

- Implements Callable<String>: Allows for returning a value (in this case a String).
- Uses ExecutorService: Controls execution of threads rather than creating a direct Thread object.
- calls submit() method: It submits the callable object for execution in another thread.
- Uses future.get(): Pauses the main program execution until the result is obtained.
- shutdown(): Shutdowns the thread pool after execution in a suitable way.
Thread Class vs Runnable Interface
Feature |
Thread Class |
Runnable Interface |
Inheritance |
Extends Thread, restricting further inheritance. |
Implements Runnable, allowing multiple inheritance. |
Flexibility |
Tightly couples thread logic with class. |
Decouples thread logic, making it reusable. |
Object Creation |
Directly creates and starts a thread. |
Requires a Thread object to run. |
Memory Usage |
More, as each thread has a separate object. |
Less, as multiple threads can share a Runnable instance. |
Best For |
Simple, independent threads. |
Scalable, multi-threaded applications. |
Tip: Use Runnable for better reusability and flexibility.
Priorities in Threads
Every thread in Java typically has a priority that simply helps the thread scheduler to allocate CPU time among multiple different threads. The actual execution may be different based on the JVM and OS, but in general, threads with higher priorities receive a larger share of CPU time than lower-priority threads.
Understanding Thread Priorities
Thread priorities in Java are represented as integer values in the range between 1 (MIN_PRIORITY) and 10 (MAX_PRIORITY). The default priority assigned to a thread is 5 (NORM_PRIORITY).
Priority Constants in the Thread Class
Constant |
Value |
Description |
Thread.MIN_PRIORITY |
1 |
Lowest priority |
Thread.NORM_PRIORITY |
5 |
Default priority |
Thread.MAX_PRIORITY |
10 |
Highest priority |
Even if threads are given a higher level of priority, there is no surety about when or if they will be executed since it totally depends on the underlying OS thread scheduler.
Setting and Getting Thread Priorities
Example: Setting and Getting Thread Priorities
Output:

Code Explanation:
- setPriority(int priority): Sets a given priority for a thread (1-10).
- getPriority(): Retrieves the current priority for this thread.
- start() method: Initiates running the thread.
- Thread names and priorities are printed out for following the ordering in execution.
Does Priority Guarantee Execution Order?
No. The threads are not guaranteed to be executed in a certain order based on priorities. The OS-level threads system is utilized in Java and can override priorities in some cases.
For example, in some systems:
- Even a high-priority thread can fail to execute if the CPU is busy in some other process.
- A lower-priority thread may be running instead if a higher-priority thread is blocked for some resource.
Thus, while priorities in threads may have some influence on execution, priorities should not be relied upon for synchronizing threads. Locking and wait/notify mechanisms should be used for managing threads efficiently.
Lifecycle of a Thread in Java
A Java thread goes through a number of stages in its lifecycle during the execution time. The stages are defined in the enum Thread. State and are used in understanding how a thread transitions from being created to being finished.
Thread States and Transitions
Java threads follow the lifecycle New -> Runnable -> Running -> Blocked/Waiting -> Terminated.

- New (Created) State
- A new state is when a thread is created but has not yet started running.
- It generally remains in this state until the start() method is called.
Thread t = new Thread(); // Thread is in New state
- Runnable State
- Upon calling start(), the thread is shifted to Runnable state.
- The thread is eligible for being executed but is chosen for running by the CPU scheduler.

- Running State
- A thread is in the Running state when it is being executed by the CPU.
- The Runnable transitions to Running depend entirely on the CPU scheduler.
Example:
- Blocked / Waiting / Timed Waiting State
A running thread can continue to:
- Blocked: While waiting for a blocked resource (i.e., in a synchronized block).
- Waiting: It waits indefinitely using wait().
- Timed waiting: It waits for some time using sleep() or join().
Example (Blocked State):
- Terminated (Dead) State
- A thread is in the Terminated state when it is finished running or is stopped using stop() method (deprecated).
- Once terminated, a thread cannot be restarted.
Example:
These concepts are very important for efficient multithreading and synchronizing threads in programs in Java.
Thread Methods in Java
Java provides a number of inbuilt methods in the Thread class for managing and controlling threads for execution. These methods are utilized for efficient starting, pausing, running, and stopping threads.
Method |
Description |
start() |
Begins the execution of a thread. Calls the run() method internally. |
run() |
Defines the code that the thread executes. |
sleep(ms) |
Pauses the thread for a specified time (in milliseconds). |
join() |
Forces the current thread to wait until another thread completes execution. |
yield() |
Suggests the scheduler to pause the current thread and allow other threads to execute. |
interrupt() |
Interrupts a sleeping or waiting thread. |
isAlive() |
Check if a thread is still running. |
setPriority(int priority) |
Sets the priority of a thread. |
getPriority() |
Returns the priority of a thread. |
setName(String name) |
Assign a name to a thread. |
getName() |
Retrieves the thread’s name. |
Thread Operations in Java
Thread operations in Java refer to the various actions that can be performed on threads to control their execution. These include:
- Starting a thread using start()
- Pausing execution using sleep()
- Waiting for completion using join()
- Stopping or interrupting a thread using interrupt()
- Verifying status of threads using isAlive() and isInterrupted()
1. Starting a Thread
start() method in Java is used to execute a new thread. It internally calls the run() method of the thread but in a separate path of execution.
Example:
Output:

- Creates a new thread instead of running run() in the main thread.
- Enables concurrent execution.
2. Pausing a Thread
sleep() method makes the current thread wait for a period without releasing the underlying lock..
Example:
Output:

- This allows for other threads to be executed while the current thread sleeps.
- Useful for polling or background processes for rate-limited operations.
3. Waiting for a Thread to Finish
The join() method makes the current thread wait for another running thread to complete the execution.
Example:
Output:

- Guarantees sequential execution in instances in which thread completion is required before proceeding further.
- Frequent in dependency-based tasks (i.e., waiting for a database query to finish before proceeding to process information).
4. Stopping a Thread
Java does not have a direct way to stop a running thread but can use interrupt() method to request a running thread to stop. The running thread must be checked for interruption explicitly using isInterrupted() or catch InterruptedException.
Example:
Output:

- More secure than stop()(deprecated) as it allows for graceful shutting down.
- Useful for interrupting extended processes (i.e., network requests, computations).
5. Confirming Thread Status
IsAlive() method is generally used to know if a given thread is running or dead.
Example:
Output:

Useful for debugging and for observing active threads.
6. Stopping a Thread Suddenly (Not Preferred)
The stop() method in multithreading forcefully stops a thread and may produce resource leaks and unpredictable state conditions. The method is now deprecated in Java as it is unsafe.
Thread Synchronization in Java
Thread synchronization is a programming method used in multiple threads for synchronizing access to a shared resource. It ensures a block of code is executed once while preventing race conditions, resource corruption, and unpredictable behavior.
Why is Synchronization Needed?
In a system based on multiple threads, multiple threads can concurrently read and write to shared data, leading to:
- Race conditions: Where there are multiple threads updating some common information concurrently, resulting in unpredictable behavior.
- Data inconsistencies: Incomplete or corrupted information resulting from incorrect access.
- Thread interference: When one thread modifies data before another thread completes its operation.
Example of a Race Condition (Without Synchronization)
Output:

Problem: Since count++ is read-modify-write (non-atomic) in nature, threads can interfere and give incorrect values.
How to Perform Synchronization in Java
Now Let’s discuss every method to achieve smoother synchronization in Java:
1. Synchronized Methods
Using the synchronized method makes sure there is only a single thread capable of running it at a time. If another thread is accessing the method while being accessed, it will be blocked until that method gets free.
Example: Using a Synchronized Method
Output:

- Guarantees thread safety since there can be at most one thread running increment() at once.
- Simple to use but can be performance-intensive if used excessively.
2. Synchronized Blocks
Instead of synchronizing the entire method, synchronizing just the critical section of code can be done, which generally helps to reduce the performance overhead.
Example: Using a Synchronized Block
- More efficient than synchronized methods as only the critical section is being protected.
- Guarantees consistency in information and makes other operations non-blocking.
3. Static Synchronization (Synchronizing Static Methods)
Use synchronized static methods when there are multiple threads accessing a shared static resource.
Example: Using Static Synchronization
- It ensures synchronization between all the instances of the class.
- Prevents concurrent modification of static variables in different threads.
4. Using Lock (ReentrantLock)
Java supports direct locks with the use of java.util.concurrent.locks.Lock, which generally offers greater control than a synchronized one.
Advantages of Using ReentrantLock:
- Offers a tryLock(), which does not block if it is already held.
- Promotes policies for equity, offering long waiting threads early access.
- Facilitates explicit unlocking, without deadlocks.
Example: Using ReentrantLock
- More flexible and expandable than synchronized.
- Useful in complex locking situations when finer control is needed.
5. Using Atomic Variables (Avoid Synchronization Overhead)
Java.util.concurrent.atomic classes can be used for basic numeric operations on shared variables instead of locks.
Benefits of Atomic Variables:
- No explicit synchronization is needed, avoiding blocking overhead.
- Quicker and more efficient for things like incrementing a counter.
Example: Using AtomicInteger
- Thread-safe without locks and minimizing contention.
- Ideal for counters, flags, and accumulators.
6. Using Semaphore for Thread Synchronization
A Semaphore in java.util.concurrent is used to restrict the number of threads accessing a resource simultaneously.
Example: Using Semaphore
Output:

- Handles multiple accesses simultaneously.
- Avoids resource exhaustion in highly loaded conditions.
Key Points about Synchronization Methods
- Use synchronized for simple thread safety but avoid unnecessary locking.
- Use synchronized blocks instead of entire methods for better performance.
- Use ReentrantLock for complex locking situations like fairness policies.
- Use AtomicInteger for counts and flags in order to prevent block overhead.
- Use Semaphore to control concurrent use of limited resources.
Inter-thread Communication in Java
Java inter-thread communication is typically referred to as the collaboration between different threads accessing common resources. Instead of going for CPU-intensive processes like polling continuously, threads efficiently communicate and share information using inbuilt methods like wait(), notify(), and notifyAll().
Such a mechanism helps threads to simply work together since it causes one thread to temporarily pause the executions and wait for another signal when it is complete.
How to Implement Inter-Thread Communication in Java
1. Using wait(), notify(), and notifyAll()
Java provides three primary Object class methods for inter-thread communication:
- Wait(): Releases the lock and makes the current thread go into a waiting state.
- notify(): It wakes up a waiting thread.
- notifyAll(): Wakes up all waiting threads for the object
Example: Producer-Consumer Problem Using wait() and notify()
In this example, the consumer and producer threads place and remove an object in and out of the buffer, and for interprocess communication, they use wait() and notify().
Code Implementation:
Output:

How Does It Work?
- The producer thread typically generates data and sends that generated data to the SharedBuffer. It will be blocked (wait()) when the buffer is full.
- Consumer thread removes the data. It waits (wait()) when the buffer runs out.
- Once the producer is producing, it calls notify() to signal to the consumer to wake up.
- Similarly, when a consumer consumes, it calls notify() to alert the producer to wake up.
2. By BlockingQueue (Recommended in modern Java)
Instead of managing wait() and notify() programmatically, Java’s BlockingQueue does this inter-thread communication automatically.
Example: Producer-Consumer Using BlockingQueue
Output:

Why Use BlockingQueue Instead of wait() and notify()?
- Simpler and safer: Not required to use wait() or notify() explicitly.
- Handles waiting automatically: The consumer will wait if it is empty and the producer will wait if the queue is full.
- More readable and maintainable.
3. Using PipedInputStream and PipedOutputStream for Inter-thread Communication
Java’s PipedInputStream and PipedOutputStream allow the data to be passed between different threads directly using the pipes.
Example: Using Piped Streams
Output:

- Useful for inter-thread low-level data streaming.
- Threads work in sync, as the consumer reads when and as the producer writes.
Thread Deadlock in Java
Thread deadlock occurs when there are at least two threads waiting indefinitely for each other to release some resource. It is a situation in which no threads can proceed and thereby causes a program to hang.
Deadlocks in multithreading are caused when threads are holding some resource and waiting for another resource simultaneously. A deadlock is inevitable if there is circular waiting.
Example of Thread Deadlock in Java:
Output:

Explanation:
- Thread-1 locks Resource r1 and waits to lock r2.
- Thread-2 locks Resource r2 and waits to lock r1.
- They are awaiting each other to release their locks and are in a state of deadlock.
How to Prevent Deadlock in Java?
- Avoid nested locks: Always try to minimize locking multiple resources inside any synchronized blocks.
- Use a Consistent Locking Order: Always acquire locks in a consistent order in all threads.
- Use tryLock() instead of synchronized, as it does not block indefinitely if a lock is not available.
- Set Lock Timeout: Avoid deadlocks by limiting the waiting time for acquiring a lock.
Atomic and Volatile Keywords in Java
Data inconsistency and race conditions can be caused in multithreading when multiple threads are accessing and modifying shared data simultaneously. Multithreading in Java allows for volatile and atomic classes to achieve thread-safety without implementing complete synchronization.
1. The volatile Keyword in Java
volatile is a synchronization tool that is very light-weight and makes sure that a change in a variable is available simultaneously for visibility to multiple threads. It makes sure a thread reads the latest copy in the main memory and not cached values.
Key Features of Volatile
- Guarantees visibility of changes in different threads.
- Does not provide atomicity (Operations like x++ are not threadsafe).
- Evades compiler optimizations that may reorder instructions.
Example of volatile in Java:
Output:

Explanation:
- With volatility, changes in running may not be easily noticed by other threads as a consequence of CPU caching.
- With volatiles, threads read and write to the main memory without accessing stale values.
2. Atomic Variables in Java (java.util.concurrent.atomic)
Atomic variables are able to provide thread-safe accesses without synchronizations. They achieve atomicity for accesses like increment, decrement, and update for variables being accessed simultaneously.
Key Features of Atomic Variables
- Guarantees atomicity: Avoiding interference among threads while updating.
- Faster than synchronization: It uses CPU-level atomics.
- Found in theutil.concurrent.atomic package.
Example of AtomicInteger:
Output:

Explanation:
- Uses AtomicInteger for atomically incrementing a variable without involving locks.
- Guarantees threadsafety without blocking other threads.
Java Memory Model (JMM)
Java Memory Model (JMM) governs how threads interact upon shared memory, ensuring visibility, ordering, and atomicity for multithreaded program operations. It prevents conditions like race conditions and stale data caused by CPU cache and instruction reordering.
Key Concepts of JMM:
- Happens-Before Relationship: This simply ensures proper sequencing in threads.
- Visibility: It also guarantees that any changes made by one thread must be noticed by other threads.
- Atomicity typically ensures that each operation is executed as a unit.
Thread Safety in JMM:
- volatile Keyword: It typically prevents cache conflicts and reordering of instructions.
- Synchronized Blocks: It ensures exclusive visibility and access.
- Thread-safe classes: (AtomicInteger, ConcurrentHashMap) safely handle shared information..
Example Fixing Visibility Issue:
Output:

With volatility, updating would not be visible as a consequence of CPU caching.
Thread Local Storage in Java
Thread-Local Storage (TLS) is utilized for supplying each thread with a unique copy of a variable in order to prevent accidental inter-thread sharing. In Java, this is accomplished via the use of the ThreadLocal class, providing variables for each thread in a context inaccessible to other threads
Why Use ThreadLocal?
- Thread-Private Data: Each thread has its own independent object for variables.
- Avoids Synchronization: Since there is no sharing, there is no need for synchronizations.
- Useful for Managing Per-Thread Context: Often utilized in database connections, user sessions, and transactions.
How to Use ThreadLocal in Java
- Declaring a Thread-Local Variable
Each thread is given its own instance of the ThreadLocal variable.
Example:
Output:

- Generally, each thread here has its own independent threadLocalValue.
- Also, no data conflicts, as values are isolated per thread.
- Clearing a ThreadLocal Value (Memory Management)
In avoiding memory leaks in extended processes, remove ThreadLocal values when no longer in use.
threadLocalValue.remove(); // Clears the value for the current thread
This is the best practice to avoid memory leaks, specifically in thread pools.
Performance Considerations in Multithreading
Multithreading can improve application performance drastically if multiple CPU cores are utilized efficiently, but incorrect usage can yield performance bottlenecks, contentions, and unnecessary overhead. The following are some best performance considerations for programs using multiple threads.
1. Context Switching Overhead
In cases when multiple threads are executed, the CPU switches between them and saves and restores the state. Excessive context switches can degrade performance.
Suggestion: Use fewer threads than available CPU cores in order to prevent unnecessary context switches.
2. Concurrency and Locking on Threads
Threads waiting for shared resources (i.e., synchronized blocks) are potential causes for delays.
Suggestion: Minimize critical sections, use ReentrantLock, and go for lock-free collections such as ConcurrentHashMap.
3. False Sharing
It typically happens when threads update nearby locations in memory, which leads to cache invalidation in the CPU.
Suggestion: Add padding (i.e., @Contended annotation in Java 8+) to space out highly modified variables.
4. Too Many Threads (Thread Pooling)
An excess of threads can slow down the system, reduce performance, and use too much memory.
Suggestion: Utilize thread pools (ExecutorService) for managing active threads in hand.
5. Load Balancing in Multithreaded Applications
The inconsistent workload can lead to some threads remaining idle and thus lower efficiency.
Suggestion: Use work-stealing algorithms (ForkJoinPool) for automatically rebalancing tasks.
Real-World Applications for Multithreading in Java
- Web Servers and Request Handling: In different web servers like Apache Tomcat or Jetty, many client requests are being managed based on threads, where each request is allocated a separate thread, which simply optimizes the response time and resource utilization.
- Multithreading in Big Data Sets: Many Big data frameworks such as Apache Spark and Apache Hadoop use multithreading in order to execute very large datasets at increased speed.
- Background Tasks in GUI Applications: Different Java GUI frameworks like Swing and JavaFX generally have separate threads for performing different background tasks in order to prevent freezing and lagging of the User Interface(UI) and maintain a smooth User Experience(UX).
- Real-time Stock Market Processing: There are thousands of processing of stock prices that happen every day in the financial markets that are generally based on concurrent collections like ConcurrentLinkedQueue for efficient and non-blocking execution
- Multi-Player Games: The Multithreading concept is very crucial in the gaming industry. Games typically use multiple threads in order to execute the player input, the overall physics, rendering, and network handling. Also, real-time performance and responsiveness of games are typically managed with the help of thread pools like ExecutorService.
Challenges in Multithreading in Java
- Race conditions: These conditions typically occur when many different threads want to utilize the same resource without the proper synchronization, which simply leads to undesired outputs and data corruption.
- Deadlocks: Deadlocks generally occur when multiple different threads are in a blocked state, and waiting for each other to free up some resource
- Synchronization overhead: Accessing and synchronizing a shared resource can be very expensive, which typically leads to slowing down the overall performance of the system.
- Thread Interference: Interference between threads may occur when multiple threads are updating shared variables concurrently and producing unexpected behavior.
- Debugging: Debugging any Multithreaded program is a very difficult task, and as a result of this, the nondeterministic behavior of threads occurs.
Java Multithreading Best Practices
- Use Thread Pooling: Always avoid creating a separate thread for every task, you can use thread pooling. It is very efficient and also saves overhead while threads can be managed and reused.
- Synchronizing: Use the synchronizing constructs like synchronizable functions or blocks and Lock interfaces and atomics in order to prevent race conditions and ensure consistency in the data.
- Avoid Global Variables: Try to minimize the use of globally accessed variables across threads. Always prefer to use thread-local variables or pass variables in an explicit way between threads.
- Use immutable objects as shareable data: Immutable objects do not need to be synchronized when accessed.
- Volatile Keyword: Make use of the volatile keyword in a way that makes changes caused in one thread visible to other threads.
Conclusion
With this, we have come to the end of this Java Multithreading Tutorial. We have discussed that multithreading is a highly effective way to improve the overall performance, scalability, and responsiveness of modern Java applications. By making the best use of CPU cores, coordinating concurrent processes, and optimizing resource sharing, multithreading becomes very useful in designing performance-critical systems.
We have also covered how incorrect usage of threads can lead to race conditions, deadlocks, and performance bottlenecks. A good understanding of these topics such as synchronizations, thread safety, Java Memory Model (JMM), and advanced concurrency tools makes for efficient multithreaded execution.
FAQs on Multithreading in Java
1. What is the main advantage of multithreading in Java?
Java Multithreading generally maximizes the application performance, responsiveness, and resource utilization by offering multiple threads for concurrent running, which simply helps in making better use of modern multi-core processors.
2. How does thread synchronization work in Java?
Thread synchronizations in Java ensure that there is one thread accessing shared resources at a time and no race conditions. It is implemented using synchronized blocks, locks (ReentrantLock), and atomic variables.
3. What are the differences between volatile and synchronized in Java?
- volatile simply ensures the visibility for a variable between threads, but does not prevent race conditions.
- Synchronized in Java typically handles the mutual exclusiveness in that one block is accessed at a time by a single thread.
4. How does Java handle deadlocks in multithreading?
Java does not prevent deadlocks natively, instead, it avoids deadlocks by using best practices like ordering locks in a definite way, time limits, and avoiding nested locks.
5. What is Project Loom and how does it affect Java multithreading?
Project Loom introduces virtual threads, a much lighter alternative to OS threads, and greatly reduces concurrent application scalability overhead.