Multi-Threading
1. Ways to create multi-threads
-
Extends Thread class
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Hello");
}
}
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
}- Pros:
- Simple to write
- When needing to access the current thread, you can directly use
this
without callingThread.currentThread()
- Cons:
- Since the thread class already inherits from
Thread
, it cannot inherit from any other parent class
- Since the thread class already inherits from
- Pros:
-
Implements Runnable interface
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("MyRunnable");
}
}
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start();
}- Pros:
- The thread class only implements
Runnable
interface, allowing it to inherit from other classes. - Multiple threads can share the same target object, making it highly suitable for scenarios where multiple threads.need to access the same resource.
- The thread class only implements
- Cons:
- Slightly more complex.
- If you need to access the current thread, you must use
Thread.currentThread()
. - Cannot return a result.
- Pros:
-
Implement Callable interface and FutureTask
java.util.concurrent.Callable
interface is similar toRunnable
, but thecall()
method returns a result and thorw exceptions.- To execute
Callable
task, it needs to be wrapped in aFutureTask
object because the constructor ofThread
class takes aRunnable
object, andFutureTask
implementsRunnable
interface.
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "Hello World";
}
}
public static void main(String[] args) {
MyCallable myCallable = new MyCallable();
FutureTask<String> futureTask = new FutureTask<>(myCallable);
Thread t = new Thread(futureTask);
t.start();
try {
String res = futureTask.get();
System.out.println("Result: " + res);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}- Pros
- Same as
Runnable
.
- Same as
- Cons
- Same as
Runnable
.
- Same as
-
Extends ExecutorService
- Starting from Java5,
java.util.concurrent.ExecutorService
introduced support for thread pools. - A way more efficient to manage threads, avoiding the overhead of creating and destroying threads.
- Different types of thread pools can be created using the static methods of the
Executors
class.
class Task implements Runnable {
@Override
public void run() {
System.out.println("Task start");
}
}
ExecutorService executor = Executors.newFixedThreadPool(10);
for(int i = 0; i < 100; i++) {
executor.submit(new Task());
}
executor.shutdown();- Pros
- Can resue pre-created threads, avoiding the overhead of thread creation and destruction.
- For concurrent requests requiring quick responses, thread pools can rapidly provide threads to handle the tasks, reducing wait time.
- Thread pools can control the number of running threads, preventing system resource exhausion due to creating too many threads.
- By properly configuring the thread pool size, CPU utilization and system throughput can be optimized.
- Cons
- Increase program complexity, especially in tuning thread pool parameters and troubleshooting issues.
- Incorrect configurations may lead to problems such as deadlocks or resource exhaustion.
- Starting from Java5,
2. How to prevent deadlocks?
-
A deadlock occurs only when the following four conditions are simultaneously met:
- Mutual Exclusion: Multiple threads cannot use the same resource simulatenously.
- Hold and Wait: Refers to situation where Thread A, which already holds Resource 1, want to acquire Resource 2. However, Resource 2 is held by Thread B, so Thread A enters a waiting state. While Thread A is waiting for Resource 2, Thread A does not release Resource 1.
- No Preemption Condition: Once a thread holds a resource, it cannot be taken away by other threads until the thread releases the resource voluntarily. If Thread B wants to use this Resource 1, it can only acquire it after Thread A has finished using and released it.
- Circular Wait Condition: During a deadlock, the order in which two or more threads acquire resources forms a circular chain.
Deadlock Exampleprivate static final Object resource1 = new Object();
private static final Object resource2 = new Object();
Thread threadA = new Thread(() -> {
synchronized (resource1) {
System.out.println("Thread A acquired resource1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource2) {
System.out.println("Thread A acquired resource2");
}
}
});
Thread threadB = new Thread(() -> {
synchronized (resource2) {
System.out.println("Thread B acquired resource2");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource1) {
System.out.println("Thread B acquired resource1");
}
}
});
thread1.start();
thread2.start();
// Output: Thread 1 acquired resource1
// Output: Thread 2 acquired resource2 -
Most common approach:
Resource Ordering Allocation Method- To break the circular wait condition.
- Thread A and Thread B must acquire resources in the same order. When Thread A attempts to acquire Resource A first and then Resource B, Thread B must also attempt to acquire Resource A first and then Resource B.
3. What are the differences and similarities between wait
and sleep
in Java?
Class
sleep
: static method that is defined in theThread
classwait
: instance method that is defined in theObject
class
Lock Release Behavior
Thread.sleep()
: When called, the thread pauses execution for the specified time but does not release any object locks it holds.Object.wait()
: When called, the thread releases the object lock it holds and enters a waiting state until another thread callsnotify()
ornotifyAll()
on the same object to wake it up.
Usage Condition
sleep
: Can be called anywhere without the need to acquire a lock beforehand.wait
: must be called within a synchronized block or synchronized method (i.e., the thread must hold the object’s lock); otherwise, it throws an IllegalMonitorStateException.
Wake-up Mechanism
sleep
: After the specified sleep duration ends, the thread automatically returns to the runnable state, awaiting CPU scheduling.wait
: The thread remains in the waiting state until another thread calls notify() or notifyAll() on the same object to wake it up.notify()
randomly wakes up one thread waiting on the object, whilenotifyAll()
wakes up all threads waiting on the object.
4. What are the statuses of a thread?
-
java.lang.Thread.State
enum class defines six thread states. You can callgetState()
method on aThread
object to get the current state of the thread.State Description NEW The thread has not yet started, i.e., it has been created but the start
method has not been called.RUNNABLE The thread is in the ready state (after calling start
, awaiting scheduling) or actively running.BLOCKED The thread is blocked, waiting to acquire a monitor lock to enter a synchronized block or method. WAITING The thread is in a waiting state, waiting for another thread to perform a specific action (e.g., Object.notify()
).TIMED_WAITING The thread is in a waiting state with a specified timeout. TERMINATED The thread has completed execution and is in a terminated state.
5. Describe the lifecycle of a Java thread?
- The lifecycle of a thread is divided into five states:
- New: The thread has just been created using the
new
method but has not yet started. - Runnable: After calling the
start()
method, the thread is in a state where it is waiting for CPU resources to be allocated. - Running: When a runnable thread is scheduled and obtains CPU resources, it enters the running state.
- Blocked: While in the running state, a thread may enter a blocked state due to certain reasons, such as calling
sleep()
orwait()
. Woken threads do not immediately execute therun
method; they must wait again for CPU resource allocation to re-enter the running state. - Terminated: If the thread completes execution normally, is forcibly terminated prematurely, or ends due to an exception, the thread is destroyed, releasing its resources.
- New: The thread has just been created using the
6. How do different threads communicate with each other?
-
Shared variables
volatile
- Multiple threads can access and modify the same shared variable to exchange information.
- To ensure thread safety, it is necessary to use
synchronized
orvolatile
keyword.
Multiple Threads - Volatileprivate static volatile boolean flag = false;
public static void main(String[] args) {
Thread producer = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("Producer: Flag is set to true.");
});
Thread consumer = new Thread(() -> {
while (!flag) {
}
System.out.println("Consumer: Flag is now true.");
});
producer.start();
consumer.start();
} -
wait()
,notify()
, andnotifyAll()
methods inObject
class- The
wait()
method causes the current thread to enter a waiting state. - The
notify()
method wakes up a single thread that is waiting on this object's monitor. - The
notifyAll()
method wakes up all threads that are waiting on this object's monitor.
class WaitNotifyExample {
private static final Object lock = new Object();
public static void main(String[] args) {
// Thread producer
Thread producer = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("Producer: Producing...");
Thread.sleep(2000); // Simulate production time
System.out.println("Producer: Production finished. Notifying consumer");
// Notify waiting threads
lock.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// Thread consumer
Thread consumer = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("Consumer: Waiting for production to finish...");
// Wait for notification
lock.wait();
System.out.println("Consumer: Production finished. Consuming...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
consumer.start();
producer.start();
}
} - The
-
Lock
andCondition
interfaces- More flexible appraoch compared to
synchronized
- The
await()
method of theCondition
interface is similar to thewait()
method. - The
signal()
is similar to thenotify()
method. - The
signalAll()
is similar to thenotifyAll()
method. - The
ReentrantLock
class implements theLock
interface.
private static final Lock lock = new ReentrantLock();
private static final Condition condition = lock.newCondition();
public static void main(String[] args) {
// Producer thread
Thread producer = new Thread(() -> {
lock.lock();
try {
System.out.println("Producer: Producing...");
Thread.sleep(2000); // Simulate production time
System.out.println("Producer: Production finished. Notifying consumer...");
// Notify waiting threads
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
// Consumer thread
Thread consumer = new Thread(() -> {
lock.lock();
try {
System.out.println("Consumer: Waiting for production to finish...");
// Wait for notification
condition.await();
System.out.println("Consumer: Production finished. Consuming...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
consumer.start();
producer.start();
} - More flexible appraoch compared to
-
BlockingQueue
Interface- Thread-safe queue operations.
- When the queue is full, threads attempting to insert elements will be blocked.
- When the queue is empty, threads attempting to retrieve elements will be blocked.
private static final BlockingQueue queue = new LinkedBlockingQueue<>(1);
public static void main(String[] args) {
// Producer thread
Thread producer = new Thread(() -> {
try {
System.out.println("Producer: Producing...");
queue.put(1); // Blocks if queue is full
System.out.println("Producer: Production finished.");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// Consumer thread
Thread consumer = new Thread(() -> {
try {
System.out.println("Consumer: Waiting for production to finish...");
int item = (int) queue.take(); // Blocks if queue is empty
System.out.println("Consumer: Consumed item: " + item);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
consumer.start();
producer.start();
}
7. What are the methods for inter-thread communication?
8. Describe the differences between sleep()
and wait()
?
sleep()
:Thread
class, pauses the current thread for a specified time.wait()
:Object
class, waits for another thread to notify.
9. Describe the differences between notify()
and notifyAll()
?
notify()
: Wakes up one thread waiting on the object’s monitor.notifyAll()
: Wakes up all threads waiting on the object’s monitor
10. What is the difference between the run()
and start()
methods of a thread?
Thread Pool
Configuration of Thread Pool.
- corePoolSize: The minimum number of threads kept alive in the pool, even when idle.
- maximumPoolSize: The maximum number of threads allowed in the pool when the task queue is full.
- Queue Capacity: The number of tasks that can be queued when all core threads are busy (before scaling up to maxPoolSize).
- KeepAliveTime: The amount of time a non-core thread can remain idle before being terminated.
- workQueue: The queue used to store tasks when all core threads are busy.
- handler: The policy to use when the task queue is full and all threads are busy.
Built-in Rejection Strategies.
CallerRunsPolicy
: The task is executed by the thread that calls the execute method.AbortPolicy
: Directly throws an exception indicating that the task was rejected by the thread pool.DiscardPolicy
: Silently discards the submitted task without any processing.DiscardOldestPolicy
: Discards the oldest task in the queue and then executes the new task.
Do you have experience with thread pool parameter settings?
- CPU-intensive: corePooSize = coreSize + 1;
- If the number of threads exceeds the number of CPU core significantly, threads will compete for CPU time, causing frequent context switching.
- Trade-off of increasing number of threads:
- Context Switching: CPU switches from one thread to another, cost arise due to save and restore thread's state.
E-commerce:
- Instantaneous high concurrency
new ThreadPoolExecutor(
16, // corePoolSize = 16 (assuming 8-core CPU × 2)
32, // maximumPoolSize = 32 (expand for burst traffic)
10, TimeUnit.SECONDS, // non-core threads recycled after 10s idle
new SynchronousQueue<>(), // no task caching, directly expand threads
new AbortPolicy() // reject directly to avoid system overload
);
Microservice HTTP Request:
new ThreadPoolExecutor(
16, // corePoolSize = 16 (8-core × 2)
64, // maximumPoolSize = 64 (handle slow downstream)
60, TimeUnit.SECONDS, // non-core threads recycled after 60s idle
new LinkedBlockingQueue<>(200), // bounded queue with capacity 200
new CustomRetryPolicy() // custom rejection policy (retry or fallback)
);
- CallRunsPolicy():
- AbortPolicy():