Programming in Java
Unit 11: Exceptions, Assertions & Multithreading
From graceful error handling to concurrent execution — master Java's exception hierarchy, assertion mechanisms, and multithreading to build production-grade applications like Zerodha and Paytm.
⏱️ 8 hrs theory + 6 hrs lab | 💰 ₹15K–₹50K/month | 📝 30 MCQs (Bloom's Mapped)
💼 Jobs this unlocks: Java Backend Developer (₹6–12 LPA) | Multithreaded Systems Engineer (₹8–18 LPA) | Fintech Developer (₹10–25 LPA)
Opening Hook — When 10 Lakh Orders Hit Zerodha at 9:15 AM
🏢 How Zerodha Handles ₹15 Lakh Crore Without Crashing
Every trading day at 9:15 AM, the Indian stock market opens. In that single second, Zerodha processes over 10 lakh concurrent buy/sell orders. Each order is a separate thread. When two traders try to modify the same shared portfolio balance simultaneously, Java's synchronized keyword ensures only one thread touches the balance at a time — preventing ₹15 lakh crore worth of data corruption.
But what happens when a UPI payment to Zerodha fails mid-transaction? A network timeout? An invalid stock symbol? The system doesn't crash — it catches the exception gracefully, logs the error, sends you a friendly "Order failed — please retry" message, and keeps running for the other 9,99,999 orders.
This unit teaches you both pillars: Exceptions (how to handle failures without crashing) and Multithreading (how to run thousands of tasks simultaneously). Together, they make Java the #1 language for fintech, banking, and high-frequency trading systems across India.
Learning Outcomes — Bloom's Taxonomy Mapped
| Bloom's Level | Learning Outcome |
|---|---|
| 🔴 Remember | List the exception hierarchy: Throwable → Error/Exception → RuntimeException, and differentiate checked vs unchecked exceptions |
| 🔴 Remember | Identify the five states in the Java thread lifecycle: NEW → RUNNABLE → BLOCKED → WAITING → TERMINATED |
| 🟠 Understand | Explain the execution flow of try-catch-finally and how try-with-resources auto-closes resources |
| 🟠 Understand | Describe how synchronized prevents race conditions in concurrent access to shared data |
| 🟢 Apply | Write custom checked and unchecked exceptions (InsufficientBalanceException) with exception chaining |
| 🟢 Apply | Create multithreaded programs using Thread class, Runnable interface, and lambda expressions |
| 🟡 Analyze | Analyze deadlock scenarios and determine prevention strategies using lock ordering |
| 🟡 Analyze | Compare Thread class extension vs Runnable implementation vs lambda-based thread creation |
| 🔵 Evaluate | Evaluate when to use ExecutorService thread pools vs manual thread management in production systems |
| 🔵 Evaluate | Assess the trade-offs of assert vs exception throwing for input validation in different deployment scenarios |
| 🟣 Create | Design a producer-consumer system (Zerodha Order Book) using wait/notify with full exception handling |
| 🟣 Create | Build a multithreaded bank transfer simulation with synchronized blocks and custom exception chains |
Concept Explanation — Exceptions, Assertions & Multithreading
📛 PART A: Exception Handling
Think of exceptions like this: You're withdrawing ₹5,000 from an ATM. What could go wrong? Insufficient balance, network failure, card expired, ATM out of cash. Each of these is an exception — an abnormal condition that disrupts normal program flow. Java doesn't crash when these happen — it catches them and responds gracefully.
1. Exception Hierarchy in Java
Java organises all errors and exceptions into a class hierarchy rooted at Throwable. Understanding this tree is the foundation of exception handling.
🌳 The Exception Family Tree
Hierarchy Throwable / \ Error Exception / \ / \ OutOfMemoryError IOException RuntimeException StackOverflowError SQLException / | \ VirtualMachineError NullPointerException ArrayIndexOutOfBoundsException ArithmeticException ClassCastException IllegalArgumentExceptionThrowable (Root)
The superclass of all errors and exceptions in Java. Every throwable has a message (getMessage()) and a stack trace (printStackTrace()).
Serious JVM-level problems you cannot recover from. OutOfMemoryError, StackOverflowError. These are like a building collapsing — you can't "handle" it, you evacuate. Never catch Errors in production code.
Exception → Checked ExceptionsExceptions the compiler forces you to handle at compile time. IOException, SQLException, FileNotFoundException. The compiler says: "I know this can fail — prove you've handled it." You must use try-catch or declare throws.
Exceptions that occur at runtime due to programming bugs. NullPointerException, ArrayIndexOutOfBoundsException. The compiler doesn't force you to catch them — but you should fix the bug that causes them.
| Aspect | Checked Exception | Unchecked Exception |
|---|---|---|
| Checked at | Compile time | Runtime |
| Must handle? | Yes (try-catch or throws) | No (optional) |
| Extends | Exception (not RuntimeException) | RuntimeException |
| Examples | IOException, SQLException, FileNotFoundException | NullPointerException, ArithmeticException |
| Cause | External factors (file, network, DB) | Programming bugs (null access, bad index) |
| Indian Analogy | Train cancelled (external) — you must plan B | Forgot your ticket (your bug) — fix yourself |
catch(Error e).
2. try-catch-finally Execution Flow
The try-catch-finally block is Java's mechanism for handling exceptions. Think of it as a safety net under a tightrope walker.
🔄 Execution Flow Rules
Rule 1: Code in try block executes normally until an exception occurs.
Rule 2: If exception occurs, remaining try code is SKIPPED — control jumps to matching catch.
Rule 3: finally block ALWAYS executes — exception or no exception, caught or uncaught. Only exception: System.exit().
Rule 4: If no matching catch is found, exception propagates up the call stack.
Rule 5: Multiple catch blocks are checked top-to-bottom — put specific exceptions BEFORE general ones.
Java public class ATMWithdrawal { public static void main(String[] args) { double balance = 5000.0; double amount = 7000.0; try { System.out.println("Attempting withdrawal of ₹" + amount); if (amount > balance) { throw new ArithmeticException("Insufficient balance!"); } balance -= amount; System.out.println("Withdrawal successful. New balance: ₹" + balance); } catch (ArithmeticException e) { System.out.println("❌ Transaction failed: " + e.getMessage()); System.out.println("💡 Please deposit more funds or try smaller amount."); } finally { System.out.println("📋 Transaction log updated. ATM session ended."); // This ALWAYS runs — cleanup code goes here } } }
Multi-catch (Java 7+): catch(A | B e)
When multiple exceptions need the same handling, use multi-catch to avoid code duplication:
Java try { // Code that might throw different exceptions String data = readFromFile("orders.csv"); int quantity = Integer.parseInt(data); } catch (IOException | NumberFormatException e) { // Single block handles both — DRY principle! System.out.println("Error processing order: " + e.getMessage()); logError(e); }
e inside a multi-catch block. This is by design — since e could be either type, reassigning it would create ambiguity.
try-with-resources (Java 7+): AutoCloseable
Resources like files, database connections, and sockets must be closed after use. Before Java 7, you'd write verbose finally blocks. Now, any class implementing AutoCloseable can be auto-closed:
Java // ❌ OLD WAY — verbose and error-prone BufferedReader reader = null; try { reader = new BufferedReader(new FileReader("transactions.csv")); String line = reader.readLine(); } catch (IOException e) { e.printStackTrace(); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { /* swallowed */ } } } // ✅ NEW WAY — clean, safe, automatic try (BufferedReader reader = new BufferedReader(new FileReader("transactions.csv"))) { String line = reader.readLine(); System.out.println("Transaction: " + line); } catch (IOException e) { System.out.println("Could not read transactions: " + e.getMessage()); } // reader.close() is called automatically — even if exception occurs!
3. throw vs throws
| Aspect | throw | throws |
|---|---|---|
| Used in | Method body | Method signature (declaration) |
| Purpose | Actually throws an exception object | Declares that method MAY throw exception |
| Followed by | Exception instance (throw new XYZ()) | Exception class name(s) |
| Count | One exception at a time | Multiple: throws A, B, C |
Java // 'throws' — declares the method may throw this exception public void transferMoney(double amount) throws InsufficientBalanceException { if (amount > balance) { // 'throw' — actually creates and throws the exception throw new InsufficientBalanceException("Balance ₹" + balance + " < ₹" + amount); } balance -= amount; }
4. Custom Exceptions — InsufficientBalanceException for Paytm
Java's built-in exceptions don't cover business-specific errors. Paytm needs InsufficientBalanceException, Zerodha needs MarketClosedException. You create your own by extending Exception (checked) or RuntimeException (unchecked).
Java — Custom Checked Exception // Checked — compiler forces caller to handle it public class InsufficientBalanceException extends Exception { private double balance; private double amount; public InsufficientBalanceException(String message, double balance, double amount) { super(message); this.balance = balance; this.amount = amount; } public double getBalance() { return balance; } public double getAmount() { return amount; } public double getDeficit() { return amount - balance; } }
Java — Custom Unchecked Exception // Unchecked — no forced handling (for programming errors) public class InvalidStockSymbolException extends RuntimeException { public InvalidStockSymbolException(String symbol) { super("Invalid stock symbol: " + symbol + ". Use NSE/BSE format like RELIANCE, TCS, INFY"); } }
Java — Using Custom Exception (Paytm Wallet) public class PaytmWallet { private double balance; private String userId; public PaytmWallet(String userId, double balance) { this.userId = userId; this.balance = balance; } public void pay(String merchant, double amount) throws InsufficientBalanceException { if (amount > balance) { throw new InsufficientBalanceException( "Paytm wallet balance insufficient for " + merchant, balance, amount ); } balance -= amount; System.out.println("✅ ₹" + amount + " paid to " + merchant + ". Balance: ₹" + balance); } public static void main(String[] args) { PaytmWallet wallet = new PaytmWallet("user_mumbai_42", 500.0); try { wallet.pay("Zomato", 350.0); // ✅ Success wallet.pay("Swiggy", 300.0); // ❌ Fails — only ₹150 left } catch (InsufficientBalanceException e) { System.out.println("❌ " + e.getMessage()); System.out.println("💡 Add ₹" + e.getDeficit() + " to complete this payment."); } } }
5. Exception Chaining (initCause)
Sometimes an exception is caused by another exception. Exception chaining preserves the original cause while wrapping it in a more meaningful business exception.
Java public void processUPIPayment(String upiId, double amount) throws PaymentException { try { connectToUPIGateway(upiId); // May throw IOException debitAccount(amount); // May throw SQLException } catch (IOException e) { PaymentException pe = new PaymentException("UPI gateway unreachable"); pe.initCause(e); // Chain the original IOException as root cause throw pe; } catch (SQLException e) { // Alternative: constructor chaining throw new PaymentException("Database error during debit", e); } } // Later, debugging: catch (PaymentException pe) { System.out.println("Payment failed: " + pe.getMessage()); System.out.println("Root cause: " + pe.getCause().getMessage()); }
6. Assertions — assert Statement & java -ea
Assertions are sanity checks during development. They verify conditions that should always be true. If the condition is false, the JVM throws AssertionError.
🛡️ assert Statement
Syntax 1: assert condition;
Syntax 2: assert condition : "error message";
Enable: java -ea MyProgram (assertions are DISABLED by default)
When to use: Internal invariants, post-conditions, unreachable code markers
When NOT to use: Input validation (user input), argument checking in public methods
Java public class AssertionDemo { public static double calculateDiscount(double price, double discountPercent) { assert price >= 0 : "Price cannot be negative: " + price; assert discountPercent >= 0 && discountPercent <= 100 : "Invalid discount: " + discountPercent + "%"; double finalPrice = price - (price * discountPercent / 100); assert finalPrice >= 0 : "Final price went negative!"; // post-condition return finalPrice; } public static void main(String[] args) { System.out.println("Price after 20% off: ₹" + calculateDiscount(1000, 20)); System.out.println("Price after 150% off: ₹" + calculateDiscount(1000, 150)); // Run with: java -ea AssertionDemo // Second call triggers: AssertionError: Invalid discount: 150.0% } }
java without -ea). Use IllegalArgumentException for public API validation. Assertions are for internal sanity checks that "should never fail if the code is correct."
⚡ PART B: Multithreading
A thread is the smallest unit of execution within a process. When you run a Java program, it starts with one thread (the main thread). Multithreading lets you run multiple threads simultaneously — like Zerodha processing lakhs of buy and sell orders at the same time.
7. Creating Threads — Thread Class vs Runnable Interface
Method 1: Extending Thread Class
Java class BuyOrderThread extends Thread { private String stock; private int quantity; public BuyOrderThread(String stock, int quantity) { this.stock = stock; this.quantity = quantity; } @Override public void run() { System.out.println(Thread.currentThread().getName() + " → BUY " + quantity + " shares of " + stock); try { Thread.sleep(1000); } catch (InterruptedException e) { } System.out.println(Thread.currentThread().getName() + " → Order filled ✅"); } } // Usage: BuyOrderThread t1 = new BuyOrderThread("RELIANCE", 100); BuyOrderThread t2 = new BuyOrderThread("TCS", 50); t1.start(); // DO NOT call run() — call start()! t2.start(); // Both threads run concurrently
Method 2: Implementing Runnable Interface (Preferred)
Java class SellOrderRunnable implements Runnable { private String stock; private int quantity; public SellOrderRunnable(String stock, int quantity) { this.stock = stock; this.quantity = quantity; } @Override public void run() { System.out.println(Thread.currentThread().getName() + " → SELL " + quantity + " shares of " + stock); } } // Usage — Runnable is passed TO a Thread object Thread t3 = new Thread(new SellOrderRunnable("INFY", 200), "SellThread-1"); t3.start();
Method 3: Lambda-based Thread Creation (Java 8+, Cleanest)
Java // Since Runnable is a @FunctionalInterface with one abstract method (run), // it can be replaced with a lambda expression: Thread t4 = new Thread(() -> { System.out.println(Thread.currentThread().getName() + " → Processing order via Lambda"); try { Thread.sleep(500); } catch (InterruptedException e) { } System.out.println(Thread.currentThread().getName() + " → Lambda order done ✅"); }, "LambdaThread"); t4.start();
| Approach | Pros | Cons | When to Use |
|---|---|---|---|
extends Thread | Simple, direct | Can't extend another class (Java single inheritance) | Quick prototypes only |
implements Runnable | Allows extending another class, separation of concerns | Slightly more verbose | Production code (recommended) |
| Lambda | Concise, modern, inline | Less reusable for complex logic | Short tasks, one-off threads |
8. Thread Lifecycle — Five States
🔄 Thread State Diagram
Lifecycle new Thread() start() Scheduler picks run() ends | | | | [NEW] ──────────> [RUNNABLE] ──────────> [RUNNING] ──────────> [TERMINATED] ↑ | | | sleep()/wait()/ | | blocked on I/O | ↓ notify()/ [BLOCKED] / sleep over/ [WAITING] / I/O complete [TIMED_WAITING]
NEW: Thread object created but start() not yet called.
RUNNABLE: start() called. Thread is ready to run, waiting for CPU scheduling.
RUNNING: Thread's run() method is actually executing on CPU.
BLOCKED: Thread waiting to acquire a lock (trying to enter synchronized block).
WAITING: Thread called wait(), join(), or LockSupport.park(). Waiting indefinitely for notification.
TIMED_WAITING: Thread called sleep(ms), wait(ms), or join(ms). Wakes up after timeout.
TERMINATED: run() method completed or exception thrown. Thread cannot be restarted.
9. Key Thread Methods
| Method | Description | Example Use Case |
|---|---|---|
start() | Starts thread, calls run() in new thread | Launching an order processing thread |
sleep(ms) | Pauses current thread for ms milliseconds | Simulating network delay |
join() | Current thread waits for this thread to die | Wait for all downloads to complete before zipping |
interrupt() | Sets interrupt flag; sleeping threads get InterruptedException | Cancelling a long-running order search |
yield() | Hints scheduler to give other threads a chance | Cooperative multitasking (rarely used) |
isAlive() | Returns true if thread is started but not yet terminated | Checking if background task is still running |
setDaemon(true) | Makes thread a daemon (dies when all user threads end) | Background logging thread |
Java — join() Example public class JoinDemo { public static void main(String[] args) throws InterruptedException { Thread download = new Thread(() -> { System.out.println("📥 Downloading stock data..."); try { Thread.sleep(2000); } catch (InterruptedException e) { } System.out.println("📥 Download complete!"); }); download.start(); download.join(); // Main thread WAITS here until download finishes System.out.println("📊 Now processing downloaded data..."); } }
10. Synchronization — Preventing Race Conditions
When multiple threads access shared data simultaneously, you get race conditions — unpredictable, incorrect results. Java's synchronized keyword ensures only one thread enters a critical section at a time.
Java — Race Condition (BUG!) class UnsafeBankAccount { private double balance = 10000; // ❌ NOT synchronized — two threads can withdraw simultaneously! public void withdraw(double amount) { if (balance >= amount) { // Thread A checks: 10000 >= 8000 ✅ // Thread B checks: 10000 >= 7000 ✅ (SAME balance!) balance -= amount; // Both withdraw — balance goes NEGATIVE! ₹-5000 } } }
Java — Fixed with synchronized class SafeBankAccount { private double balance = 10000; // ✅ Synchronized method — only ONE thread can execute at a time public synchronized void withdraw(double amount) { if (balance >= amount) { System.out.println(Thread.currentThread().getName() + " withdrawing ₹" + amount); balance -= amount; System.out.println("Remaining balance: ₹" + balance); } else { System.out.println(Thread.currentThread().getName() + " — Insufficient balance!"); } } // ✅ Synchronized block — more granular control public void transfer(SafeBankAccount target, double amount) { synchronized (this) { // Lock on source account if (balance >= amount) { balance -= amount; target.deposit(amount); } } } public synchronized void deposit(double amount) { balance += amount; } public synchronized double getBalance() { return balance; } }
11. wait() / notify() / notifyAll() — Inter-Thread Communication
Sometimes threads need to coordinate. A producer thread creates data, a consumer thread processes it. The consumer must wait when the queue is empty, and the producer must notify the consumer when new data arrives.
Java — Producer-Consumer (Zerodha Order Book) import java.util.LinkedList; import java.util.Queue; public class ZerodhaOrderBook { private final Queue<String> orders = new LinkedList<>(); private final int MAX_CAPACITY = 5; // PRODUCER — Traders placing orders public synchronized void placeOrder(String order) throws InterruptedException { while (orders.size() == MAX_CAPACITY) { System.out.println("📋 Order book FULL — " + Thread.currentThread().getName() + " waiting..."); wait(); // Release lock and wait } orders.add(order); System.out.println("📥 " + Thread.currentThread().getName() + " placed: " + order + " [Queue size: " + orders.size() + "]"); notifyAll(); // Wake up consumers } // CONSUMER — Matching engine processing orders public synchronized String executeOrder() throws InterruptedException { while (orders.isEmpty()) { System.out.println("⏳ Order book EMPTY — " + Thread.currentThread().getName() + " waiting..."); wait(); // Release lock and wait } String order = orders.poll(); System.out.println("✅ " + Thread.currentThread().getName() + " executed: " + order + " [Queue size: " + orders.size() + "]"); notifyAll(); // Wake up producers return order; } public static void main(String[] args) { ZerodhaOrderBook book = new ZerodhaOrderBook(); // Producer threads (traders) Thread trader1 = new Thread(() -> { String[] stocks = {"BUY RELIANCE 100", "BUY TCS 50", "SELL INFY 200"}; for (String s : stocks) { try { book.placeOrder(s); Thread.sleep(300); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }, "Trader-Ravi"); Thread trader2 = new Thread(() -> { String[] stocks = {"SELL HDFC 75", "BUY WIPRO 150", "BUY SBI 300"}; for (String s : stocks) { try { book.placeOrder(s); Thread.sleep(200); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }, "Trader-Priya"); // Consumer thread (matching engine) Thread engine = new Thread(() -> { for (int i = 0; i < 6; i++) { try { book.executeOrder(); Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }, "MatchEngine"); trader1.start(); trader2.start(); engine.start(); } }
12. Deadlock — Detection & Prevention
Deadlock occurs when two or more threads are permanently blocked, each waiting for a lock held by the other. It's a Mexican standoff — neither can proceed.
💀 Deadlock Scenario
Deadlock
Thread A: Locks Account1 → wants Account2 (BLOCKED — B has it)
Thread B: Locks Account2 → wants Account1 (BLOCKED — A has it)
Both waiting forever → DEADLOCK! 💀
Prevention Strategies:
1. Lock Ordering: Always acquire locks in the same order (e.g., by account ID)
2. Lock Timeout: Use tryLock(timeout) from ReentrantLock
3. Avoid Nested Locks: Minimize holding multiple locks
4. Use Higher-Level Concurrency: java.util.concurrent collections
Java — Deadlock Prevention via Lock Ordering public void safeTransfer(SafeBankAccount from, SafeBankAccount to, double amount) { // Always lock the account with the smaller ID first SafeBankAccount first = from.getId() < to.getId() ? from : to; SafeBankAccount second = from.getId() < to.getId() ? to : from; synchronized (first) { synchronized (second) { if (from.getBalance() >= amount) { from.withdraw(amount); to.deposit(amount); } } } }
13. Thread Pools — ExecutorService
Creating a new thread for every task is expensive. Thread pools reuse a fixed set of threads, queuing tasks when all threads are busy. This is how Zerodha handles 10 lakh concurrent requests with far fewer actual threads.
Java import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ZerodhaThreadPool { public static void main(String[] args) { // Create a pool of 4 threads — reused for all orders ExecutorService pool = Executors.newFixedThreadPool(4); String[] orders = { "BUY RELIANCE 100", "SELL TCS 50", "BUY INFY 200", "SELL HDFC 75", "BUY WIPRO 150", "SELL SBI 300", "BUY BHARTIARTL 80", "SELL HCLTECH 120" }; for (String order : orders) { pool.submit(() -> { System.out.println(Thread.currentThread().getName() + " → Processing: " + order); try { Thread.sleep(500); } catch (InterruptedException e) { } System.out.println(Thread.currentThread().getName() + " → Completed: " + order + " ✅"); }); } pool.shutdown(); // No new tasks accepted; existing tasks complete System.out.println("All orders submitted to pool."); } }
Executors.newFixedThreadPool(n) where n ≈ number of CPU cores for CPU-bound tasks, or n ≈ cores × 2 for I/O-bound tasks. Zerodha uses Netty's event loop (similar concept) to handle lakhs of connections with just a few threads.
14. Full Code: FileReaderWithResources
Java import java.io.*; public class FileReaderWithResources { public static void main(String[] args) { String filename = "transactions.csv"; // try-with-resources — auto-closes reader try (BufferedReader reader = new BufferedReader(new FileReader(filename))) { String line; int lineNum = 0; System.out.println("📄 Reading " + filename + "..."); while ((line = reader.readLine()) != null) { lineNum++; System.out.println(" Line " + lineNum + ": " + line); } System.out.println("✅ Total lines read: " + lineNum); } catch (FileNotFoundException e) { System.out.println("❌ File not found: " + filename); System.out.println("💡 Create the file or check the path."); } catch (IOException e) { System.out.println("❌ Error reading file: " + e.getMessage()); } // reader.close() is automatically called here — even if exception occurred! } }
15. Full Code: BankTransferThreads
Java public class BankTransferThreads { static class Account { private final int id; private double balance; public Account(int id, double balance) { this.id = id; this.balance = balance; } public int getId() { return id; } public double getBalance() { return balance; } public void deposit(double amt) { balance += amt; } public void withdraw(double amt) throws InsufficientBalanceException { if (amt > balance) { throw new InsufficientBalanceException( "Account " + id + ": ₹" + balance + " < ₹" + amt, balance, amt); } balance -= amt; } } public static void safeTransfer(Account from, Account to, double amount) { // Lock ordering by account ID to prevent deadlock Account first = from.getId() < to.getId() ? from : to; Account second = from.getId() < to.getId() ? to : from; synchronized (first) { synchronized (second) { try { from.withdraw(amount); to.deposit(amount); System.out.println("✅ " + Thread.currentThread().getName() + ": ₹" + amount + " transferred from Acc-" + from.getId() + " to Acc-" + to.getId()); } catch (InsufficientBalanceException e) { System.out.println("❌ " + Thread.currentThread().getName() + ": " + e.getMessage()); } } } } public static void main(String[] args) throws InterruptedException { Account ravi = new Account(1001, 50000); Account priya = new Account(1002, 30000); Thread t1 = new Thread(() -> { for (int i = 0; i < 5; i++) safeTransfer(ravi, priya, 5000); }, "NEFT-Thread"); Thread t2 = new Thread(() -> { for (int i = 0; i < 3; i++) safeTransfer(priya, ravi, 8000); }, "UPI-Thread"); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("\n📊 Final Balances:"); System.out.println(" Ravi (Acc-1001): ₹" + ravi.getBalance()); System.out.println(" Priya (Acc-1002): ₹" + priya.getBalance()); } }
Learn by Doing — 3-Tier Lab Structure (Zerodha Order Book)
🟢 Tier 1 — GUIDED: Basic Exception Handling for Zerodha Orders
Step 1: Create OrderException
Create a custom checked exception OrderException that extends Exception with fields for orderId and reason.
Step 2: Create Order Validator
Write a method validateOrder(String stock, int qty, double price) that throws OrderException if: stock symbol is null/empty, quantity ≤ 0, or price ≤ 0.
Step 3: Handle with try-catch-finally
In main, call validateOrder with valid and invalid inputs. Catch exceptions and print user-friendly messages. Use finally to log "Order validation attempt completed."
Step 4: Test edge cases
Test with: null stock, negative quantity, zero price, valid order. Verify each exception is caught properly.
🟡 Tier 2 — SEMI-GUIDED: Multithreaded Order Processing
Your Mission:
Build a multithreaded Zerodha order processor where 3 trader threads submit orders to a shared order book and 2 matching engine threads execute them.
Hints:
- Use the
ZerodhaOrderBookclass from Section C as your base - Add exception handling: invalid stock symbols should throw
InvalidStockSymbolException - Use
synchronizedon the order queue - Implement
wait()when queue is full (capacity 10) or empty - Use
notifyAll()after each add/remove
Thread.sleep(random) to simulate real-world network latency. Add a shutdown mechanism using a poison pill ("SHUTDOWN" order).
🔴 Tier 3 — OPEN CHALLENGE: Full Zerodha Simulation with Thread Pool
The Brief:
Build a complete stock exchange simulation with:
- Thread Pool: Use
ExecutorServicewith 8 threads - Custom Exceptions: InsufficientBalanceException, MarketClosedException, InvalidOrderException
- Producer-Consumer: Buy/Sell order queues with wait/notify
- Synchronized portfolio: Thread-safe balance updates
- Exception chaining: NetworkException → OrderFailedException
- try-with-resources: Log all transactions to a file
- Assertions: Assert portfolio balance never goes negative
Problem Set — Syntax, Programming, Industry & Interview
Syntax Questions (5)
SQ1. Identify and fix the error in this code:
Java try { int x = 10 / 0; } catch (Exception e) { System.out.println("General"); } catch (ArithmeticException e) { // ← ERROR! System.out.println("Arithmetic"); }
Answer: Compilation error — specific exception (ArithmeticException) must come BEFORE general exception (Exception). Reverse the catch order.
SQ2. What is the output?
Java System.out.println("A"); try { System.out.println("B"); int x = 10/0; System.out.println("C"); } catch(ArithmeticException e) { System.out.println("D"); } finally { System.out.println("E"); } System.out.println("F");
Answer: A B D E F — "C" is skipped (exception occurred before it), finally always runs, execution continues after try-catch.
SQ3. Will this compile?
Java public void readFile() { FileReader fr = new FileReader("test.txt"); }
Answer: No — FileReader constructor throws checked FileNotFoundException. Must use try-catch or add throws FileNotFoundException.
SQ4. Spot the bug:
Java Thread t = new Thread(() -> System.out.println("Hello")); t.run(); // ← BUG!
Answer: Calling run() executes in the CURRENT thread, not a new one. Must call t.start() to create a new thread.
SQ5. What does assert x > 0 : "Negative!"; do when run with java -ea and x = -5?
Answer: Throws AssertionError with message "Negative!" because the condition x > 0 is false and assertions are enabled.
Programming Questions (8)
PQ1. Write a Java program that takes an array of integers and handles ArrayIndexOutOfBoundsException when accessing an invalid index. Print the exception message and the valid range.
PQ2. Create a BankAccount class with withdraw() that throws a custom InsufficientFundsException. Test with multiple withdrawal attempts in a loop.
PQ3. Write a program using try-with-resources to read a file line by line. Handle FileNotFoundException and IOException separately with meaningful messages.
PQ4. Create two threads: one prints even numbers (2–20) and the other prints odd numbers (1–19). Use Thread.sleep(100) between each print. Observe interleaved output.
PQ5. Implement a Counter class with increment() method. Create 3 threads that each increment the counter 1000 times. First run without synchronized (show bug), then with synchronized (show fix). Print final count.
PQ6. Write a program demonstrating multi-catch: parse an integer from a string and access an array element. Handle NumberFormatException | ArrayIndexOutOfBoundsException in a single catch block.
PQ7. Create a producer-consumer program where a producer adds numbers 1–10 to a shared list, and a consumer removes and prints them. Use wait() and notify().
PQ8. Write a program using ExecutorService.newFixedThreadPool(3) that processes 10 tasks. Each task prints the thread name and a task number with a 500ms delay.
Industry Questions (3) — Paytm Custom Exception Chain
IQ1. Design a Paytm payment exception hierarchy: PaymentException (base) → InsufficientBalanceException, UPITimeoutException, InvalidUPIIdException. Implement exception chaining where UPITimeoutException wraps a java.net.SocketTimeoutException. Write the full implementation with test cases.
IQ2. Zerodha's matching engine processes buy and sell orders concurrently. Design a MatchingEngine class with a thread-safe order book (use synchronized + wait/notify). Handle cases: market closed (throw MarketClosedException), invalid symbol (throw InvalidSymbolException), insufficient margin (throw InsufficientMarginException). Use exception chaining to wrap low-level SQLException in business exceptions.
IQ3. HDFC Bank needs a multithreaded NEFT processing system. Design a system where: (a) 5 customer threads submit NEFT requests, (b) 2 processor threads process them from a shared queue, (c) use ExecutorService for the processor pool, (d) custom exceptions for invalid IFSC code, daily limit exceeded, and beneficiary not found. Write complete code with proper shutdown.
Interview Questions (3)
INT1. "What is the difference between throw and throws? When would you use each in a fintech application like Razorpay?"
Model Answer: throw is used inside a method body to actually throw an exception object (e.g., throw new PaymentException("Gateway timeout")). throws is used in the method signature to declare that the method might throw specified checked exceptions, forcing callers to handle them. In Razorpay, a processPayment() method would declare throws PaymentException, GatewayException in its signature, and internally use throw to create specific exception instances when failures occur.
INT2. "Explain what happens if you call run() instead of start() on a Thread object. How would this affect performance in a system like Zerodha?"
Model Answer: Calling run() executes the method in the CURRENT thread — no new thread is created. It becomes a normal method call. In Zerodha, if you called run() instead of start() for each order, all 10 lakh orders would process sequentially on one thread instead of concurrently. This would turn millisecond execution into hours, making the platform unusable during market opening.
INT3. "How do you prevent deadlock in a banking system where multiple threads transfer money between accounts?"
Model Answer: Use lock ordering — always acquire locks on accounts in a consistent order (e.g., by account number). If Thread A transfers from Acc-1001 to Acc-1002, it locks 1001 first, then 1002. If Thread B transfers from Acc-1002 to Acc-1001, it also locks 1001 first (not 1002). Since both threads acquire locks in the same order, deadlock is impossible. Additionally, use tryLock() with timeout for robustness.
MCQ Assessment Bank — 30 Questions (Bloom's Mapped)
Remember / Identify (Q1–Q6)
Which class is at the top of Java's exception hierarchy?
- Exception
- Error
- Throwable
- RuntimeException
Which of the following is a checked exception?
- NullPointerException
- ArrayIndexOutOfBoundsException
- IOException
- ArithmeticException
The finally block is executed:
- Only when an exception occurs
- Only when no exception occurs
- Always, regardless of exception
- Only with try-with-resources
To create a thread using lambda, the interface used is:
- Callable
- Runnable
- Thread
- Executor
Which keyword is used to declare that a method might throw an exception?
- throw
- throws
- try
- catch
void method() throws IOException. 'throw' is used inside the method body to actually throw an exception.Which of these is NOT a valid thread state in Java?
- NEW
- RUNNING
- BLOCKED
- TERMINATED
Understand / Explain (Q7–Q12)
Why does Java require checked exceptions to be handled at compile time?
- To make programs run faster
- To force developers to plan for recoverable error scenarios
- To prevent runtime exceptions
- To reduce memory usage
What happens if you call thread.run() instead of thread.start()?
- A new thread is created
- The run() method executes in the current thread
- A compilation error occurs
- The thread is started twice
Why is try-with-resources preferred over try-catch-finally for resource management?
- It's faster at runtime
- It automatically closes resources, even if exceptions occur, reducing boilerplate and preventing leaks
- It catches more exceptions
- It works with all Java classes
What is a race condition?
- Two threads running at the same speed
- Two threads competing to finish first
- Two threads accessing shared data without synchronization, causing unpredictable results
- A thread running too fast for the CPU
Why should assertions NOT be used for public method parameter validation?
- Assertions are too slow
- Assertions are disabled by default in production, so validation would be skipped
- Assertions can't check parameters
- Assertions cause memory leaks
Why does wait() release the lock while sleep() does not?
- wait() is more efficient
- wait() is designed for inter-thread communication — other threads need the lock to modify shared state and notify
- sleep() is a static method
- wait() can only be used inside main()
Apply / Implement (Q13–Q18)
What is the output of this code?
try {
System.out.print("A");
throw new RuntimeException();
} catch(RuntimeException e) {
System.out.print("B");
} finally {
System.out.print("C");
}
System.out.print("D");
- ABCD
- ABD
- ACD
- ABC
To create a custom checked exception, you extend:
- RuntimeException
- Error
- Exception
- Throwable
Which code correctly creates a thread using a lambda?
new Thread(() -> System.out.println("Hi")).start();new Runnable(() -> System.out.println("Hi")).start();Thread.run(() -> System.out.println("Hi"));new Thread({System.out.println("Hi")}).start();
What must a class implement to be used in try-with-resources?
- Serializable
- Closeable only
- AutoCloseable
- Runnable
How do you enable assertions when running a Java program?
java -assert MyProgramjava -ea MyProgramjava -enable-assertions MyProgram- Both B and C
-ea (short form) and -enableassertions (full form) enable assertions. By default, assertions are disabled in production.What does thread.join() do?
- Merges two threads into one
- Pauses the current thread until the specified thread completes
- Starts a new thread
- Kills the specified thread
Analyze / Compare (Q19–Q23)
In multi-catch catch(IOException | SQLException e), the variable e is:
- Of type Object
- Of type Exception
- Implicitly final — cannot be reassigned
- Mutable — can be reassigned
Which approach to thread creation allows extending another class?
- extends Thread
- implements Runnable
- Both
- Neither
What is the key difference between wait() and sleep()?
- wait() is faster
- sleep() can only be used in main()
- wait() releases the monitor lock; sleep() does not
- wait() is deprecated
Exception chaining (initCause) is useful because:
- It hides the original exception
- It wraps low-level exceptions in business-meaningful exceptions while preserving the root cause
- It automatically retries the operation
- It makes exceptions unchecked
Why is notifyAll() generally preferred over notify()?
- notifyAll() is faster
- notify() might wake up a thread that can't proceed, causing a missed signal
- notify() is deprecated
- notifyAll() uses less memory
Evaluate / Justify (Q24–Q27)
When should you use ExecutorService thread pool instead of manually creating threads?
- When you have exactly 2 threads
- When you have many short-lived tasks and want thread reuse, bounded resources, and graceful shutdown
- When you don't need concurrency
- When threads don't share data
For Zerodha's order validation, should InvalidStockSymbolException be checked or unchecked?
- Checked — because it's a recoverable business rule violation
- Unchecked — because it's a programming error (bad input should be validated earlier)
- Either works equally well
- It should be an Error
In a banking system, which deadlock prevention strategy is most practical?
- Never use locks
- Lock ordering — always acquire account locks in ascending account number order
- Use Thread.sleep() between locks
- Use only one thread
Why should you use while loop (not if) when checking conditions before wait()?
- while is faster than if
- Spurious wakeups can occur — the thread must re-check the condition after waking
- if doesn't compile with wait()
- while prevents deadlock
Create / Design (Q28–Q30)
You're designing a Paytm payment system. Which exception handling design is best?
- Catch all exceptions with
catch(Exception e)and log them - Create a hierarchy: PaymentException → InsufficientBalanceException, UPITimeoutException, InvalidUPIException — with exception chaining for root cause analysis
- Use only unchecked exceptions for everything
- Don't use exceptions — use return codes (0 = success, -1 = failure)
For a Zerodha-like system handling 10 lakh concurrent orders, which threading design is optimal?
- Create a new Thread for each order
- Use a single thread processing orders sequentially
- Use ExecutorService with a fixed thread pool sized to CPU cores, with a bounded blocking queue
- Use Thread.sleep() to slow down orders
A banking system needs to: validate inputs (assertions), handle business errors (custom exceptions), process transactions concurrently (threads), and prevent data corruption (synchronization). Which combination is correct?
- assert for input validation, catch(Exception e) for all errors, extends Thread for concurrency
- IllegalArgumentException for public API validation, assert for internal invariants, custom checked exceptions for business errors, implements Runnable with synchronized and thread pools
- Only use RuntimeException for everything
- Use single thread to avoid all concurrency issues
Short Answer Questions (8)
SA1. Differentiate between checked and unchecked exceptions with two examples each.
Answer: Checked exceptions are verified at compile time — the compiler forces you to handle them using try-catch or declare them with throws. They represent recoverable external conditions. Examples: IOException (file/network I/O failure), SQLException (database error). Unchecked exceptions occur at runtime due to programming bugs — the compiler does NOT force handling. They extend RuntimeException. Examples: NullPointerException (accessing null reference), ArrayIndexOutOfBoundsException (invalid array index). Rule of thumb: checked = external failure (plan for it), unchecked = your bug (fix it).
SA2. Explain the purpose of the finally block. Give a scenario where it is essential.
Answer: The finally block executes always — whether an exception occurs or not, whether it's caught or uncaught. Its purpose is resource cleanup: closing files, database connections, network sockets, or releasing locks. It's essential when reading a bank transaction file: if an exception occurs mid-read, the file handle must still be released. Without finally, the file remains locked, preventing other processes from accessing it. Only System.exit() prevents finally from executing.
SA3. What is try-with-resources? Which interface must a class implement to use it?
Answer: Try-with-resources (Java 7+) automatically closes resources declared in the try header when the block exits — even if an exception occurs. The class must implement AutoCloseable interface (which has a single close() method). Example: try (BufferedReader br = new BufferedReader(new FileReader("data.csv"))) { ... } — br.close() is called automatically. Closeable (which extends AutoCloseable) also works. This eliminates verbose finally cleanup blocks and prevents resource leaks.
SA4. Explain the difference between throw and throws with code examples.
Answer: throw is used inside a method body to actually throw an exception object: throw new InsufficientBalanceException("Low balance");. It can throw one exception at a time. throws is used in the method signature to declare that the method may propagate certain checked exceptions to the caller: void pay(double amt) throws InsufficientBalanceException, IOException. The caller must then handle these exceptions. throw = action (throwing), throws = declaration (warning).
SA5. Describe the five states in a Java thread's lifecycle.
Answer: (1) NEW: Thread object created via new Thread(), but start() not yet called. (2) RUNNABLE: After start() is called — thread is ready to run and waiting for CPU scheduling. (3) BLOCKED: Thread is waiting to acquire a monitor lock to enter a synchronized block. (4) WAITING/TIMED_WAITING: Thread called wait(), join(), or sleep() — it's paused. TIMED_WAITING has a timeout; WAITING waits indefinitely until notify(). (5) TERMINATED: Thread's run() method completed or an unhandled exception killed it. Cannot be restarted.
SA6. What is synchronization? Why is it needed in multithreading?
Answer: Synchronization is a mechanism that ensures only one thread can access a critical section (shared resource) at a time. It's needed to prevent race conditions — when two threads simultaneously read/modify shared data, causing corrupted or inconsistent results. In Java, the synchronized keyword locks a method or block on a monitor object. Example: Two threads withdrawing from the same bank account — without synchronization, both might check balance (₹10,000) simultaneously and both withdraw ₹8,000, leaving the account at -₹6,000 instead of correctly failing the second withdrawal.
SA7. Explain wait() and notify() with a real-world analogy.
Answer: wait() and notify() enable inter-thread communication. wait() pauses the current thread and releases the lock, allowing other threads to work. notify() wakes up one waiting thread; notifyAll() wakes all. Analogy: Think of a restaurant kitchen. The waiter (consumer) wait()s when no orders are ready. The chef (producer) prepares a dish and calls notify() — "Order ready!" The waiter wakes up, picks up the dish. If the kitchen is full (no counter space), the chef wait()s until the waiter picks up dishes and notify()s — "Counter cleared!"
SA8. What is a deadlock? How can it be prevented?
Answer: Deadlock occurs when two or more threads are permanently blocked, each holding a lock the other needs. Example: Thread A locks Account-1 and waits for Account-2; Thread B locks Account-2 and waits for Account-1 — neither can proceed. Prevention strategies: (1) Lock ordering — always acquire locks in a consistent order (e.g., ascending account number). (2) Lock timeout — use tryLock(timeout) instead of indefinite waiting. (3) Avoid nested locks — minimize holding multiple locks. (4) Use concurrent collections — ConcurrentHashMap, BlockingQueue handle synchronization internally.
Long Answer Questions (3)
LA1. Explain the complete exception hierarchy in Java. Differentiate between checked and unchecked exceptions with examples. Describe the execution flow of try-catch-finally with multiple catch blocks. (10 marks)
Answer:
Exception Hierarchy: All exceptions in Java descend from Throwable. It has two branches: Error (unrecoverable JVM-level problems like OutOfMemoryError, StackOverflowError — should NOT be caught) and Exception (recoverable application-level problems). Exception further splits into checked exceptions (like IOException, SQLException) and unchecked exceptions (RuntimeException and its subclasses like NullPointerException, ArithmeticException).
Checked vs Unchecked:
| Aspect | Checked | Unchecked |
|---|---|---|
| Verified at | Compile time | Runtime |
| Must handle? | Yes — try-catch or throws | No — optional |
| Extends | Exception (not RuntimeException) | RuntimeException |
| Represents | External failures (file, network, DB) | Programming bugs (null, bad index) |
| Examples | IOException, ClassNotFoundException | NullPointerException, ClassCastException |
try-catch-finally Execution Flow:
Case 1 — No exception: try block executes fully → catch blocks skipped → finally executes → code after try-catch-finally continues.
Case 2 — Exception caught: try block executes until exception → remaining try code SKIPPED → matching catch block executes → finally executes → code continues.
Case 3 — Exception NOT caught: try block executes until exception → no matching catch → finally STILL executes → exception propagates up the call stack.
Multiple catch blocks: Checked top-to-bottom. First matching catch handles the exception. Important: Specific exceptions MUST come before general ones — otherwise compilation error (unreachable code). Example: catch(FileNotFoundException e) must come before catch(IOException e) because FNFE is a subclass of IOE.
LA2. Describe multithreading in Java. Explain Thread class vs Runnable interface with code examples. Describe the complete thread lifecycle with a diagram. (10 marks)
Answer:
Multithreading is the ability to run multiple threads (lightweight processes) concurrently within a single program. Each thread has its own call stack but shares the process's heap memory. Java supports multithreading natively through the java.lang.Thread class and java.lang.Runnable interface.
Method 1 — Extending Thread:
Java class MyThread extends Thread { public void run() { System.out.println("Thread running: " + getName()); } } // Usage: new MyThread().start();
Limitation: Can't extend another class (single inheritance).
Method 2 — Implementing Runnable:
Java class MyTask implements Runnable { public void run() { System.out.println("Task running"); } } // Usage: new Thread(new MyTask()).start();
Advantage: Can extend another class + better separation of task from thread mechanism.
Method 3 — Lambda (Java 8+):
Java new Thread(() -> System.out.println("Lambda thread")).start();
Advantage: Most concise. Best for short, inline tasks.
Thread Lifecycle:
NEW (created, not started) → start() → RUNNABLE (ready for CPU) → scheduled → RUNNING (executing run()) → sleep()/wait()/blocked on lock → BLOCKED/WAITING/TIMED_WAITING → notify()/sleep over/lock acquired → back to RUNNABLE → run() completes → TERMINATED (dead, cannot restart).
Key methods: start() (creates new OS thread), run() (contains thread logic — never call directly), sleep(ms) (pauses), join() (wait for thread to die), interrupt() (signal thread to stop), yield() (hint to give up CPU).
LA3. What is synchronization in Java? Explain synchronized methods and synchronized blocks. Describe the producer-consumer problem with wait() and notify(). How does ExecutorService improve thread management? (10 marks)
Answer:
Synchronization prevents race conditions by ensuring mutual exclusion — only one thread can execute a synchronized section at a time. Java provides two mechanisms:
Synchronized Method: public synchronized void withdraw(double amt) — locks the entire method on the object's intrinsic monitor. When Thread A enters, Thread B must wait at the method entrance.
Synchronized Block: synchronized(lockObject) { ... } — locks only the critical section, not the entire method. More granular, better performance for methods with both synchronized and unsynchronized code.
Producer-Consumer Problem: A classic coordination problem. Producer creates data, Consumer processes it. They share a bounded buffer (queue). Producer must wait() when buffer is full; Consumer must wait() when buffer is empty. When Producer adds data, it calls notifyAll() to wake waiting consumers. When Consumer removes data, it calls notifyAll() to wake waiting producers. Important: always check conditions in a while loop (not if) due to spurious wakeups.
ExecutorService Thread Pools: Creating a new thread per task is expensive (OS overhead). ExecutorService manages a pool of reusable threads. Executors.newFixedThreadPool(n) creates n threads that process submitted tasks from a queue. Benefits: (1) Thread reuse — no creation overhead. (2) Bounded concurrency — won't exhaust system resources. (3) Task queuing — tasks wait when all threads are busy. (4) Graceful shutdown — shutdown() stops accepting new tasks; awaitTermination() waits for completion. In production systems like Zerodha, thread pools handle millions of requests with a small, fixed number of threads.
Lab Programs — Lab 1 & Lab 2
🔬 Lab 1: Exception Handling — All Keywords (try, catch, finally, throw, throws, assert)
Aim
To demonstrate all Java exception handling keywords (try, catch, finally, throw, throws) and understand the need and usage of assertions (assert with -ea).
Theory
Exception handling is Java's mechanism for dealing with runtime errors gracefully. The five keywords form a complete system: try wraps risky code, catch handles specific exceptions, finally ensures cleanup, throw creates exceptions manually, and throws delegates handling to callers. Assertions (assert) are compile-time debugging aids that verify assumptions — they are disabled by default and enabled with java -ea.
Need of Assertion: Assertions help catch bugs during development by checking conditions that should logically always be true at a certain point in the program. They are NOT for user input validation (use exceptions for that). They are for internal consistency checks — e.g., after sorting, assert the array is sorted. If an assertion fails, it reveals a logic error in the code itself. Assertions are disabled in production for performance, making them zero-overhead in deployed systems.
Program Code
Java — Lab1_ExceptionHandling.java import java.io.*; // Custom Checked Exception class InsufficientBalanceException extends Exception { private double balance, amount; public InsufficientBalanceException(String msg, double bal, double amt) { super(msg); this.balance = bal; this.amount = amt; } public double getDeficit() { return amount - balance; } } public class Lab1_ExceptionHandling { // 'throws' — declares this method may throw a checked exception public static void withdraw(double balance, double amount) throws InsufficientBalanceException { // 'assert' — internal sanity check (enable with java -ea) assert balance >= 0 : "Balance cannot be negative: " + balance; assert amount > 0 : "Withdrawal amount must be positive: " + amount; if (amount > balance) { // 'throw' — actually throws the exception throw new InsufficientBalanceException( "Cannot withdraw ₹" + amount + " from balance ₹" + balance, balance, amount ); } System.out.println("✅ Withdrawn ₹" + amount + ". Remaining: ₹" + (balance - amount)); } public static void readFile(String filename) { // try-with-resources — auto-closes the reader try (BufferedReader br = new BufferedReader(new FileReader(filename))) { String line; while ((line = br.readLine()) != null) { System.out.println(" 📄 " + line); } } catch (FileNotFoundException e) { System.out.println(" ❌ File not found: " + filename); } catch (IOException e) { System.out.println(" ❌ IO Error: " + e.getMessage()); } } public static void main(String[] args) { System.out.println("=== PART 1: try-catch-finally ==="); // 'try' — wraps risky code try { int result = 100 / 0; System.out.println("Result: " + result); // Never reached } // 'catch' — handles specific exception catch (ArithmeticException e) { System.out.println("❌ Caught: " + e.getMessage()); } // 'finally' — ALWAYS executes finally { System.out.println("📋 Finally block executed (cleanup)."); } System.out.println("\n=== PART 2: throw & throws (Custom Exception) ==="); try { withdraw(5000, 3000); // ✅ Success withdraw(2000, 5000); // ❌ Throws exception } catch (InsufficientBalanceException e) { System.out.println("❌ " + e.getMessage()); System.out.println("💡 Deficit: ₹" + e.getDeficit()); } System.out.println("\n=== PART 3: Multi-catch ==="); try { String str = null; System.out.println(str.length()); } catch (NullPointerException | ArithmeticException e) { System.out.println("❌ Multi-catch: " + e.getClass().getSimpleName()); } System.out.println("\n=== PART 4: try-with-resources ==="); readFile("nonexistent.txt"); System.out.println("\n=== PART 5: Assertions (run with java -ea) ==="); int age = -5; assert age >= 0 : "Age cannot be negative: " + age; System.out.println("Age: " + age); // Only reached if assertion passes } }
Viva Questions — Lab 1
Error and Exception?A: Error represents serious JVM-level problems (OutOfMemoryError) that cannot be recovered. Exception represents application-level problems that can be handled. Never catch Error in production code.
try without catch?A: Yes — try can be paired with just finally (no catch). Example:
try { ... } finally { cleanup(); }. The exception propagates up, but finally still executes.finally has a return statement?A: The finally's return overrides any return in try or catch. This is a bad practice and should be avoided — it can hide exceptions and confuse debugging.
A: Assertions are for development-time debugging, not production error handling. Disabling them in production eliminates performance overhead. If assertions controlled business logic, disabling them would change program behavior — that's why they're only for sanity checks.
A: Exception chaining wraps a low-level exception (e.g., SocketTimeoutException) inside a business-level exception (e.g., PaymentFailedException) using initCause() or constructor parameter. It preserves the root cause for debugging while providing meaningful context at the application level.
🔬 Lab 2: Multithreading Using Lambda — Runnable via Lambda & Thread Lifecycle
Aim
To demonstrate multithreading in Java using lambda expressions for Runnable, observe thread lifecycle states, and understand synchronization with practical examples.
Theory
Since Runnable is a @FunctionalInterface (single abstract method run()), it can be replaced by a lambda expression in Java 8+. This makes thread creation more concise. The thread lifecycle progresses through states: NEW → RUNNABLE → (BLOCKED/WAITING/TIMED_WAITING) → TERMINATED. Synchronization prevents race conditions when multiple threads access shared resources.
Program Code
Java — Lab2_MultithreadingLambda.java public class Lab2_MultithreadingLambda { // Shared counter to demonstrate race condition + fix private static int counter = 0; private static final Object lock = new Object(); public static void main(String[] args) throws InterruptedException { // ===== PART 1: Thread creation via Lambda ===== System.out.println("=== PART 1: Lambda Thread Creation ==="); // Method 1: Lambda with Runnable Thread buyThread = new Thread(() -> { for (int i = 1; i <= 5; i++) { System.out.println("🟢 " + Thread.currentThread().getName() + " → BUY Order #" + i); try { Thread.sleep(200); } catch (InterruptedException e) { } } }, "BuyThread"); Thread sellThread = new Thread(() -> { for (int i = 1; i <= 5; i++) { System.out.println("🔴 " + Thread.currentThread().getName() + " → SELL Order #" + i); try { Thread.sleep(300); } catch (InterruptedException e) { } } }, "SellThread"); // ===== PART 2: Thread Lifecycle Observation ===== System.out.println("\n=== PART 2: Thread Lifecycle States ==="); System.out.println("buyThread state after new: " + buyThread.getState()); // NEW buyThread.start(); sellThread.start(); System.out.println("buyThread state after start: " + buyThread.getState()); // RUNNABLE Thread.sleep(100); System.out.println("buyThread state during run: " + buyThread.getState()); // TIMED_WAITING (sleeping) buyThread.join(); // Main thread waits for buyThread to finish sellThread.join(); // Main thread waits for sellThread to finish System.out.println("buyThread state after join: " + buyThread.getState()); // TERMINATED // ===== PART 3: Race Condition Demo ===== System.out.println("\n=== PART 3: Race Condition (WITHOUT sync) ==="); counter = 0; Thread inc1 = new Thread(() -> { for (int i = 0; i < 10000; i++) counter++; // NOT synchronized! }, "Inc-1"); Thread inc2 = new Thread(() -> { for (int i = 0; i < 10000; i++) counter++; }, "Inc-2"); inc1.start(); inc2.start(); inc1.join(); inc2.join(); System.out.println("Expected: 20000, Actual: " + counter + " (WRONG — race condition!)"); // ===== PART 4: Fixed with Synchronized ===== System.out.println("\n=== PART 4: Fixed (WITH synchronized) ==="); counter = 0; Thread safe1 = new Thread(() -> { for (int i = 0; i < 10000; i++) { synchronized (lock) { counter++; } // ✅ Synchronized! } }, "Safe-1"); Thread safe2 = new Thread(() -> { for (int i = 0; i < 10000; i++) { synchronized (lock) { counter++; } } }, "Safe-2"); safe1.start(); safe2.start(); safe1.join(); safe2.join(); System.out.println("Expected: 20000, Actual: " + counter + " (CORRECT ✅)"); // ===== PART 5: Thread with interrupt ===== System.out.println("\n=== PART 5: Thread Interrupt ==="); Thread longTask = new Thread(() -> { try { System.out.println("⏳ Long task started (sleeping 10 sec)..."); Thread.sleep(10000); System.out.println("Long task completed."); } catch (InterruptedException e) { System.out.println("⚡ Long task INTERRUPTED! Cleaning up..."); } }, "LongTask"); longTask.start(); Thread.sleep(1000); longTask.interrupt(); // Cancel the long task after 1 sec longTask.join(); System.out.println("\n✅ All multithreading demos complete!"); } }
Viva Questions — Lab 2
A: Java supports single inheritance. If you extend Thread, you cannot extend any other class. Implementing Runnable (interface) allows extending another class. It also separates the task (what to do) from the mechanism (thread management), following the Single Responsibility Principle.
sleep() and wait()?A:
sleep() pauses the thread for a specified time and does NOT release the lock. wait() pauses the thread indefinitely (until notify()) and DOES release the lock. sleep() is for timed delays; wait() is for inter-thread communication. wait() must be called from synchronized context.A: No. Once a thread reaches TERMINATED state (run() completed or exception thrown), it cannot be restarted. Calling start() again throws IllegalThreadStateException. You must create a new Thread object.
synchronized block instead of synchronized method?A: Synchronized block provides finer granularity — you lock only the critical section, not the entire method. This allows non-synchronized parts of the method to run concurrently, improving performance. Also, synchronized blocks can lock on any object, while synchronized methods always lock on 'this'.
A: A daemon thread is a background service thread (e.g., garbage collector). It automatically terminates when all non-daemon (user) threads have finished. Set with
thread.setDaemon(true) BEFORE calling start(). Used for logging, monitoring, cleanup tasks that shouldn't prevent JVM shutdown.Industry Spotlight — A Day in the Life
👩💻 Meera Krishnamurthy, 30 — Senior Java Developer at Zerodha, Bangalore
Background: B.Tech (CSE) from RVCE, Bangalore. Started as a junior Java developer at a service company. Self-learned multithreading and concurrent programming through Zerodha's open-source projects on GitHub. Joined Zerodha's Kite trading platform team 4 years ago.
A Typical Day:
8:30 AM — Pre-market check. Review overnight exception logs from the order management system. Check if any InsufficientMarginException or OrderRejectedException patterns need attention.
9:00 AM — Market opening preparation. Ensure thread pools are scaled up for the 9:15 AM rush. Monitor JVM metrics — thread count, heap usage, GC pauses.
9:15 AM — Market opens. Watch the real-time dashboard as 10+ lakh concurrent order threads fire up. The order matching engine uses ExecutorService with optimized thread pools. Synchronized queues handle order book state.
10:30 AM — Code review for a junior developer's PR. Fix a potential deadlock in the portfolio update service — they forgot lock ordering. Add custom exception hierarchy for the new margin trading feature.
1:00 PM — Lunch break. Read about Project Loom (virtual threads) — Zerodha is evaluating it for the next platform upgrade.
2:00 PM — Work on the new UPI payment integration. Implement exception chaining: UPIGatewayException wrapping HttpTimeoutException. Write try-with-resources for connection pooling.
4:00 PM — Write unit tests for the multithreaded order processor. Use CountDownLatch and CyclicBarrier for test synchronization.
5:30 PM — Post-market analysis. Review today's exception statistics. Zero unhandled exceptions — clean day! Update Grafana dashboard for the ops team.
| Detail | Info |
|---|---|
| Tools Used Daily | Java 17, Spring Boot, Netty, ExecutorService, Kafka, PostgreSQL, Grafana, IntelliJ IDEA |
| Key Skills | Multithreading, Exception Design, Concurrent Collections, Thread Pools, Performance Tuning |
| Entry Salary | ₹6–10 LPA |
| Mid-Level (3–5 yrs) | ₹12–20 LPA |
| Senior (7+ yrs) | ₹25–45 LPA |
| Companies Hiring | Zerodha, Razorpay, PhonePe, Paytm, HDFC Securities, Upstox, NSE (IT), Goldman Sachs India, Morgan Stanley India |
Earn With It — Multithreaded Microservices ₹15K–₹50K/month
💰 Your Earning Path After This Unit
Portfolio Piece: "Concurrent Order Processing System" — a multithreaded Java application with custom exception hierarchy, thread pools, and synchronized state management. Push to GitHub with clean README.
Freelance Opportunities:
• Backend microservice development (Spring Boot + multithreading) — ₹15,000–₹30,000/project
• Exception handling audit for existing Java apps — ₹5,000–₹15,000
• Concurrent data processing scripts (CSV/JSON parsers) — ₹8,000–₹20,000
• API integration with proper error handling — ₹10,000–₹25,000
• Performance optimization (threading + connection pooling) — ₹20,000–₹50,000
| Platform | Best For | Typical Rate |
|---|---|---|
| Internshala | Java backend internships + freelance | ₹8,000–₹20,000/month |
| Upwork | Java microservice projects (global) | $20–$60/hour |
| Toptal | Premium Java developer network | $40–$100/hour |
| Full-time Java developer positions | ₹6–15 LPA (entry) | |
| GitHub | Open-source contributions → job offers | Portfolio → interviews |
Chapter Summary — Quick Revision
🧠 Exception Handling — Key Takeaways
✅ Hierarchy: Throwable → Error (don't catch) + Exception → Checked (compile-time, must handle) + RuntimeException (unchecked, runtime bugs)
✅ try-catch-finally: try wraps risky code, catch handles exceptions, finally ALWAYS runs (cleanup)
✅ Multi-catch: catch(A | B e) — handles multiple exceptions in one block, variable is implicitly final
✅ try-with-resources: Auto-closes AutoCloseable resources — no more verbose finally blocks
✅ throw vs throws: throw = create & throw exception (method body), throws = declare exceptions (method signature)
✅ Custom exceptions: Extend Exception (checked) or RuntimeException (unchecked) for business-specific errors
✅ Exception chaining: initCause() wraps low-level exceptions in business exceptions, preserving root cause
✅ Assertions: assert condition : message — development sanity checks, enable with -ea, NOT for production validation
⚡ Multithreading — Key Takeaways
✅ Thread creation: extends Thread | implements Runnable | lambda (preferred: Runnable/lambda)
✅ Lifecycle: NEW → RUNNABLE → RUNNING → BLOCKED/WAITING → TERMINATED
✅ Key methods: start() (new thread), sleep() (pause), join() (wait for completion), interrupt() (cancel), yield() (hint)
✅ synchronized: Prevents race conditions — method-level or block-level locking on monitor object
✅ wait/notify: Inter-thread communication — wait() releases lock, notify/notifyAll() wakes waiting threads
✅ Deadlock: Circular wait — prevent with lock ordering, timeout locks, avoid nested locks
✅ Thread pools: ExecutorService.newFixedThreadPool(n) — reuses threads, bounds concurrency, graceful shutdown
📱 Code Tweet — Unit 11 in 280 Characters
@JavaDev_India
Exception = UPI failure caught gracefully 🛡️
Thread = Zerodha orders running concurrently ⚡
synchronized = one thread at a time on shared balance 🔒
try-with-resources = auto-close connections ♻️
ExecutorService = 10L orders, 4 threads 🏊♂️
#Java #Fintech #Unit11
Earning Checkpoint — Are You Job-Ready?
| Skill Learned | Tool/Concept | Portfolio Deliverable | Earning Ready? |
|---|---|---|---|
| Exception Hierarchy | Throwable → Error/Exception | — | ✅ Yes — interview essential |
| try-catch-finally | All 5 keywords | Lab 1 code | ✅ Yes — every Java project uses it |
| Custom Exceptions | extends Exception/RuntimeException | InsufficientBalanceException | ✅ Yes — fintech requirement |
| try-with-resources | AutoCloseable | FileReaderWithResources | ✅ Yes — resource management |
| Assertions | assert + java -ea | Lab 1 Part 5 | ✅ Yes — debugging skill |
| Thread Creation | Thread, Runnable, Lambda | Lab 2 code | ✅ Yes — backend development |
| Thread Lifecycle | 5 states | Lab 2 Part 2 | ✅ Yes — interview essential |
| Synchronization | synchronized method/block | BankTransferThreads | ✅ Yes — concurrent apps |
| wait/notify | Producer-Consumer pattern | ZerodhaOrderBook | ✅ Yes — fintech/messaging |
| Deadlock Prevention | Lock ordering | Safe transfer code | ✅ Yes — senior interviews |
| Thread Pools | ExecutorService | ZerodhaThreadPool | ✅ Yes — production systems |
✅ Unit 11 complete. MCQs: 30. Labs 1, 2 covered. Ready for Unit 12!
[QR: Link to EduArtha video tutorial — Java Exceptions, Assertions & Multithreading]