Creating Threads in Java
The first two creation approaches do not directly return execution results, while the third approach supports getting a result after the thread finishes running.
Approach 1: Extend the Thread class
Java uses instances of java.lang.Thread to represent threads.
- You must call the
start()method to launch a new thread, do not callrun()directly - Do not place main thread tasks before launching child threads
// Step 1: Create a custom subclass extending Thread
public class CountingThread extends Thread {
// Step 2: Override the run() method to define thread task
@Override
public void run() {
for (int idx = 0; idx < 5; idx++) {
System.out.println("CountingThread output: " + idx);
}
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
// Step 3: Create an instance of your custom thread class
Thread thread = new CountingThread();
// Step 4: Launch the new thread
thread.start();
}
}
Approach 2: Implement the Runnable interface
- Advantage: The task class only implements an interface, so it can still extend other classes and implement additional interfaces, leading to better scalability
// Step 1: Implement the Runnable interface
public class WorkTask implements Runnable {
// Step 2: Override the run() method
@Override
public void run() {
// Define the task to execute
for (int idx = 0; idx < 5; idx++) {
System.out.println("Child thread output: " + idx);
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
// Step 4: Create task instance
Runnable task = new WorkTask();
// Step 5: Pass the task to a Thread instance and launch
new Thread(task).start();
for (int idx = 0; idx < 5; idx++) {
System.out.println("Main thread output: " + idx);
}
}
}
Anonymous Inner Class Variant for Runnable
- Create an anonymous inner instance of Runnable
- Pass it to a Thread instance
- Call
start()to launch the thread
public class AnonymousThreadDemo {
public static void main(String[] args) {
// 1. Anonymous inner class of Runnable
Runnable task1 = new Runnable() {
@Override
public void run() {
for (int idx = 0; idx < 5; idx++) {
System.out.println("Child thread 1 output: " + idx);
}
}
};
new Thread(task1).start();
// Simplified form
new Thread(new Runnable() {
@Override
public void run() {
for (int idx = 0; idx < 5; idx++) {
System.out.println("Child thread 2 output: " + idx);
}
}
}).start();
// Lambda simplified form
new Thread(() -> {
for (int idx = 0; idx < 5; idx++) {
System.out.println("Child thread 3 output: " + idx);
}
}).start();
for (int idx = 0; idx < 5; idx++) {
System.out.println("Main thread output: " + idx);
}
}
}
Approach 3: Implement the Callable Interface
- Added in JDK 5, works with
FutureTaskto support returning results after thread execution
Process:
- Define a class that implements
Callable, overridecall()to encapsulate your task and return data - Wrap the
Callableinstance into aFutureTaskobject - Pass the
FutureTaskto aThreadinstance - Call
start()to launch the thread - After execution completes, get the result via
FutureTask.get()
// 1. Implement Callable interface with specified return type
public class SumCalculator implements Callable<String> {
private int upperLimit;
public SumCalculator(int n) {
this.upperLimit = n;
}
// 2. Override call() method to calculate sum from 1 to n
@Override
public String call() throws Exception {
int total = 0;
for (int i = 1; i <= upperLimit; i++) {
total += i;
}
return String.format("Thread computed sum from 1 to %d: %d", upperLimit, total);
}
}
public class ThreadDemo3 {
public static void main(String[] args) throws Exception {
// 3. Create Callable instance
Callable<String> calcTask = new SumCalculator(100);
// 4. Wrap Callable into FutureTask
FutureTask<String> futureTask = new FutureTask<>(calcTask);
new Thread(futureTask).start();
// 6. Get the execution result
System.out.println(futureTask.get());
}
}
Common Thread Methods
public class NamedCountingThread extends Thread {
public NamedCountingThread(String threadName) {
super(threadName);
}
@Override
public void run(){
Thread current = Thread.currentThread();
for (int i = 0; i < 3; i++) {
System.out.println(current.getName() + " output: " + i);
}
}
}
public class ThreadMethodDemo {
public static void main(String[] args) {
Thread t1 = new NamedCountingThread("Thread-One");
t1.start();
System.out.println(t1.getName());
Thread t2 = new NamedCountingThread("Thread-Two");
t2.start();
System.out.println(t2.getName());
// Get current running thread instance
Thread mainThread = Thread.currentThread();
System.out.println(mainThread.getName());
for (int i = 0; i < 4; i++) {
System.out.println("Main thread output: " + i);
}
}
}
// Demo for sleep() and join()
public class ThreadStateMethodDemo {
public static void main(String[] args) throws Exception {
for (int i = 0; i < 5; i++) {
// Sleep for 5 seconds when i equals 3
if(i == 3) {
Thread.sleep(5000);
}
System.out.println(i);
}
// join(): makes the calling thread finish execution before the main thread continues
Thread t1 = new NamedCountingThread("Thread-1");
t1.start();
t1.join();
Thread t2 = new NamedCountingThread("Thread-2");
t2.start();
t2.join();
Thread t3 = new NamedCountingThread("Thread-3");
t3.start();
Thread t4 = new NamedCountingThread("Thread-4");
t4.start();
}
}
Thread Safety
Locking ensures only one thread can acquire the lock and enter the critical section, automatcially unlocking after execution to allow other threads to enter.
Synchronized Blocks
synchronized (lockObject) {
// Critical section accessing shared resources
}
Notes for Synchronization Locks
- All concurrent threads must use the same lock object, otherwise thread safety cannot be guaranteed
- Recommend using the shared resource itself as the lock object: use
thisfor instance methods, use the classClassobject for static methods
public class BankAccount {
private String accountNumber;
private double balance;
public BankAccount() {}
public BankAccount(String accountNumber, double balance) {
this.accountNumber = accountNumber;
this.balance = balance;
}
public String getAccountNumber() { return accountNumber; }
public void setAccountNumber(String accountNumber) { this.accountNumber = accountNumber; }
public double getBalance() { return balance; }
public void setBalance(double balance) { this.balance = balance; }
public void withdraw(int amount) {
String userName = Thread.currentThread().getName();
// this is the shared account object, used as lock
synchronized (this) {
if(this.balance >= amount) {
System.out.println(userName + " withdrew " + amount + " successfully");
this.balance -= amount;
System.out.println("Remaining balance after withdrawal: " + this.balance);
} else {
System.out.println(userName + " attempted withdrawal: insufficient balance");
}
}
}
}
public class WithdrawThread extends Thread {
private BankAccount acc;
public WithdrawThread(BankAccount acc, String name) {
super(name);
this.acc = acc;
}
@Override
public void run() {
acc.withdraw(100000);
}
}
public class ThreadSafetyDemo {
public static void main(String[] args) {
// Create a shared account with 100000 balance
BankAccount sharedAcc = new BankAccount("123456", 100000);
// Two users withdraw from the same account
new WithdrawThread(sharedAcc, "Xiaoming").start();
new WithdrawThread(sharedAcc, "Xiaohong").start();
}
}
Synchronized Methods
Locks the entire method that accesses shared resources, to guarantee thread safety.
- For instance methods, the default lock is
this - For static methods, the default lock is the class
Classobject
// Synchronized method syntax
modifier synchronized returnType methodName(parameters) {
// code operating on shared resources
}
public synchronized void withdraw(int amount) {
String userName = Thread.currentThread().getName();
if(this.balance >= amount) {
System.out.println(userName + " withdrew " + amount + " successfully");
this.balance -= amount;
System.out.println("Remaining balance after withdrawal: " + this.balance);
} else {
System.out.println(userName + " attempted withdrawal: insufficient balance");
}
}
Explicit Lock with Lock Interface
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class BankAccount {
private String accountNumber;
private double balance;
// Initialize lock object
private final Lock locker = new ReentrantLock();
public BankAccount() {}
public BankAccount(String accountNumber, double balance) {
this.accountNumber = accountNumber;
this.balance = balance;
}
public String getAccountNumber() { return accountNumber; }
public void setAccountNumber(String accountNumber) { this.accountNumber = accountNumber; }
public double getBalance() { return balance; }
public void setBalance(double balance) { this.balance = balance; }
public void withdraw(int amount) {
String userName = Thread.currentThread().getName();
locker.lock();
try {
if(this.balance >= amount) {
System.out.println(userName + " withdrew " + amount + " successfully");
this.balance -= amount;
System.out.println("Remaining balance after withdrawal: " + this.balance);
} else {
System.out.println(userName + " attempted withdrawal: insufficient balance");
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// Always unlock in finally to avoid deadlock
locker.unlock();
}
}
}
Thread Pools
Recommended thread pool sizing rules:
- Compute-bound tasks: core pool size = number of CPU cores + 1
- IO-bound tasks: core pool size = number of CPU cores * 2
1. Manually Create Thread Pool with ThreadPoolExecutor
When are temporary threads created?
- When a new task arrives, all core threads are busy, the work queue is full, and maximum pool size allows more threads, a temporary thread is created
When are new tasks rejected?
- When all core and temporary threads are busy, the work queue is full, new tasks will be rejected per the configured policy
public ThreadPoolExecutor(
int corePoolSize, // Number of core threads
int maximumPoolSize, // Maximum number of total threads (core + temporary)
long keepAliveTime, // Survival time for idle temporary threads
TimeUnit unit, // Time unit for keepAliveTime
BlockingQueue<Runnable> workQueue, // Task work queue
ThreadFactory threadFactory, // Thread factory for creating threads
RejectedExecutionHandler handler // Rejection policy for new tasks
) {}
ExecutorService pool = new ThreadPoolExecutor(
3,
5,
8,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(4),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
2. Create Thread Pool with Executors Utility Class
import java.util.concurrent.*;
public class FixedThreadPoolDemo {
public static void main(String[] args) throws Exception {
// Create a fixed size thread pool
ExecutorService pool = Executors.newFixedThreadPool(3);
// Submit Callable tasks to the pool
Future<String> f1 = pool.submit(new SumCalculator(100));
Future<String> f2 = pool.submit(new SumCalculator(200));
Future<String> f3 = pool.submit(new SumCalculator(300));
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
pool.shutdown();
}
}
Thread Pool Processing of Runnable Tasks
import java.util.concurrent.*;
public class RunnableTask implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " processed task");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
import java.util.concurrent.*;
public class RunnableThreadPoolDemo {
public static void main(String[] args) {
ExecutorService pool = new ThreadPoolExecutor(
3, 5, 8, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(4),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
RunnableTask task = new RunnableTask();
pool.execute(task);
pool.execute(task);
pool.execute(task);
pool.execute(task); // Reuses existing idle threads
pool.execute(task);
// Shutdown policies
pool.shutdown(); // Wait for all tasks to complete then shutdown
// pool.shutdownNow(); // Shutdown immediately even if tasks are incomplete
}
}
Thread Pool Processing of Callable Tasks
import java.util.concurrent.*;
public class CallableThreadPoolDemo {
public static void main(String[] args) throws Exception {
ExecutorService pool = new ThreadPoolExecutor(
3, 5, 8, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(4),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
Future<String> f1 = pool.submit(new SumCalculator(100));
Future<String> f2 = pool.submit(new SumCalculator(200));
Future<String> f3 = pool.submit(new SumCalculator(300));
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
pool.shutdown();
}
}
Optimistic vs Pessimistic Locking
Pessimistic Locking
Pessimistic locking locks the resource upfront, assuming conflitcs will happen. Only one thread can access the resource at a time, thread safe but lower performance.
public class PessimisticCounter implements Runnable {
private int count = 0;
@Override
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (this) {
System.out.println(++count);
}
}
}
}
public class PessimisticLockDemo {
public static void main(String[] args) {
Runnable counter = new PessimisticCounter();
for (int i = 1; i <= 100; i++) {
new Thread(counter).start();
}
}
}
Optimistic Locking
Optimistic locking does not lock upfront, assuming no conflicts. It only handles conflicts when they actually occur, thread safe and higher performance. Atomic classes are common implementations for optimistic locking.
import java.util.concurrent.atomic.AtomicInteger;
public class OptimisticCounter implements Runnable {
// AtomicInteger implements optimistic locking for integer updates
private AtomicInteger count = new AtomicInteger(0);
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(count.incrementAndGet());
}
}
}
public class OptimisticLockDemo {
public static void main(String[] args) {
Runnable counter = new OptimisticCounter();
for (int i = 1; i <= 100; i++) {
new Thread(counter).start();
}
}
}