Spex3
    

Java Multithreading & Concurrency


    

    

Java Multithreading & Concurrency
Multithreading in Java allows multiple threads to run concurrently, improving performance by utilizing CPU cores efficiently. It is widely used in applications requiring parallel execution, such as gaming, web servers, and real-time systems.

1. What is a Thread?
A thread is the smallest unit of execution in a program. Java supports multithreading via the Thread class and Runnable interface.

Single-threaded vs Multi-threaded Execution
Single-threaded Multi-threaded
Executes one task at a time Executes multiple tasks in parallel
Blocks execution for long-running tasks Runs tasks simultaneously, improving responsiveness
Suitable for simple applications Suitable for real-time systems, GUI applications, etc.
2. Creating Threads in Java
2.1 Using the Thread Class
✅ Example: Extending the Thread Class


class MyThread extends Thread {
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + " - Count: " + i);
try {
Thread.sleep(500); // Pause for 500ms
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

public class ThreadExample {
public static void main(String[] args) {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();

thread1.start();
thread2.start();
}
}


💡 Output (Interleaved Execution):


Thread-0 - Count: 1
Thread-1 - Count: 1
Thread-0 - Count: 2
Thread-1 - Count: 2
...

2.2 Using the Runnable Interface
✅ Example: Implementing Runnable Interface


class MyRunnable implements Runnable {
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + " - Count: " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

public class RunnableExample {
public static void main(String[] args) {
Thread thread1 = new Thread(new MyRunnable());
Thread thread2 = new Thread(new MyRunnable());

thread1.start();
thread2.start();
}
}


💡 Why use Runnable?

More flexible than extending Thread (since Java supports only single inheritance).

Allows better separation of logic from threading.

3. Thread Lifecycle in Java

State Description

New Thread is created but not started (new Thread())

Runnable Ready to run (start()) but waiting for CPU

Running Actively executing run() method

Blocked Waiting for a lock to be released

Waiting Waiting indefinitely for another thread's signal

Timed Waiting Paused for a fixed time (sleep(), join())

Terminated Execution completed

✅ Example: Demonstrating join() Method


class MyTask extends Thread {
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println(Thread.currentThread().getName() + " - Task " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

public class ThreadJoinExample {
public static void main(String[] args) {
MyTask thread1 = new MyTask();
MyTask thread2 = new MyTask();

thread1.start();
try {
thread1.join(); // Wait for thread1 to finish before starting thread2
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.start();
}
}


💡 Output:


Thread-0 - Task 1
Thread-0 - Task 2
Thread-0 - Task 3
Thread-1 - Task 1
Thread-1 - Task 2
Thread-1 - Task 3

4. Synchronization in Java
Without synchronization, multiple threads can interfere with shared resources, leading to race conditions.

4.1 Synchronized Methods
Ensures that only one thread can access a method at a time.

✅ Example: Synchronizing a Counter


class Counter {
private int count = 0;

public synchronized void increment() { // Synchronized method
count++;
}

public int getCount() {
return count;
}
}

public class SyncExample {
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());
}
}


💡 Output (Corrected with Synchronization):


Final count: 2000

4.2 Synchronized Blocks
Instead of synchronizing an entire method, we can synchronize only critical sections.

✅ Example: Synchronizing a Block


class SharedResource {
public void printNumbers() {
synchronized (this) { // Synchronized block
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + " - " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

public class SyncBlockExample {
public static void main(String[] args) {
SharedResource resource = new SharedResource();

Thread t1 = new Thread(() -> resource.printNumbers());
Thread t2 = new Thread(() -> resource.printNumbers());

t1.start();
t2.start();
}
}


5. Deadlocks & Avoiding Them
A deadlock occurs when two threads wait for each other indefinitely.

✅ Example: Deadlock Scenario


class Resource {
void methodA(Resource r) {
synchronized (this) {
System.out.println(Thread.currentThread().getName() + " locked methodA");
synchronized (r) {
System.out.println(Thread.currentThread().getName() + " locked methodB");
}
}
}
}

public class DeadlockExample {
public static void main(String[] args) {
Resource r1 = new Resource();
Resource r2 = new Resource();

Thread t1 = new Thread(() -> r1.methodA(r2));
Thread t2 = new Thread(() -> r2.methodA(r1));

t1.start();
t2.start();
}
}


💡 How to Avoid Deadlocks?

Always acquire locks in the same order.

Use timeout-based locking with tryLock().

Summary

Concept Description

Creating Threads Thread class, Runnable interface

Thread States New, Runnable, Running, Blocked, Waiting, Terminated

Thread Methods start(), sleep(), join()

Synchronization Prevents race conditions using synchronized methods/blocks

Deadlocks Occurs when two threads wait on each other