Multithreading enables a program to run multiple threads concurrently, improving performance and responsiveness. Each thread runs independently but shares the same memory, making efficient resource utilization possible. It's ideal for tasks like handling user input, background processing, or parallel computations. However, it requires careful synchronization to avoid race conditions and deadlocks.
Multithreading enables a program to run multiple threads concurrently, improving performance and responsiveness. Each thread runs independently but shares the same memory, making efficient resource utilization possible. It's ideal for tasks like handling user input, background processing, or parallel computations. However, it requires careful synchronization to avoid race conditions and deadlocks.
1. What is Multithreading?
Multithreading in Java is a process of executing multiple threads simultaneously to maximize CPU utilization. A thread is the smallest unit of a process that can be scheduled independently.
In a game:
Java executes threads (lightweight processes) within a JVM, sharing memory and resources. No code required here.
Thread states: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED.
Thread thread = new Thread(() -> System.out.println("Running"));
System.out.println(thread.getState()); // NEW
thread.start();
System.out.println(thread.getState()); // RUNNABLE
Creating Threads:
1. Using Thread Class:
package com.niteshsynergy.multithreading;
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread running using Thread class");
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
2. Using Runnable Interface:
package com.niteshsynergy.multithreading;
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Thread running using Runnable interface");
}
}
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
3. Using Anonymous Inner Class:
package com.niteshsynergy.multithreading;
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Thread running using anonymous inner class");
}
});
thread.start();
}
}
4. Using Lambda Expression (Java 8+):
package com.niteshsynergy.multithreading;
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("Thread running using lambda expression"));
thread.start();
}
}
5. Using ExecutorService:
package com.niteshsynergy.multithreading;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> System.out.println("Thread running using ExecutorService"));
executor.shutdown();
}
}
6. Using Callable with ExecutorService:
package com.niteshsynergy.multithreading;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Main {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
Callable<String> task = () -> {
return "Thread running using Callable and ExecutorService";
};
try {
Future<String> future = executor.submit(task);
System.out.println(future.get());
} catch (Exception e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
}
}
Creating a complete real-time, complex project is extensive, but I can provide a full implementation for a simplified real-time multiplayer gaming application using Java, showcasing multithreading, socket communication, and concurrency.
Here’s an example of a "Multiplayer Tic-Tac-Toe Game":
GameServer
: Listens for players, creates game sessions.GameClient
: Connects players to the server.GameSession
: Handles game logic and state for two players.Shared Board
: Synchronized for thread-safe operations.
package utils;
public class Constants {
public static final int PORT = 5000;
public static final String HOST = "localhost";
}
2. Board.java (Model)
package model;
import java.util.Arrays;
public class Board {
private char[][] board = new char[3][3];
private char winner = '\0';
public Board() {
for (char[] row : board) {
Arrays.fill(row, ' ');
}
}
public synchronized boolean makeMove(int row, int col, char symbol) {
if (row < 0 || row >= 3 || col < 0 || col >= 3 || board[row][col] != ' ') {
return false;
}
board[row][col] = symbol;
return true;
}
public synchronized boolean checkWinner(char symbol) {
// Check rows, columns, and diagonals for victory
for (int i = 0; i < 3; i++) {
if ((board[i][0] == symbol && board[i][1] == symbol && board[i][2] == symbol) ||
(board[0][i] == symbol && board[1][i] == symbol && board[2][i] == symbol)) {
winner = symbol;
return true;
}
}
if ((board[0][0] == symbol && board[1][1] == symbol && board[2][2] == symbol) ||
(board[0][2] == symbol && board[1][1] == symbol && board[2][0] == symbol)) {
winner = symbol;
return true;
}
return false;
}
public synchronized boolean isFull() {
for (char[] row : board) {
for (char cell : row) {
if (cell == ' ') return false;
}
}
return true;
}
public synchronized char getWinner() {
return winner;
}
public synchronized void printBoard() {
for (char[] row : board) {
System.out.println(Arrays.toString(row));
}
}
}
package server;
import model.Board;
import java.io.*;
import java.net.Socket;
public class GameSession implements Runnable {
private Socket player1;
private Socket player2;
private Board board;
public GameSession(Socket player1, Socket player2) {
this.player1 = player1;
this.player2 = player2;
this.board = new Board();
}
@Override
public void run() {
try {
DataInputStream input1 = new DataInputStream(player1.getInputStream());
DataOutputStream output1 = new DataOutputStream(player1.getOutputStream());
DataInputStream input2 = new DataInputStream(player2.getInputStream());
DataOutputStream output2 = new DataOutputStream(player2.getOutputStream());
output1.writeUTF("Welcome Player 1 (Symbol: X)");
output2.writeUTF("Welcome Player 2 (Symbol: O)");
char currentSymbol = 'X';
while (true) {
DataInputStream currentInput = (currentSymbol == 'X') ? input1 : input2;
DataOutputStream currentOutput = (currentSymbol == 'X') ? output1 : output2;
currentOutput.writeUTF("Your move (row and column): ");
int row = currentInput.readInt();
int col = currentInput.readInt();
if (board.makeMove(row, col, currentSymbol)) {
if (board.checkWinner(currentSymbol)) {
currentOutput.writeUTF("You win!");
break;
}
if (board.isFull()) {
output1.writeUTF("Game is a draw!");
output2.writeUTF("Game is a draw!");
break;
}
currentSymbol = (currentSymbol == 'X') ? 'O' : 'X';
} else {
currentOutput.writeUTF("Invalid move. Try again.");
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
4. GameServer.java (Main Server)
package server;
import utils.Constants;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class GameServer {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(10);
try (ServerSocket serverSocket = new ServerSocket(Constants.PORT)) {
System.out.println("Game server is running...");
while (true) {
Socket player1 = serverSocket.accept();
System.out.println("Player 1 connected!");
Socket player2 = serverSocket.accept();
System.out.println("Player 2 connected!");
pool.execute(new GameSession(player1, player2));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
5. GameClient.java (Client)
package client;
import utils.Constants;
import java.io.*;
import java.net.Socket;
public class GameClient {
public static void main(String[] args) {
try (Socket socket = new Socket(Constants.HOST, Constants.PORT);
DataInputStream input = new DataInputStream(socket.getInputStream());
DataOutputStream output = new DataOutputStream(socket.getOutputStream())) {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
System.out.println(input.readUTF()); // Welcome message
while (true) {
System.out.println(input.readUTF()); // Your move or result
if (input.available() > 0) break;
System.out.print("Enter row: ");
int row = Integer.parseInt(reader.readLine());
System.out.print("Enter col: ");
int col = Integer.parseInt(reader.readLine());
output.writeInt(row);
output.writeInt(col);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
1. start() Method
Purpose: To start a new thread and execute its run() method in a separate call stack.
Use Case: Starting multiple threads for parallel processing.
class DownloadTask extends Thread {
private final String fileName;
public DownloadTask(String fileName) {
this.fileName = fileName;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " started downloading: " + fileName);
try {
Thread.sleep(2000); // Simulate download time
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " finished downloading: " + fileName);
}
}
public class StartExample {
public static void main(String[] args) {
Thread t1 = new DownloadTask("File1.zip");
Thread t2 = new DownloadTask("File2.zip");
t1.start();
t2.start();
}
}
2. run() Method
Purpose: Defines the task that the thread will execute when started.
Use Case: Customizing thread behavior.
class DataProcessor implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is processing data...");
}
}
public class RunExample {
public static void main(String[] args) {
DataProcessor task = new DataProcessor();
Thread thread = new Thread(task, "ProcessorThread");
thread.start();
}
}
3. sleep() Method
Purpose: Pauses the thread execution for a specified time.
Use Case: Simulating delay in task execution.
class Scheduler extends Thread {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("Task " + i + " executed by " + Thread.currentThread().getName());
try {
Thread.sleep(1000); // Pause for 1 second
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class SleepExample {
public static void main(String[] args) {
Thread scheduler = new Scheduler();
scheduler.start();
}
}
4. join() Method
Purpose: Waits for a thread to finish before continuing execution of the main thread.
Use Case: Ensuring thread completion before proceeding.
class DatabaseMigration extends Thread {
private final String dbName;
public DatabaseMigration(String dbName) {
this.dbName = dbName;
}
@Override
public void run() {
System.out.println("Migrating " + dbName);
try {
Thread.sleep(3000); // Simulate time for migration
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(dbName + " migration completed.");
}
}
public class JoinExample {
public static void main(String[] args) {
Thread db1 = new DatabaseMigration("DB1");
Thread db2 = new DatabaseMigration("DB2");
db1.start();
db2.start();
try {
db1.join(); // Wait for db1 to finish
db2.join(); // Wait for db2 to finish
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("All database migrations are completed.");
}
}
5. yield() Method
Purpose: Suggests the thread scheduler to pause and allow other threads of the same priority to execute.
Use Case: Demonstrating cooperative multitasking.
class CalculationTask extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " calculating...");
if (i == 2) {
System.out.println(Thread.currentThread().getName() + " yielding...");
Thread.yield();
}
}
}
}
public class YieldExample {
public static void main(String[] args) {
Thread t1 = new CalculationTask();
Thread t2 = new CalculationTask();
t1.start();
t2.start();
}
}
6. isAlive() Method
Purpose: Checks if a thread is still running.
Use Case: Monitoring the status of a thread.
class FileUploader extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " uploading file...");
try {
Thread.sleep(2000); // Simulate upload time
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " upload complete.");
}
}
public class IsAliveExample {
public static void main(String[] args) {
Thread uploader = new FileUploader();
uploader.start();
while (uploader.isAlive()) {
System.out.println("Uploader thread is still running...");
}
System.out.println("Uploader thread has finished.");
}
}
7. getName() Method
Purpose: Retrieves the name of the thread.
Use Case: Logging and debugging.
class Task extends Thread {
public Task(String name) {
super(name); // Assign a name to the thread
}
@Override
public void run() {
System.out.println("Thread " + Thread.currentThread().getName() + " is running.");
}
}
public class GetNameExample {
public static void main(String[] args) {
Thread t1 = new Task("Worker-1");
Thread t2 = new Task("Worker-2");
t1.start();
t2.start();
}
}
Purpose: Ensures only one thread accesses a critical section at a time.
Use Case: Prevent data inconsistency during concurrent updates.
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
public class SynchronizedExample {
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) counter.increment();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) counter.increment();
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final Count: " + counter.getCount());
}
}
Purpose: Synchronizes static methods to ensure thread safety across all instances of the class.
Use Case: Shared resources accessed via static methods.
class Database {
public static synchronized void connect(String threadName) {
System.out.println(threadName + " connecting to database...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadName + " disconnected.");
}
}
public class StaticSyncExample {
public static void main(String[] args) {
Thread t1 = new Thread(() -> Database.connect("Thread-1"));
Thread t2 = new Thread(() -> Database.connect("Thread-2"));
t1.start();
t2.start();
}
}
Purpose: Synchronizes specific blocks of code instead of the entire method to improve performance.
Use Case: Optimizing thread-safe operations.
class Printer {
public void print(String message) {
synchronized (this) {
System.out.print("[");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(message + "]");
}
}
}
public class SyncBlockExample {
public static void main(String[] args) {
Printer printer = new Printer();
Thread t1 = new Thread(() -> printer.print("Hello"));
Thread t2 = new Thread(() -> printer.print("World"));
t1.start();
t2.start();
}
}
wait()
, notify()
, notifyAll()
)Purpose: Allows threads to communicate by waiting and notifying when a condition changes.
Use Case: Resource sharing between threads.
class Message {
private String message;
private boolean hasMessage = false;
public synchronized void write(String msg) {
while (hasMessage) {
try {
wait(); // Wait until the message is read
} catch (InterruptedException e) {
e.printStackTrace();
}
}
message = msg;
hasMessage = true;
System.out.println("Message written: " + msg);
notify();
}
public synchronized String read() {
while (!hasMessage) {
try {
wait(); // Wait until the message is written
} catch (InterruptedException e) {
e.printStackTrace();
}
}
hasMessage = false;
notify();
return message;
}
}
public class InterThreadCommExample {
public static void main(String[] args) {
Message msg = new Message();
Thread writer = new Thread(() -> msg.write("Hello World!"));
Thread reader = new Thread(() -> System.out.println("Message read: " + msg.read()));
writer.start();
reader.start();
}
}
Purpose: Demonstrates coordination between producer and consumer threads.
import java.util.LinkedList;
class ProducerConsumer {
private LinkedList<Integer> list = new LinkedList<>();
private final int CAPACITY = 5;
public void produce() throws InterruptedException {
int value = 0;
while (true) {
synchronized (this) {
while (list.size() == CAPACITY) {
wait();
}
System.out.println("Produced: " + value);
list.add(value++);
notify();
Thread.sleep(500);
}
}
}
public void consume() throws InterruptedException {
while (true) {
synchronized (this) {
while (list.isEmpty()) {
wait();
}
int value = list.removeFirst();
System.out.println("Consumed: " + value);
notify();
Thread.sleep(500);
}
}
}
}
public class ProducerConsumerExample {
public static void main(String[] args) {
ProducerConsumer pc = new ProducerConsumer();
Thread producer = new Thread(() -> {
try {
pc.produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread consumer = new Thread(() -> {
try {
pc.consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
producer.start();
consumer.start();
}
}
Purpose: Assign thread execution priority.
public class ThreadPriorityExample {
public static void main(String[] args) {
Thread t1 = new Thread(() -> System.out.println("Thread 1"));
Thread t2 = new Thread(() -> System.out.println("Thread 2"));
Thread t3 = new Thread(() -> System.out.println("Thread 3"));
t1.setPriority(Thread.MIN_PRIORITY); // 1
t2.setPriority(Thread.MAX_PRIORITY); // 10
t3.setPriority(Thread.NORM_PRIORITY); // 5
t1.start();
t2.start();
t3.start();
}
}
Purpose: Organizes threads into groups for easier management.
public class ThreadGroupExample {
public static void main(String[] args) {
ThreadGroup group = new ThreadGroup("Group-A");
Thread t1 = new Thread(group, () -> {
System.out.println(Thread.currentThread().getName() + " running.");
}, "Thread-1");
Thread t2 = new Thread(group, () -> {
System.out.println(Thread.currentThread().getName() + " running.");
}, "Thread-2");
t1.start();
t2.start();
System.out.println("Active threads in group: " + group.activeCount());
group.list();
}
}
In Java, locks are mechanisms used to enforce thread synchronization. There are two main types of locks:
Definition:
An object-level lock is a mechanism that synchronizes access to an instance of a class. It ensures that only one thread can execute synchronized instance methods of an object at a time.
Key Points:
synchronized
is used on an instance method or block.
class SharedResource {
public synchronized void print() {
System.out.println(Thread.currentThread().getName() + " is executing print()");
try {
Thread.sleep(1000); // Simulate some work
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " has finished execution.");
}
}
public class ObjectLevelLockExample {
public static void main(String[] args) {
SharedResource resource1 = new SharedResource();
SharedResource resource2 = new SharedResource();
// Two threads working on the same object
Thread t1 = new Thread(resource1::print, "Thread-1");
Thread t2 = new Thread(resource1::print, "Thread-2");
// A thread working on a different object
Thread t3 = new Thread(resource2::print, "Thread-3");
t1.start();
t2.start();
t3.start();
}
}
Behavior: Thread-1 and Thread-2 will synchronize on the same object resource1, so only one will execute the print() method at a time. Thread-3 operates on resource2 independently since it’s a different instance.
Definition:
A class-level lock is a mechanism that synchronizes access to static methods or blocks. It ensures that only one thread can execute synchronized static methods or blocks of the class at a time, across all instances.
Key Points:
. Acquired when synchronized is used on a static method or block.
.It applies to the class, not to specific instances. Hence, it affects all threads interacting with static synchronized methods, regardless of the object instance.
class SharedResource {
public static synchronized void printStatic() {
System.out.println(Thread.currentThread().getName() + " is executing printStatic()");
try {
Thread.sleep(1000); // Simulate some work
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " has finished execution.");
}
public void printInstance() {
synchronized (SharedResource.class) { // Class-level lock
System.out.println(Thread.currentThread().getName() + " is executing printInstance() with class lock.");
try {
Thread.sleep(1000); // Simulate some work
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " has finished execution.");
}
}
}
public class ClassLevelLockExample {
public static void main(String[] args) {
SharedResource resource1 = new SharedResource();
SharedResource resource2 = new SharedResource();
// Threads accessing static synchronized method (class-level lock)
Thread t1 = new Thread(SharedResource::printStatic, "Thread-1");
Thread t2 = new Thread(SharedResource::printStatic, "Thread-2");
// Threads using synchronized block with class-level lock
Thread t3 = new Thread(resource1::printInstance, "Thread-3");
Thread t4 = new Thread(resource2::printInstance, "Thread-4");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
Behavior: Thread-1 and Thread-2 will synchronize on the class-level lock because printStatic() is a static synchronized method. Thread-3 and Thread-4 will also synchronize on the class-level lock, even though they use different instances, because the synchronized block explicitly locks on the SharedResource.class.
Both race condition and deadlock are common issues in multi-threaded programming, where multiple threads interact with shared resources. These issues can result in incorrect behavior or performance degradation. Here's a breakdown of these concepts:
Definition:
A race condition occurs when two or more threads attempt to modify shared data simultaneously, leading to unpredictable results. The final outcome depends on the timing and order in which the threads execute, which is usually not under the control of the developer.
Example: Consider a simple bank account system where multiple threads update the balance of an account at the same time.
class BankAccount {
private int balance = 0;
public void deposit(int amount) {
int temp = balance;
temp = temp + amount; // Simulating some operation
balance = temp;
}
public int getBalance() {
return balance;
}
}
public class RaceConditionExample {
public static void main(String[] args) {
BankAccount account = new BankAccount();
// Thread 1 deposits 100
Thread t1 = new Thread(() -> account.deposit(100));
// Thread 2 deposits 200
Thread t2 = new Thread(() -> account.deposit(200));
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final Account Balance: " + account.getBalance());
}
}
Problem: In the above example, both threads are attempting to modify the balance at the same time. If both threads read the balance value before it is updated, the result will be incorrect.
How to Solve Race Conditions Solution:
To solve a race condition, we can use synchronization mechanisms such as: synchronized keyword - Ensure that only one thread can access critical sections of code.
Atomic classes - Use classes from java.util.concurrent.atomic (e.g., AtomicInteger, AtomicLong), which provide thread-safe operations.
Example Solution (using synchronized):
class BankAccount {
private int balance = 0;
public synchronized void deposit(int amount) {
int temp = balance;
temp = temp + amount;
balance = temp;
}
public int getBalance() {
return balance;
}
}
public class SynchronizedRaceConditionExample {
public static void main(String[] args) {
BankAccount account = new BankAccount();
// Thread 1 deposits 100
Thread t1 = new Thread(() -> account.deposit(100));
// Thread 2 deposits 200
Thread t2 = new Thread(() -> account.deposit(200));
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final Account Balance: " + account.getBalance());
}
}
In this solution, the deposit method is synchronized, ensuring that only one thread can execute it at a time, thus preventing the race condition.
Definition:
Deadlock occurs when two or more threads are blocked forever because they are each waiting for the other to release a resource. This leads to a situation where none of the threads can proceed, resulting in a "deadlock."
Example: Consider two threads attempting to lock two resources (e.g., Resource A and Resource B). If Thread 1 locks Resource A and waits for Resource B, while Thread 2 locks Resource B and waits for Resource A, both threads will be stuck.
class ResourceA {
synchronized void methodA(ResourceB b) {
System.out.println("Thread 1 locked ResourceA");
b.last();
}
synchronized void last() {
System.out.println("Thread 1 unlocked ResourceA");
}
}
class ResourceB {
synchronized void methodB(ResourceA a) {
System.out.println("Thread 2 locked ResourceB");
a.last();
}
synchronized void last() {
System.out.println("Thread 2 unlocked ResourceB");
}
}
public class DeadlockExample {
public static void main(String[] args) {
ResourceA resourceA = new ResourceA();
ResourceB resourceB = new ResourceB();
Thread t1 = new Thread(() -> resourceA.methodA(resourceB));
Thread t2 = new Thread(() -> resourceB.methodB(resourceA));
t1.start();
t2.start();
}
}
Problem: Thread 1 locks Resource A and waits for Resource B. Thread 2 locks Resource B and waits for Resource A. Both threads are stuck, causing a deadlock.
Example Solution (Lock Ordering):
class ResourceA {
synchronized void methodA(ResourceB b) {
System.out.println("Thread 1 locked ResourceA");
b.last();
}
synchronized void last() {
System.out.println("Thread 1 unlocked ResourceA");
}
}
class ResourceB {
synchronized void methodB(ResourceA a) {
System.out.println("Thread 2 locked ResourceB");
a.last();
}
synchronized void last() {
System.out.println("Thread 2 unlocked ResourceB");
}
}
public class DeadlockResolved {
public static void main(String[] args) {
ResourceA resourceA = new ResourceA();
ResourceB resourceB = new ResourceB();
// Fix by acquiring locks in a specific order
Thread t1 = new Thread(() -> {
synchronized (resourceA) {
System.out.println("Thread 1 locked ResourceA");
synchronized (resourceB) {
resourceB.last();
}
}
});
Thread t2 = new Thread(() -> {
synchronized (resourceB) {
System.out.println("Thread 2 locked ResourceB");
synchronized (resourceA) {
resourceA.last();
}
}
});
t1.start();
t2.start();
}
}
In this example, we ensure that both threads acquire the resources in the same order (ResourceA then ResourceB), which prevents the circular dependency that causes deadlock.
Summary of Solutions Race Condition: Use synchronization techniques like synchronized methods/blocks or atomic operations to avoid concurrent access to shared resources.
Deadlock: To avoid deadlocks, you can: Follow a lock ordering strategy. Avoid holding multiple locks. Implement timeout mechanisms.
2. What is the difference between wait(), notify(), and notifyAll() in Java?
Explanation: wait(): Causes the current thread to wait until another thread sends a signal via notify() or notifyAll(). notify(): Wakes up one thread that is waiting on the object’s monitor. notifyAll(): Wakes up all threads that are waiting on the object’s monitor.
class SharedResource {
private int value = 0;
// Producer
public synchronized void produce() throws InterruptedException {
while (value == 1) {
wait(); // Wait if value is 1
}
value = 1; // Produce
System.out.println("Produced: " + value);
notify(); // Notify consumer
}
// Consumer
public synchronized void consume() throws InterruptedException {
while (value == 0) {
wait(); // Wait if value is 0
}
value = 0; // Consume
System.out.println("Consumed: " + value);
notify(); // Notify producer
}
}
public class WaitNotifyExample {
public static void main(String[] args) {
SharedResource resource = new SharedResource();
Thread producer = new Thread(() -> {
try {
while (true) {
resource.produce();
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread consumer = new Thread(() -> {
try {
while (true) {
resource.consume();
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
producer.start();
consumer.start();
}
}