Essential Guide to Java Multithreading and JUC Fundamentals
Startnig Threads by Extending the Thread Class
This approach is straightforward and allows direct use of Thread class methods, but it limits inheritance because Java does not support multiple inheritance.
public class WorkerThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + " processing task");
}
}
}
public class ThreadTest {
public static void main(String[] args) {
WorkerThread w1 = new WorkerThread();
WorkerThread w2 = new WorkerThread();
w1.setName("Worker-1");
w2.setName("Worker-2");
w1.start();
w2.start();
}
}
Implementing Runnable to Create Threads
This method offers better extensibility since the class can still inherit from other classes, though it requires obtaining the current thread reference to access Thread methods.
public class TaskRunner implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
Thread current = Thread.currentThread();
System.out.println(current.getName() + " executing task");
}
}
}
public class ThreadTest {
public static void main(String[] args) {
TaskRunner runner = new TaskRunner();
Thread t1 = new Thread(runner);
Thread t2 = new Thread(runner);
t1.setName("Task-1");
t2.setName("Task-2");
t1.start();
t2.start();
}
}
Using Callable to Obtain Thread Results
Callable allows threads to return results, making it suitable for tasks that need to produce a computed value.
import java.util.concurrent.Callable;
public class SumCalculator implements Callable<Integer> {
@Override
public Integer call() {
int total = 0;
for (int i = 1; i <= 100; i++) {
total += i;
}
return total;
}
}
import java.util.concurrent.FutureTask;
public class ThreadTest {
public static void main(String[] args) throws Exception {
SumCalculator calculator = new SumCalculator();
FutureTask<Integer> future = new FutureTask<>(calculator);
Thread taskThread = new Thread(future);
taskThread.start();
Integer result = future.get();
System.out.println("Sum result: " + result);
}
}
Common Thread Instance Methods
- getName(): Retrieves the thread name. If not set, default names like Thread-0, Thread-1 are assigned.
- setName(String): Sets the thread name. Can also be configured via constructor.
- sleep(long milliseconds): Pauses the current thread for the specified duration.
public class NamedThread extends Thread {
public NamedThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + " - " + i);
}
}
}
public class ThreadTest {
public static void main(String[] args) {
NamedThread plane = new NamedThread("Plane");
NamedThread tank = new NamedThread("Tank");
plane.start();
tank.start();
System.out.println("Main thread: " + Thread.currentThread().getName());
}
}
Thread Priority
Threads can be assigned priorities ranging from 1 to 10, influencing (but not guaranteeing) scheduling order.
public class PriorityTask implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " - " + i);
}
}
}
public class ThreadTest {
public static void main(String[] args) {
PriorityTask task = new PriorityTask();
Thread t1 = new Thread(task, "HighPriority");
Thread t2 = new Thread(task, "LowPriority");
t1.setPriority(10);
t2.setPriority(1);
System.out.println("T1 priority: " + t1.getPriority());
System.out.println("T2 priority: " + t2.getPriority());
t1.start();
t2.start();
}
}
Daemon Threads
Daemon threads run in the background and terminate automatically when all non-daemon threads finish execution.
public class MainWorker extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName() + " - " + i);
}
}
}
public class SupportWorker extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + " - " + i);
}
}
}
public class ThreadTest {
public static void main(String[] args) {
MainWorker main = new MainWorker();
SupportWorker support = new SupportWorker();
main.setName("MainProcess");
support.setName("DaemonProcess");
support.setDaemon(true);
main.start();
support.start();
}
}
Thread Yielding
The yield() method hints the scheduler to give up the current thread's CPU time, allowing other threads to execute.
public class YieldThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + " - " + i);
Thread.yield();
}
}
}
public class ThreadTest {
public static void main(String[] args) {
YieldThread t1 = new YieldThread();
YieldThread t2 = new YieldThread();
t1.setName("Thread-A");
t2.setName("Thread-B");
t1.start();
t2.start();
}
}
Thread Joining
The join() method makes the current thread wait until the joined thread completes execution.
public class JoinThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + " - " + i);
}
}
}
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
JoinThread task = new JoinThread();
task.setName("JoinTask");
task.start();
task.join();
for (int i = 0; i < 10; i++) {
System.out.println("Main thread iteration: " + i);
}
}
}
Synchronized Code Blocks
Synchronized blocks protect shared resources by requiring threads to acquire a common lock before executing critical sections.
public class TicketSeller extends Thread {
private static int tickets = 0;
@Override
public void run() {
while (true) {
synchronized (TicketSeller.class) {
try { Thread.sleep(100); } catch (InterruptedException e) {}
if (tickets < 100) {
tickets++;
System.out.println(getName() + " sold ticket #" + tickets);
} else {
break;
}
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
TicketSeller w1 = new TicketSeller();
TicketSeller w2 = new TicketSeller();
TicketSeller w3 = new TicketSeller();
w1.setName("Window1");
w2.setName("Window2");
w3.setName("Window3");
w1.start();
w2.start();
w3.start();
}
}
Synchronized Methods
Methods marked with synchronized automatically lock on the current instance (or class for static methods).
public class TicketTask implements Runnable {
private int count = 0;
@Override
public void run() {
while (!sellTicket()) {}
}
private synchronized boolean sellTicket() {
if (count >= 1000) return true;
count++;
System.out.println(Thread.currentThread().getName() + " sold ticket #" + count);
return false;
}
}
public class ThreadTest {
public static void main(String[] args) {
TicketTask task = new TicketTask();
Thread t1 = new Thread(task, "Window1");
Thread t2 = new Thread(task, "Window2");
Thread t3 = new Thread(task, "Window3");
t1.start();
t2.start();
t3.start();
}
}
Using Lock API
ReentrantLock provides explicit lock management similar to synchronized blocks but with more flexibility.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockTicketSeller extends Thread {
private static int tickets = 0;
private static final Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock();
try {
if (tickets >= 100) break;
Thread.sleep(10);
tickets++;
System.out.println(getName() + " sold ticket #" + tickets);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
LockTicketSeller s1 = new LockTicketSeller();
LockTicketSeller s2 = new LockTicketSeller();
LockTicketSeller s3 = new LockTicketSeller();
s1.setName("Window1");
s2.setName("Window2");
s3.setName("Window3");
s1.start();
s2.start();
s3.start();
}
}
Deadlock Scenario
Deadlocks occur when multiple threads hold locks needed by others, causing indefinite waiting.
public class DeadlockThread extends Thread {
static final Object resourceA = new Object();
static final Object resourceB = new Object();
@Override
public void run() {
while (true) {
if ("Thread-A".equals(getName())) {
synchronized (resourceA) {
System.out.println("Thread-A acquired A, waiting for B");
synchronized (resourceB) {
System.out.println("Thread-A acquired B");
}
}
} else {
synchronized (resourceB) {
System.out.println("Thread-B acquired B, waiting for A");
synchronized (resourceA) {
System.out.println("Thread-B acquired A");
}
}
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
DeadlockThread t1 = new DeadlockThread();
DeadlockThread t2 = new DeadlockThread();
t1.setName("Thread-A");
t2.setName("Thread-B");
t1.start();
t2.start();
}
}
Wait and Notify Mechanism (Producer-Consumer)
Threads communicate using wait() and notifyAll() with a shared lock to coordinate production and consumption.
class RestaurantDesk {
static int mealReady = 0;
static int servingsLeft = 10;
static final Object lock = new Object();
}
public class Chef extends Thread {
@Override
public void run() {
while (true) {
synchronized (RestaurantDesk.lock) {
if (RestaurantDesk.servingsLeft == 0) break;
if (RestaurantDesk.mealReady == 1) {
try { RestaurantDesk.lock.wait(); } catch (InterruptedException e) {}
} else {
System.out.println("Chef prepared a meal");
RestaurantDesk.mealReady = 1;
RestaurantDesk.lock.notifyAll();
}
}
}
}
}
public class Customer extends Thread {
@Override
public void run() {
while (true) {
synchronized (RestaurantDesk.lock) {
if (RestaurantDesk.servingsLeft == 0) break;
if (RestaurantDesk.mealReady == 0) {
try { RestaurantDesk.lock.wait(); } catch (InterruptedException e) {}
} else {
RestaurantDesk.servingsLeft--;
System.out.println("Customer ate meal, remaining: " + RestaurantDesk.servingsLeft);
RestaurantDesk.mealReady = 0;
RestaurantDesk.lock.notifyAll();
}
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
Chef chef = new Chef();
Customer customer = new Customer();
chef.setName("Chef");
customer.setName("Customer");
chef.start();
customer.start();
}
}
BlockingQueue-Based Producer-Consumer
ArrayBlockingQueue simplifies synchronization by handling thread-safe puts and takes internal.
import java.util.concurrent.ArrayBlockingQueue;
public class QueueChef extends Thread {
private final ArrayBlockingQueue<String> queue;
public QueueChef(ArrayBlockingQueue<String> queue) { this.queue = queue; }
@Override
public void run() {
while (true) {
try {
queue.put("Meal");
System.out.println("Chef put a meal in queue");
} catch (InterruptedException e) {}
}
}
}
import java.util.concurrent.ArrayBlockingQueue;
public class QueueCustomer extends Thread {
private final ArrayBlockingQueue<String> queue;
public QueueCustomer(ArrayBlockingQueue<String> queue) { this.queue = queue; }
@Override
public void run() {
while (true) {
try {
String meal = queue.take();
System.out.println("Customer took: " + meal);
} catch (InterruptedException e) {}
}
}
}
import java.util.concurrent.ArrayBlockingQueue;
public class ThreadTest {
public static void main(String[] args) {
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
QueueChef chef = new QueueChef(queue);
QueueCustomer customer = new QueueCustomer(queue);
chef.start();
customer.start();
}
}
Thread Pool Creation
The Executors utility class provides factory methods for common thread pool configurations.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class PoolTask implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " executing");
}
}
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolTest {
public static void main(String[] args) throws InterruptedException {
ExecutorService fixedPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
fixedPool.submit(new PoolTask());
Thread.sleep(1000);
}
fixedPool.shutdown();
}
}
Custom Thread Pool Configuration
ThreadPoolExecutor allows fine-grained control over core pool size, queue capacity, and rejection policies.
import java.util.concurrent.*;
public class CustomPoolTest {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
3, // core pool size
6, // maximum pool size
60L, // keep-alive time
TimeUnit.SECONDS, // time unit
new ArrayBlockingQueue<>(3), // work queue
Executors.defaultThreadFactory(), // thread factory
new ThreadPoolExecutor.CallerRunsPolicy() // rejection policy
);
executor.submit(() -> System.out.println(Thread.currentThread().getName()));
}
}