Java concurrency example problems

Beata 138 Published: 10/31/2024

Java concurrency example problems

I'd be happy to help! Here are some Java concurrency example problems and their solutions:

Problem 1: Producer-Consumer Problem

In a producer-consumer system, producers generate items (e.g., integers) and consumers consume these items. The problem is to ensure that the consumer consumes the items correctly without missing or duplicating any.

class Consumer implements Runnable {

private List sharedList;

private int count;

public Consumer(List sharedList, int count) {

this.sharedList = sharedList;

this.count = count;

}

@Override

public void run() {

for (int i = 0; i < count; i++) {

synchronized (sharedList) {

while (sharedList.isEmpty()) {

try {

sharedList.wait();

} catch (InterruptedException e) {

Thread.currentThread().interrupt();

return;

}

}

Integer item = sharedList.remove(0);

System.out.println("Consumed: " + item);

}

}

}

}

class Producer implements Runnable {

private List sharedList;

public Producer(List sharedList) {

this.sharedList = sharedList;

}

@Override

public void run() {

for (int i = 0; i < 10; i++) {

synchronized (sharedList) {

sharedList.add(i);

System.out.println("Produced: " + i);

sharedList.notifyAll();

}

try {

Thread.sleep(100);

} catch (InterruptedException e) {

Thread.currentThread().interrupt();

return;

}

}

}

}

public class ProducerConsumer {

public static void main(String[] args) throws InterruptedException {

List sharedList = new ArrayList<>();

Consumer consumer = new Consumer(sharedList, 5);

Producer producer = new Producer(sharedList);

Thread t1 = new Thread(consumer);

Thread t2 = new Thread(producer);

t1.start();

t2.start();

t1.join();

t2.join();

}

}

Problem 2: Deadlock in a Bank Transfer System

In a bank transfer system, multiple accounts can be involved in the transfer process. However, if two accounts are holding each other's locks and no account is able to release its lock, a deadlock occurs.

class Account implements Runnable {

private int balance;

private Object lock;

public Account(int balance, Object lock) {

this.balance = balance;

this.lock = lock;

}

@Override

public void run() {

try {

transfer((Account) Thread.currentThread().getContextClassLoader());

} catch (InterruptedException e) {

Thread.currentThread().interrupt();

}

}

private synchronized void transfer(Account account) throws InterruptedException {

if (account != this) {

while (!this.lock.equals(account.lock)) {

wait();

}

// Simulate a transfer

int amount = 100;

this.balance -= amount;

account.balance += amount;

System.out.println("Transfer of " + amount + " from account " + this.hashCode() + " to account " + account.hashCode());

} else {

throw new IllegalMonitorStateException("Trying to lock self!");

}

}

}

public class BankTransferSystem {

public static void main(String[] args) throws InterruptedException {

Account account1 = new Account(1000, new Object());

Account account2 = new Account(2000, new Object());

Thread t1 = new Thread(account1);

Thread t2 = new Thread(account2);

t1.start();

t2.start();

try {

// Wait for the threads to finish

t1.join();

t2.join();

} catch (InterruptedException e) {

Thread.currentThread().interrupt();

}

}

}

Problem 3: Multiple Readers and Writers

In a multi-threaded environment, multiple readers can read data simultaneously. However, when a writer wants to write new data, it must ensure that no reader is accessing the data at that time.

class Reader implements Runnable {

private Object lock;

public Reader(Object lock) {

this.lock = lock;

}

@Override

public void run() {

try {

read();

} catch (InterruptedException e) {

Thread.currentThread().interrupt();

}

}

private synchronized void read() throws InterruptedException {

for (int i = 0; i < 3; i++) {

// Simulate reading

System.out.println("Reader " + this.hashCode() + ": Reading...");

wait(100);

System.out.println("Reader " + this.hashCode() + ": Done reading.");

}

}

}

class Writer implements Runnable {

private Object lock;

public Writer(Object lock) {

this.lock = lock;

}

@Override

public void run() {

try {

write();

} catch (InterruptedException e) {

Thread.currentThread().interrupt();

}

}

private synchronized void write() throws InterruptedException {

// Simulate writing

System.out.println("Writer: Writing...");

wait(1000);

System.out.println("Writer: Done writing.");

}

}

public class MultipleReadersAndWriters {

public static void main(String[] args) {

Object lock = new Object();

Reader reader1 = new Reader(lock);

Reader reader2 = new Reader(lock);

Thread t1 = new Thread(reader1);

Thread t2 = new Thread(reader2);

Thread t3 = new Thread(new Writer(lock));

t1.start();

t2.start();

t3.start();

try {

// Wait for the threads to finish

t1.join();

t2.join();

t3.join();

} catch (InterruptedException e) {

Thread.currentThread().interrupt();

}

}

}

These examples demonstrate some of the fundamental issues that can arise when multiple threads are interacting with shared resources. By using Java's built-in concurrency support, developers can create efficient and scalable multi-threaded applications.

What is the Java concurrency API?

The Java concurrency API refers to a set of classes and interfaces within the Java Standard Edition (SE) that provide support for concurrent programming, also known as multi-threading or parallel processing. The goal of these APIs is to simplify the development of concurrent programs by providing higher-level abstractions and utilities that can help developers avoid many common pitfalls associated with multithreaded programming.

The core classes in the Java concurrency API are:

Runnable: This interface defines a single method, run(), which represents the task that needs to be executed by the thread. Thread: This class is used to create and manage threads. It provides methods for starting, stopping, and synchronizing threads. Executor and its variants: These interfaces define a way to execute tasks asynchronously, decoupling the execution of tasks from the creation of threads. Examples include ExecutorService, ScheduledExecutorService, and ThreadPoolExecutor. Future and its variants: These interfaces provide a way to retrieve the result of an asynchronous computation. Callable: This interface defines a method that can be executed by a thread, and returns a value. ThreadFactory: This interface provides a way to create threads.

Some key benefits of using the Java concurrency API include:

Improved performance: By executing multiple tasks concurrently, your program can take advantage of multi-core processors and improve overall throughput. Simplified programming: The concurrency API provides higher-level abstractions that simplify many common threading-related tasks, making it easier for developers to write concurrent programs. Better scalability: With the ability to execute tasks asynchronously, you can write programs that scale better with increased loads.

Some best practices when using the Java concurrency API include:

Minimize shared state: Try to minimize the amount of shared state between threads, as this can lead to synchronization and consistency issues. Favor immutable objects: Use immutable objects whenever possible, as they are safer for concurrent access. Use thread-safe data structures: Use data structures that are designed to be accessed concurrently, such as CopyOnWriteArrayList or ConcurrentHashMap. Monitor and debug your threads: Use profiling tools and debugging techniques to monitor and understand the behavior of your threads.

In summary, the Java concurrency API provides a set of powerful abstractions and utilities for concurrent programming in Java. By using these APIs, you can write more efficient, scalable, and maintainable programs that take advantage of multi-core processors and improved parallel processing capabilities.