Multithreading
1. Concepts: Process and Thread
A process refers to an actively executing program instance. A thread acts as a single execution path within a process, handling the sequential execution of code segments. All application code runs within threads; by default, a JVM launches a main thread to start execution.
Single-threaded execution processes tasks sequentially and cannot handle multiple operations simultaneously. Multithreading enables concurrent (apparent simultaneous) task handling. From a macroscopic perspective, fast thread scheduling creates the illusion of continuous parallel work. Microscopically, threads compete for CPU time slices, with only one thread executing per core at any given moment.
2. Multithreading Creation Approaches
The Thread class and its subclasses represent thread objects in Java. There are two primary creation methods.
Method 1: Extend Thread Class
- Define a subclass of
Threadand override itsrun()method to encapsulate thread-specific logic. - Instantiate the subclass, optionally set a thread name, and invoke
start()(notrun()directly) to launch it.
public class TicketSellerThread extends Thread {
@Override
public void run() {
// Thread execution logic
for (int count = 0; count < 100; count++) {
System.out.println("Processing ticket sale step...");
}
}
}
Usage example:
public class TicketStation {
public static void main(String[] args) {
TicketSellerThread seller1 = new TicketSellerThread();
TicketSellerThread seller2 = new TicketSellerThread();
TicketSellerThread seller3 = new TicketSellerThread();
seller1.setName("Counter A");
seller2.setName("Counter B");
seller3.setName("Counter C");
seller1.start();
seller2.start();
seller3.start();
}
}
Method 2: Implement Runnable Interface
- Create a class that implements the
Runnableinterface and defines therun()method. - Pass an instance of this implementation to one or more
Threadconstructors.
public class TicketProcessor implements Runnable {
@Override
public void run() {
for (int idx = 0; idx < 100; idx++) {
System.out.println(Thread.currentThread().getName() + " is handling a ticket request");
}
}
}
Usage example:
public class OnlineTicketSystem {
public static void main(String[] args) {
TicketProcessor processor = new TicketProcessor();
Thread worker1 = new Thread(processor, "Server Node 1");
Thread worker2 = new Thread(processor, "Server Node 2");
Thread worker3 = new Thread(processor, "Server Node 3");
worker1.start();
worker2.start();
worker3.start();
}
}
3. Thread Lifecycle States
- New: A thread object created via
newbut not yet started; it cannot compete for CPU resources. - Runnable: After calling
start(), the thread enters this state, eligible to grab CPU time slices but not currently executing. - Running: The thread has secured CPU control and is executing its
run()method; it returns to Runnable if CPU time expires or it is preempted. - Blocked/Waiting/Timed Waiting: The thread pauses execution due to operations like
sleep(),wait(), or I/O; it transitions back to Runnable after the blocking condition resolves (e.g., timeout,notify()). - Terminated: The thread completes
run()execution or is interrupted unexpectedly, ending its lifecycle.
4. Common Thread Operations
(1) Adjust Thread Priority
Thread priority ranges from 1 (MIN_PRIORITY) to 10 (MAX_PRIORITY), with a default of 5 (NORM_PRIORITY). Higher priority threads have a greater probability of being scheduled first, not guaranteed.
public class PriorityDemoThread extends Thread {
@Override
public void run() {
for (int step = 0; step < 100; step++) {
System.out.println(Thread.currentThread().getName() + " is processing step " + step);
}
}
}
public class PriorityTest {
public static void main(String[] args) {
PriorityDemoThread high = new PriorityDemoThread();
PriorityDemoThread normal = new PriorityDemoThread();
PriorityDemoThread low = new PriorityDemoThread();
high.setName("High-Priority Worker");
normal.setName("Normal-Priority Worker");
low.setName("Low-Priority Worker");
high.setPriority(Thread.MAX_PRIORITY);
normal.setPriority(Thread.NORM_PRIORITY);
low.setPriority(Thread.MIN_PRIORITY);
high.start();
normal.start();
low.start();
}
}
(2) Thread Sleep
Thread.sleep(long millis) pauses the current thread for a specified duration (milliseconds), entering Timed Waiting state, and automatically resumes afterward. It throws InterruptedException.
public class SleepDemoThread extends Thread {
@Override
public void run() {
String threadLabel = Thread.currentThread().getName();
for (int tick = 0; tick < 100; tick++) {
if ("Delayed Worker".equals(threadLabel)) {
try {
System.out.println("Delayed Worker pausing for 300ms");
Thread.sleep(300);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println(threadLabel + " completed tick " + tick);
}
}
}
(3) Thread Yield
Thread.yield() voluntarily releases the current CPU time slice, moving the thread back to Runnable state, allowing other same/higher-priority threads to execute. It does not guarantee immediate handoff.
public class YieldDemoThread extends Thread {
@Override
public void run() {
String id = Thread.currentThread().getName();
for (int round = 0; round < 100; round++) {
if ("Yielding Thread".equals(id) && round == 15) {
Thread.yield();
}
System.out.println(id + " is at round " + round);
}
}
}
(4) Thread Join
join() forces the current thread to wait until the target thread finishes execution. It throws InterruptedException.
public class ParentTask extends Thread {
@Override
public void run() {
String parentName = Thread.currentThread().getName();
System.out.println(parentName + " starts preparing dough");
System.out.println(parentName + " shapes dough into pizza base");
System.out.println(parentName + " realizes no cheese is left");
ChildTask fetchCheese = new ChildTask();
fetchCheese.setName("Kid Runner");
fetchCheese.start();
try {
fetchCheese.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println(parentName + " adds cheese to pizza base");
System.out.println(parentName + " bakes and serves pizza");
}
}
public class ChildTask extends Thread {
@Override
public void run() {
String childName = Thread.currentThread().getName();
System.out.println(childName + " runs downstairs");
System.out.println(childName + " walks to nearby grocery");
System.out.println(childName + " buys mozzarella cheese");
System.out.println(childName + " returns home with cheese");
}
}
public class PizzaPrep {
public static void main(String[] args) {
ParentTask mom = new ParentTask();
mom.setName("Mom");
mom.start();
}
}
5. Thread Synchronization
Thread safety issues arise when multiple threads modify shared mutable data without coordination, leading to inconsistent results.
Synchronized Code Block
Wrap shared mutable operations in a synchronized block with a common lock object to ensure exclusive execution. Only one thread can enter the block at a time.
public class SharedTicketSeller implements Runnable {
private static int remainingTickets = 100;
@Override
public void run() {
while (true) {
synchronized (SharedTicketSeller.class) {
if (remainingTickets <= 0) {
break;
}
String counter = Thread.currentThread().getName();
System.out.println(counter + " sold ticket #" + remainingTickets);
remainingTickets--;
}
}
}
}
Synchronized Methods
Non-Static Synchronized Method
Uses this as the implicit lock object, ensuring exclusive execution per instance.
public synchronized void deductTicket() {
// Shared mutable logic here
}
Static Synchronized Method
Uses the class’s bytecode object (Class<T>) as the implicit lock, ensuring exclusive execution across all instances.
public static synchronized void resetTicketPool() {
// Static shared mutable logic here
}