Module 08: Multithreading and Concurrency - Exercises
Practice exercises to master concurrent programming and thread management in Java
Practice Exercises
Exercise 1: Basic Thread Creation
Practice creating and running threads using different approaches.
Requirements:
- Create a thread by extending the Thread class
- Create a thread by implementing the Runnable interface
- Create a thread using lambda expressions
- Start multiple threads and observe their execution
- Use Thread.sleep() to simulate work
Sample Code:
public class ThreadCreationPractice {
// Extending Thread class
static class MyThread extends Thread {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("MyThread: " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// Implementing Runnable interface
static class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("MyRunnable: " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
// Using Thread class
MyThread thread1 = new MyThread();
thread1.start();
// Using Runnable interface
Thread thread2 = new Thread(new MyRunnable());
thread2.start();
// Using lambda expression
Thread thread3 = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("Lambda Thread: " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread3.start();
}
}
Exercise 2: Thread Synchronization
Practice using synchronized methods and blocks to prevent race conditions.
Requirements:
- Create a shared counter class with synchronized methods
- Create multiple threads that increment the counter
- Demonstrate race conditions without synchronization
- Use synchronized blocks for fine-grained control
- Compare the results with and without synchronization
Sample Code:
public class SynchronizationPractice {
static class SharedCounter {
private int count = 0;
// Synchronized method
public synchronized void increment() {
count++;
}
// Synchronized block for fine-grained control
public void incrementWithBlock() {
// Some non-critical code here
synchronized (this) {
count++;
}
// More non-critical code here
}
public synchronized int getCount() {
return count;
}
}
static class IncrementerThread implements Runnable {
private SharedCounter counter;
private int increments;
public IncrementerThread(SharedCounter counter, int increments) {
this.counter = counter;
this.increments = increments;
}
@Override
public void run() {
for (int i = 0; i < increments; i++) {
counter.increment();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
SharedCounter counter = new SharedCounter();
// Create multiple threads
Thread thread1 = new Thread(new IncrementerThread(counter, 1000));
Thread thread2 = new Thread(new IncrementerThread(counter, 1000));
thread1.start();
thread2.start();
// Wait for both threads to complete
thread1.join();
thread2.join();
System.out.println("Final count: " + counter.getCount());
}
}
Exercise 3: Thread Communication
Practice using wait(), notify(), and notifyAll() for thread communication.
Requirements:
- Create a producer-consumer scenario using wait/notify
- Implement a bounded buffer with synchronization
- Use wait() when the buffer is full or empty
- Use notify() to wake up waiting threads
- Handle multiple producers and consumers
Sample Code:
public class ThreadCommunicationPractice {
static class BoundedBuffer {
private final int[] buffer;
private int count = 0;
private int putIndex = 0;
private int takeIndex = 0;
public BoundedBuffer(int size) {
this.buffer = new int[size];
}
public synchronized void put(int value) throws InterruptedException {
while (count == buffer.length) {
System.out.println("Buffer full, producer waiting...");
wait();
}
buffer[putIndex] = value;
putIndex = (putIndex + 1) % buffer.length;
count++;
System.out.println("Produced: " + value);
notify();
}
public synchronized int take() throws InterruptedException {
while (count == 0) {
System.out.println("Buffer empty, consumer waiting...");
wait();
}
int value = buffer[takeIndex];
takeIndex = (takeIndex + 1) % buffer.length;
count--;
System.out.println("Consumed: " + value);
notify();
return value;
}
}
static class Producer implements Runnable {
private BoundedBuffer buffer;
private int items;
public Producer(BoundedBuffer buffer, int items) {
this.buffer = buffer;
this.items = items;
}
@Override
public void run() {
for (int i = 0; i < items; i++) {
try {
buffer.put(i);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
static class Consumer implements Runnable {
private BoundedBuffer buffer;
private int items;
public Consumer(BoundedBuffer buffer, int items) {
this.buffer = buffer;
this.items = items;
}
@Override
public void run() {
for (int i = 0; i < items; i++) {
try {
buffer.take();
Thread.sleep(150);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
Exercise 4: Executor Framework
Practice using the Executor framework for thread management.
Requirements:
- Create a fixed thread pool using Executors.newFixedThreadPool()
- Submit multiple tasks to the executor
- Use Callable interface to return results from threads
- Handle Future objects to get results
- Shutdown the executor properly
Sample Code:
import java.util.concurrent.*;
public class ExecutorFrameworkPractice {
static class Task implements Callable {
private int id;
public Task(int id) {
this.id = id;
}
@Override
public Integer call() throws Exception {
System.out.println("Task " + id + " started");
Thread.sleep(1000); // Simulate work
System.out.println("Task " + id + " completed");
return id * 10;
}
}
public static void main(String[] args) {
// Create a fixed thread pool
ExecutorService executor = Executors.newFixedThreadPool(3);
try {
// Submit multiple tasks
Future future1 = executor.submit(new Task(1));
Future future2 = executor.submit(new Task(2));
Future future3 = executor.submit(new Task(3));
// Get results
System.out.println("Result 1: " + future1.get());
System.out.println("Result 2: " + future2.get());
System.out.println("Result 3: " + future3.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
// Shutdown the executor
executor.shutdown();
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
}
}
}
Exercise 5: Thread Safety and Atomic Operations
Practice using atomic classes and thread-safe collections.
Requirements:
- Use AtomicInteger for thread-safe counter operations
- Use ConcurrentHashMap for thread-safe map operations
- Use CopyOnWriteArrayList for thread-safe list operations
- Compare performance with synchronized alternatives
- Create a thread-safe bank account class
Sample Code:
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
public class ThreadSafetyPractice {
// Thread-safe counter using AtomicInteger
static class AtomicCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public void decrement() {
count.decrementAndGet();
}
public int getCount() {
return count.get();
}
}
// Thread-safe bank account
static class BankAccount {
private AtomicInteger balance;
public BankAccount(int initialBalance) {
this.balance = new AtomicInteger(initialBalance);
}
public void deposit(int amount) {
balance.addAndGet(amount);
}
public boolean withdraw(int amount) {
while (true) {
int currentBalance = balance.get();
if (currentBalance < amount) {
return false; // Insufficient funds
}
if (balance.compareAndSet(currentBalance, currentBalance - amount)) {
return true; // Withdrawal successful
}
// If CAS failed, retry
}
}
public int getBalance() {
return balance.get();
}
}
public static void main(String[] args) throws InterruptedException {
// Test atomic counter
AtomicCounter counter = new AtomicCounter();
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println("Final count: " + counter.getCount());
// Test thread-safe collections
ConcurrentHashMap map = new ConcurrentHashMap<>();
CopyOnWriteArrayList list = new CopyOnWriteArrayList<>();
// These operations are thread-safe
map.put("key1", 1);
list.add("item1");
}
}