I'm always excited to take on new projects and collaborate with innovative minds.

Phone

+1 234 567 890

Email

contact@botble.com

Website

https://botble.com

Address

123 Main Street, New York, NY 10001

Social

Multithreading

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.

 

2. Why Multithreading?

  1. Maximizes CPU Utilization: Keeps the CPU busy by performing multiple tasks concurrently.
  2. Improves Performance: Reduces response time for applications like games or server-based systems.
  3. Handles Multiple Tasks: Ideal for real-time use cases like handling concurrent requests in web servers or processing video games.
  4. Responsive Systems: Keeps GUIs responsive (e.g., gaming apps with background music and rendering).

3. Complex Use Case: Gaming (e.g., Free Fire or Racing Games)

In a game:

  • One thread handles user input (keyboard/mouse).
  • Another handles rendering graphics.
  • A third manages background music or network synchronization.

 

Threads vs. Processes

Java executes threads (lightweight processes) within a JVM, sharing memory and resources. No code required here.

 

Thread Lifecycle (States)

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":

Overview: Multiplayer Tic-Tac-Toe

  1. Technology Stack:
    • Java Multithreading: Manages multiple players.
    • Socket Communication: Real-time communication between server and clients.
    • Concurrency: Synchronized board access.
  2. Structure:
    • 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.
image-183.png

 

Code

1. Constants.java (Utilities)

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));
       }
   }
}
 

 

3. GameSession.java (Server)

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();
       }
   }
}
 

image-184.png

 

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();
   }
}
 

image-185.png

 

 

image-186.png

 

1. Synchronized Keyword

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());
   }
}
 

2. Static Synchronization

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();
   }
}
 

3. Synchronizing Blocks

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();
   }
}
 

4. Inter-Thread Communication (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();
   }
}
 

5. Producer-Consumer Problem

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();
   }
}
 

 

6. Thread Priorities

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();
   }
}
 

7. Thread Groups

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();
   }
}
 

image-187.png

 

In Java, locks are mechanisms used to enforce thread synchronization. There are two main types of locks:

  1. Class-Level Lock
  2. Object-Level Lock

    Note: From Nitesh Synergy side this question is not valid ( From our side we say it static member level & instance level lock)

1. Object-Level Lock

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:

  • Acquired when synchronized is used on an instance method or block.
  • It applies only to the specific object instance, meaning different threads can access synchronized methods of different instances simultaneously.

 

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.

 

 

2. Class-Level Lock

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.

 

image-188.png
  • Use object-level locks when synchronizing access to instance-specific data.
  • Use class-level locks when synchronizing access to shared resources or static data across all instances.

 

Race Condition and Deadlock

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:

 

1. Race Condition

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.

 

 

2. Deadlock

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.

 

How to Solve Deadlock

  1. Avoid Nested Locks: Minimize the number of locks that need to be acquired at the same time.
  2. Lock Ordering: Establish a consistent order in which locks are acquired to avoid circular dependencies.
  3. Timeouts: Use timeouts when trying to acquire locks, allowing threads to back out if they cannot acquire all required resources.
  4. Deadlock Detection: Periodically check if a deadlock situation exists and try to recover from it.

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();
   }
}
 

35 min read
नव. 21, 2024
By Nitesh Synergy
Share