0% found this document useful (0 votes)
35 views55 pages

UNIT III Multithreaded

The document covers multithreaded programming in Java, explaining concepts such as thread creation, synchronization, inter-thread communication, and deadlocks. It details the lifecycle of threads, methods for creating threads using the Thread class and Runnable interface, and the importance of synchronization to prevent race conditions. Additionally, it discusses the differences between single-threaded and multi-threaded execution, emphasizing Java's preemptive multitasking model.

Uploaded by

PRIYA
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
35 views55 pages

UNIT III Multithreaded

The document covers multithreaded programming in Java, explaining concepts such as thread creation, synchronization, inter-thread communication, and deadlocks. It details the lifecycle of threads, methods for creating threads using the Thread class and Runnable interface, and the importance of synchronization to prevent race conditions. Additionally, it discusses the differences between single-threaded and multi-threaded execution, emphasizing Java's preemptive multitasking model.

Uploaded by

PRIYA
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 55

UNIT III Multithreaded Programming: Thread Class-Runnable interface–Synchronization–

Using synchronized methods– Using synchronized statement – Inter thread


Communication–Deadlock.
I/O Streams: Concepts of streams-Stream classes-Byte and Character stream -
Reading console Input and Writing Console output – File Handling.

1
2
Multithreading in Java is a process of executing multiple threads simultaneously.
A thread is a lightweight sub-process, the smallest unit of processing. Multiprocessing and multithreading,
both are used to achieve multitasking
Multitasking
Multitasking is a process of executing multiple tasks simultaneously. We use multitasking to utilize the
CPU. Multitasking can be achieved in two ways:
o Process-based Multitasking (Multiprocessing)
o Thread-based Multitasking (Multithreading)
1) Process-based Multitasking (Multiprocessing)
o Each process has an address in memory. In other words, each process allocates a separate memory
area.
o A process is heavyweight.
o Cost of communication between the process is high.
o Switching from one process to another requires some time for saving and loading registers, memory
maps, updating lists, etc.
2) Thread-based Multitasking (Multithreading)
o Threads share the same address space.
o A thread is lightweight.
o Cost of communication between the thread is low.
Note: At least one process is required for each thread.
What is Thread in Java?
A thread is a lightweight subprocess, the smallest unit of processing. It is a separate path of execution.
Threads are independent. If there occurs exception in one thread, it doesn't affect other threads. It uses a
shared memory area.
Java Thread class
Java provides Thread class to achieve thread programming. Thread class provides constructors and
methods to create and perform operations on a thread. Thread class extends Object class and implements
Runnable interface.
Thread States:
o New: A thread that has been created but not yet started.
o Runnable: A thread that is ready to run and waiting for CPU time.
o Blocked: A thread that is blocked waiting for a monitor lock.
o Waiting: A thread that is waiting indefinitely for another thread to perform a particular action.
o Timed Waiting: A thread that is waiting for another thread to perform an action for up to a
specified waiting time.
o Terminated: A thread that has completed its task.
Life cycle of a Thread (Thread States)
In Java, a thread always exists in any one of the following states. These states are:
1. New
2. Active
o Runnable
o Running
3. Blocked / Waiting(Non-runnable state)
4. Timed Waiting
5. Terminated (Dead)

3
Explanation of Different Thread States
New: Whenever a new thread is created, it is always in the new state. For a thread in the new state, the code
has not been run yet and thus has not begun its execution.
Active: When a thread invokes the start() method, it moves from the new state to the active state. The
active state contains two states within it: one is runnable, and the other is running.
o Runnable: A thread, that is ready to run is then moved to the runnable state. In the runnable state,
the thread may be running or may be ready to run at any given instant of time.
o Running: When the thread gets the CPU, it moves from the runnable to the running state.
Generally, the most common change in the state of a thread is from runnable to running and again
back to runnable.
Blocked or Waiting: Whenever a thread is inactive for a span of time then, either the thread is in the
blocked state or is in the waiting state
Timed Waiting: Sometimes, waiting for leads to starvation. For example, a thread (its name is A) has
entered the critical section of a code and is not willing to leave that critical section. A real example of timed
waiting is when we invoke the sleep() method on a specific thread. The sleep() method puts the thread in
the timed wait state.
Terminated: A thread reaches the termination state because of the following reasons:
o When a thread has finished its job, then it exists or terminates normally.
o Abnormal termination: It occurs when some unusual events such as an unhandled exception or
segmentation fault.
A terminated thread means the thread is no more in the system.
The following diagram shows the different states involved in the life cycle of a thread.

Thread States in Java

A thread is a path of execution in a program that enters any one of the following five states during its life
cycle. The five states are as follows:
1. New State:
When we create a thread object using the Thread class, the thread is born and enters the New state. This
means the thread is created, but the start() method has not yet been called on the instance.
2. Runnable State:
Runnable state means a thread is ready for execution of any statement. When the start() method is called on
a new thread, thread enters into from New to a Runnable state.
3. Running State:

4
Running means Processor (CPU) has allocated time slot to thread for its execution. When thread scheduler
selects a thread from the runnable state for execution, it goes into running state. Look at the above figure.
his code is an example of thread’s state transitions. When start() method is called, the thread enters the
Runnable state. Once the CPU schedules it, the thread transitions to the Running state, executing the run()
method.
A running thread may give up its control in any one of the following situations and can enter into the
blocked state.
a) When sleep() method is invoked on a thread to sleep for specified time period, the thread is out of queue
during this time period. The thread again reenters into the runnable state as soon as this time period is
elapsed.
b) When a thread is suspended using suspend() method for some time in order to satisfy some conditions. A
suspended thread can be revived by using resume() method.
c) When wait() method is called on a thread to wait for some time. The thread in wait state can be run again
using notify() or notifyAll() method.

4. Blocked State:
A thread is considered to be in the blocked state when it is suspended, sleeping, or waiting for some time in
order to satisfy some condition.
For example, a thread enters the Blocked state when it is waiting to acquire a lock or a monitor that is held
by another thread. This generally happens when multiple threads are trying to access a synchronized block
or method.

Java Multithreading - The Thread Model


1. Introduction to Multithreading
 Multithreading allows multiple tasks to execute simultaneously within the same program.
 Java’s runtime system depends on threads for many operations, making Java a multithreaded
language.
 It eliminates inefficiencies like polling and blocking by using an asynchronous execution model.

2. Single-Threaded vs. Multi-Threaded Execution


🔹 Single-threaded Execution (Event Loop with Polling)
 Traditional single-threaded systems rely on event loops to check for pending tasks.
 If a task is waiting (blocked) (e.g., for user input), the entire program stops.
 Wastes CPU time and can cause delays in execution.
🔹 Multi-threaded Execution (Java Approach)
 Java allows multiple threads to execute concurrently.
 A thread can pause (block) without stopping other parts of the program.
 For example, while one thread waits for user input, another thread can continue running.

3. The Java Thread Model


🔹 Key Features of Java’s Multithreading Model
 Preemptive multitasking: A higher-priority thread can preempt a lower-priority thread.
 Time-slicing (Round-robin scheduling): Threads of equal priority are given CPU time in cycles.
 Context switching: When a thread yields or blocks, another thread gets CPU time.
🔹 Thread Lifecycle
A thread in Java exists in different states:

5
State Description
New Thread is created but not started (Thread t = new Thread();)
Runnable Thread is ready to run but waiting for CPU time (start())
Running Thread is currently executing
Blocked Thread is waiting for a resource (e.g., file I/O, synchronized block)
Waiting Thread is indefinitely waiting (wait())
Timed Waiting Thread waits for a specific time (sleep(1000))
Terminated Thread has completed execution

Method Description
start() Starts a new thread and calls run()
run() Defines the code executed by the thread
sleep(ms) Makes the thread sleep for ms milliseconds
join() Waits for the thread to complete
interrupt() Interrupts the thread execution
setPriority(int) Sets thread priority (1 to 10)
getName() Returns the thread name
setName(String) Sets a custom name for the thread
isAlive() Checks if the thread is still running

4. Creating a Thread in Java


🔹 Using the Thread Class
class MyThread extends Thread {
public void run() {
System.out.println("Thread is running...");
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start(); // Start the thread
} }
📌 Key Methods of Thread Class

🔹 Using the Runnable Interface


class MyRunnable implements Runnable {
public void run() {
System.out.println("Thread is running...");
}
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable());
t1.start();
}

6
}
5. Synchronization in Java
🔹 Why Synchronization?
 Prevents race conditions when multiple threads access shared resources.
 Ensures only one thread modifies a critical section at a time.
🔹 Using Synchronized Methods
class SharedResource {
synchronized void printNumbers(int n) {
for (int i = 1; i <= 5; i++) {
System.out.println(n * i);
try {
Thread.sleep(500);
} catch (Exception e) {
System.out.println(e);
}
}
}
}

class MyThread extends Thread {


SharedResource obj;
MyThread(SharedResource obj) {
this.obj = obj;
}

public void run() {


obj.printNumbers(5);
}
}

public class SyncExample {


public static void main(String[] args) {
SharedResource obj = new SharedResource();
MyThread t1 = new MyThread(obj);
MyThread t2 = new MyThread(obj);
t1.start();
t2.start();
}
}
📌 Key Point: synchronized ensures only one thread accesses printNumbers() at a time.

🔹 Using Synchronized Blocks


 Instead of locking an entire method, lock only a specific block of code.
class Shared {
void printNumbers(int n) {
synchronized (this) { // Locking only this block

7
for (int i = 1; i <= 5; i++) {
System.out.println(n * i);
try { Thread.sleep(500); } catch (Exception e) {}
}
}
}
}
📌 Synchronized blocks improve efficiency by reducing the locked code region.

6. Inter-Thread Communication
🔹 Using wait(), notify(), and notifyAll()
 wait(): Makes a thread pause execution until another thread notifies it.
 notify(): Wakes up one waiting thread.
 notifyAll(): Wakes up all waiting threads.
🔹 Example
class SharedResource {
synchronized void produce() {
System.out.println("Producing...");
try {
wait(); // Thread waits
} catch (Exception e) {}
System.out.println("Resumed...");
}

synchronized void consume() {


System.out.println("Consuming...");
notify(); // Notify waiting thread
}
}

public class ThreadCommExample {


public static void main(String[] args) {
SharedResource obj = new SharedResource();

new Thread(() -> obj.produce()).start();


try { Thread.sleep(1000); } catch (Exception e) {}
new Thread(() -> obj.consume()).start();
}
}
📌 Key Point: wait() and notify() help coordinate communication between threads.

7. Deadlock in Java
🔹 What is a Deadlock?
 Occurs when two or more threads wait indefinitely for each other to release a lock.
🔹 Example of Deadlock
class DeadlockExample {

8
static final Object lock1 = new Object();
static final Object lock2 = new Object();

public static void main(String[] args) {


Thread t1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1: Holding lock 1...");
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (lock2) {
System.out.println("Thread 1: Holding lock 2...");
}
}
});

Thread t2 = new Thread(() -> {


synchronized (lock2) {
System.out.println("Thread 2: Holding lock 2...");
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (lock1) {
System.out.println("Thread 2: Holding lock 1...");
}
}
});

t1.start();
t2.start();
}
}
Creating Threads in Java (Simplified Explanation)
Java provides two ways to create a thread:
1️⃣ By implementing the Runnable interface
2️⃣ By extending the Thread class

1. Implementing Runnable Interface (Recommended Way)


In this approach:
✔️We create a class that implements the Runnable interface.
✔️We define the run() method, which contains the code that will execute in a separate thread.
✔️We create a Thread object and pass our Runnable object to it.
✔️We call the start() method to begin execution.
Example:
class MyRunnable implements Runnable {
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("Thread is running: " + i);
try {
Thread.sleep(500); // Sleep for 500 milliseconds

9
} catch (InterruptedException e) {
System.out.println(e);
}
}
}
}

public class RunnableExample {


public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable(); // Create Runnable object
Thread thread = new Thread(myRunnable); // Create Thread object
thread.start(); // Start the thread
}
}
✔️Output: This will print numbers 1 to 5 in a separate thread, pausing for 500ms between each.

2. Extending the Thread Class


In this approach:
✔️We create a class that extends the Thread class.
✔️We override the run() method.
✔️We create an instance of our class and call start() to begin execution.
Example:
class MyThread extends Thread {
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("Thread is running: " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println(e);
}
}
}
}

public class ThreadExample {


public static void main(String[] args) {
MyThread myThread = new MyThread(); // Create Thread object
myThread.start(); // Start the thread
}
}
✔️Output: Similar to the Runnable example.

Which Method is Better?


✅ Implementing Runnable is preferred because:
 Java allows only one class to extend another (so extending Thread limits flexibility).

10
 It keeps your design cleaner by separating the "task" (Runnable) from the "execution" (Thread).

Key Thread Methods


 start() → Starts the thread, calling the run() method.
 run() → Contains the code that executes in the thread.
 sleep(ms) → Makes the thread pause for a given time (in milliseconds).
 join() → Makes the current thread wait until another thread completes.
 isAlive() → Checks if the thread is still running.
1️⃣ Thread Priorities
Threads in Java have priorities that help the thread scheduler decide which thread to run first.
📌 Priority Levels:
 Thread.MIN_PRIORITY = 1
 Thread.NORM_PRIORITY = 5 (Default)
 Thread.MAX_PRIORITY = 10
✅ Setting Thread Priority:
Thread t1 = new Thread();
t1.setPriority(Thread.MAX_PRIORITY);
🔍 Note:
 Higher priority doesn't guarantee more CPU time.
 Different operating systems handle priorities differently.

2️⃣ Thread Synchronization


When multiple threads share a resource, synchronization ensures only one thread accesses it at a time to
avoid errors.
📌 Using synchronized Method:
class SharedResource {
synchronized void printMessage(String msg) {
System.out.print("[" + msg);
try { Thread.sleep(1000); } catch (InterruptedException e) {}
System.out.println("]");
}
}
🔍 Key Points:
 synchronized ensures only one thread can execute the method at a time.
 Other threads must wait until the current thread completes execution.

3️⃣ Inter-thread Communication (wait(), notify())


Instead of polling, Java provides wait(), notify(), and notifyAll() to let threads communicate.
📌 Example:
class SharedData {
int value;
boolean available = false;

synchronized void produce(int num) {


while (available) try { wait(); } catch (InterruptedException e) {}
value = num;

11
available = true;
notify();
}

synchronized int consume() {


while (!available) try { wait(); } catch (InterruptedException e) {}
available = false;
notify();
return value;
}
}
🔍 Key Points:
 wait() → Makes a thread pause and release the lock.
 notify() → Wakes one waiting thread.
 notifyAll() → Wakes all waiting threads.

4️⃣ Deadlock (Thread Blocking Issue)


Deadlock occurs when two or more threads wait for each other indefinitely.
📌 Example:
class Resource {
synchronized void methodA(Resource r) {
r.methodB(this); // Thread waiting on another thread
}

synchronized void methodB(Resource r) {


r.methodA(this); // Circular dependency (Deadlock!)
}
}
🔍 Solution:
 Avoid nested locks.
 Use tryLock() from ReentrantLock.

5️⃣ Suspending & Resuming Threads


Old methods (suspend(), resume(), stop()) are deprecated due to safety issues. Instead, use:
✅ Modern Approach:
class MyThread extends Thread {
private volatile boolean running = true;
public void run() {
while (running) {
System.out.println("Thread is running...");
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
}
public void stopThread() {
running = false; // Graceful stopping
}}

12
🔍 Key Points:
 Use volatile boolean flag to control execution.
 Avoid stop() as it may leave shared resources in an inconsistent state.

💡 Summary:
 Use priorities (setPriority()) but don’t rely on them.
 Use synchronization (synchronized) to avoid data inconsistency.
 Use wait() & notify() for better thread communication.
 Avoid deadlocks by managing resource locks properly.
 Use modern thread control (volatile flag) instead of old methods.

Example: Creating Threads in Java


1. Using Thread Class
class MyThread extends Thread {
public void run() {
System.out.println("Thread Running: " + Thread.currentThread().getName());
} }
public class ThreadExample {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start(); // Start the thread
} }
2. Using Runnable Interface
class MyRunnable implements Runnable {
public void run() {
System.out.println("Thread Running: " + Thread.currentThread().getName());
}
}
public class RunnableExample {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable());
t1.start();
} }
Thread Class
Java provides two primary ways to create a thread:
1. Extending the Thread class
2. Implementing the Runnable interface (preferred for better code reusability)
Method 1: Extending the Thread Class
You can create a thread by subclassing Thread and overriding the run() method.
Example: Creating a Thread using Thread Class
class MyThread extends Thread {
public void run() {
System.out.println("Thread running: " + Thread.currentThread().getName());
}
}
public class ThreadExample {
public static void main(String[] args) {
13
MyThread t1 = new MyThread(); // Creating a thread object
t1.start(); // Starting the thread
}
}
Method 2: Implementing Runnable Interface (Preferred)
Instead of extending Thread, implementing Runnable is a better approach because Java does not support
multiple inheritance.
class MyRunnable implements Runnable {
public void run() {
System.out.println("Thread running: " + Thread.currentThread().getName());
}
}
public class RunnableExample {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable()); // Creating a thread object
t1.start(); // Starting the thread
}
}

2. Important Methods of the Thread Class

3. Thread Lifecycle (States)


A thread can be in one of the following states:
1. New – Thread is created but not started.
2. Runnable – start() has been called, and it is ready to run.
3. Running – The thread is currently executing.
4. Blocked/Waiting – The thread is waiting due to synchronization or sleep().
5. Terminated – The thread has completed execution.

4. Example: Thread Sleep and Join


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

public class ThreadMethodsExample {


public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
14
t1.start();
try {
t1.join(); // Main thread waits for t1 to finish
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
t2.start();
}
}
Output
Thread-0 - 1
Thread-0 - 2
Thread-0 - 3
Thread-0 - 4
Thread-0 - 5
Thread-1 - 1
Thread-1 - 2
Thread-1 - 3
Thread-1 - 4
Thread-1 - 5
Here, join() ensures that t2 starts only after t1 completes execution.

5. Example: Thread Priority


Java allows setting priorities to threads using setPriority(). However, thread scheduling is dependent on the
JVM and OS.
class PriorityThread extends Thread {
public void run() {
System.out.println(Thread.currentThread().getName() + " - Priority: " +
Thread.currentThread().getPriority());
}
}

public class ThreadPriorityExample {


public static void main(String[] args) {
PriorityThread t1 = new PriorityThread();
PriorityThread t2 = new PriorityThread();

t1.setPriority(Thread.MIN_PRIORITY); // Priority 1
t2.setPriority(Thread.MAX_PRIORITY); // Priority 10

t1.start();
t2.start();
}
}
Output
Thread-0 - Priority: 1
15
Thread-1 - Priority: 10
(Note: The execution order is not guaranteed.)

6. Daemon Threads
A daemon thread is a background thread that automatically terminates when all user threads finish
execution.
class DaemonExample extends Thread {
public void run() {
while (true) {
System.out.println("Daemon thread running...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
}
}
}
public class DaemonThreadExample {
public static void main(String[] args) {
DaemonExample daemon = new DaemonExample();
daemon.setDaemon(true); // Set the thread as a daemon thread
daemon.start();

System.out.println("Main thread sleeping...");


try {
Thread.sleep(3000);
} catch (InterruptedException e) {
System.out.println("Main thread interrupted");
}
System.out.println("Main thread finished execution.");
}
}
Output
Main thread sleeping...
Daemon thread running...
Daemon thread running...
Daemon thread running...
Main thread finished execution.
The daemon thread stops automatically when the main thread exits.

7. Summary
 The Thread class provides methods to create and manage threads in Java.
 The start() method initiates a new thread, while run() contains the thread’s logic.
 The sleep(), join(), and yield() methods control thread execution.
 The synchronized keyword ensures thread-safe execution.
 Thread priority can be set but is OS-dependent.
16
 Daemon threads run in the background and terminate when all user threads finish.

Runnable Interface

1. Why Use Runnable Interface?


Java provides two ways to create a thread:
1. Extending the Thread class
2. Implementing the Runnable interface (Preferred)
Using Runnable is better because:
✔ Java does not support multiple inheritance, so extending Thread limits class flexibility.
✔ It promotes better object-oriented design by separating thread logic from class hierarchy.

2. Implementing the Runnable Interface


The Runnable interface has only one method:
public interface Runnable {
void run(); // Must be implemented by any Runnable class
}
When a class implements Runnable, it must override run() and then pass an instance of the class to a
Thread object.

3. Example: Implementing Runnable


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

public class RunnableExample {


public static void main(String[] args) {
MyRunnable obj = new MyRunnable(); // Create a Runnable instance
Thread t1 = new Thread(obj); // Pass the instance to a Thread object
Thread t2 = new Thread(obj);

t1.start(); // Start thread 1


t2.start(); // Start thread 2
}
}
Output (Threads run independently)
Thread-0 - 1
Thread-1 - 1
17
Thread-0 - 2
Thread-1 - 2
...

4. Thread Synchronization Using Runnable


When multiple threads share a common resource, synchronization is necessary to avoid race conditions.
class Counter implements Runnable {
private int count = 0;

public synchronized void run() { // Synchronizing run method


for (int i = 0; i < 5; i++) {
count++;
System.out.println(Thread.currentThread().getName() + " - Count: " + count);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
}
}
}

public class SynchronizedRunnable {


public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(counter);
Thread t2 = new Thread(counter);

t1.start();
t2.start();
}
}
Output (Avoids race conditions)
Thread-0 - Count: 1
Thread-0 - Count: 2
Thread-1 - Count: 3
Thread-1 - Count: 4
...

5. Using Runnable with Lambda Expressions (Java 8+)


Java 8 introduced lambda expressions, allowing a cleaner way to define a Runnable:
public class LambdaRunnable {
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + " - " + i);
}
18
};

Thread t1 = new Thread(task);


Thread t2 = new Thread(task);

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

6. Advantages of Using Runnable


✅ Better Object-Oriented Design - Separates thread logic from class inheritance.
✅ Multiple Inheritance - A class can extend another class while implementing Runnable.
✅ Reusability - The same Runnable instance can be used with multiple threads.

Synchronization in Java

1. Why Synchronization?
When multiple threads access a shared resource (e.g., a variable or an object), data inconsistency can
occur. Synchronization ensures that only one thread accesses the critical section of the code at a time.

2. Types of Synchronization in Java


1. Synchronized Methods
2. Synchronized Blocks
3. Static Synchronization
4. Using ReentrantLock (Java 5+)

3. Synchronized Methods
A synchronized method ensures that only one thread can execute it at a time.
Example: Synchronizing a Counter
class Counter {
private int count = 0;

// Synchronized method
public synchronized void increment() {
count++;
System.out.println(Thread.currentThread().getName() + " - Count: " + count);
}
}
class MyThread extends Thread {
Counter counter;
public MyThread(Counter counter) {
this.counter = counter;
}
public void run() {
for (int i = 0; i < 5; i++) {
counter.increment();
19
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
}
}
}
public class SyncExample {
public static void main(String[] args) {
Counter counter = new Counter();

Thread t1 = new MyThread(counter);


Thread t2 = new MyThread(counter);

t1.start();
t2.start();
}
}
Output (Ensures proper count updates)
Thread-0 - Count: 1
Thread-1 - Count: 2
Thread-0 - Count: 3
Thread-1 - Count: 4
...

4. Synchronized Blocks
Instead of synchronizing the entire method, we can synchronize only the critical section of the code.
Example: Synchronizing a Block of Code
class Counter {
private int count = 0;

public void increment() {


synchronized (this) { // Synchronized block
count++;
System.out.println(Thread.currentThread().getName() + " - Count: " + count);
}
}
}
Why Use Synchronized Blocks?
✔ Reduces the performance overhead of synchronizing the entire method.
✔ Only locks the required section instead of the whole method.

5. Static Synchronization
If a method is static, synchronization applies to the class level, not the instance level. This means only one
thread can access a static synchronized method across all instances.
20
Example: Synchronizing a Static Method
class SharedResource {
private static int count = 0;

public static synchronized void increment() { // Static synchronized method


count++;
System.out.println(Thread.currentThread().getName() + " - Count: " + count);
}
}

class MyThread extends Thread {


public void run() {
SharedResource.increment();
}
}

public class StaticSyncExample {


public static void main(String[] args) {
Thread t1 = new MyThread();
Thread t2 = new MyThread();

t1.start();
t2.start();
}
}
Static synchronization is needed when:
✔ A shared resource is static (belongs to the class, not instances).
✔ We need to synchronize across all objects of the class.

Synchronized Methods

1. Why Use Synchronized Methods?


When multiple threads access a shared resource, there is a chance of data inconsistency. A synchronized
method ensures that only one thread can execute the method at a time.

2. Syntax of a Synchronized Method


synchronized returnType methodName(parameters) {
// Critical section - only one thread can access at a time
}

3. Example: Using a Synchronized Method


Problem Without Synchronization
Without synchronization, multiple threads can modify a shared resource simultaneously, leading to
incorrect results.
class Counter {
private int count = 0;

21
public void increment() { // Not synchronized
count++;
System.out.println(Thread.currentThread().getName() + " - Count: " + count);
}
}

class MyThread extends Thread {


Counter counter;

public MyThread(Counter counter) {


this.counter = counter;
}

public void run() {


for (int i = 0; i < 5; i++) {
counter.increment();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
}
}
}

public class WithoutSynchronization {


public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new MyThread(counter);
Thread t2 = new MyThread(counter);

t1.start();
t2.start();
}
}
Output (Data inconsistency)
Thread-0 - Count: 1
Thread-1 - Count: 2
Thread-0 - Count: 3
Thread-1 - Count: 3 <-- Wrong count!
...
Here, multiple threads modify the count variable simultaneously, causing inconsistent results.

4. Solving the Problem with Synchronized Methods


By making increment() synchronized, we ensure that only one thread can modify the shared variable at a
time.
class Counter {
22
private int count = 0;

// Synchronized method
public synchronized void increment() {
count++;
System.out.println(Thread.currentThread().getName() + " - Count: " + count);
}
}

class MyThread extends Thread {


Counter counter;

public MyThread(Counter counter) {


this.counter = counter;
}

public void run() {


for (int i = 0; i < 5; i++) {
counter.increment();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
}
}
}

public class WithSynchronization {


public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new MyThread(counter);
Thread t2 = new MyThread(counter);

t1.start();
t2.start();
}
}
Output (Correct Count)
Thread-0 - Count: 1
Thread-0 - Count: 2
Thread-0 - Count: 3
Thread-1 - Count: 4
Thread-1 - Count: 5
...
Now, one thread completes execution before the next thread can access increment(), ensuring correct
values.
23
5. Important Points About Synchronized Methods
✅ A synchronized method locks the entire object (this), allowing only one thread to access it at a time.
✅ If one thread is inside a synchronized method, other threads must wait until the method is free.
✅ It is useful for instance methods that modify shared data.

6. Synchronized Methods in Static Context


If a method is static, synchronization applies at the class level instead of the instance level.
Example: Static Synchronized Method
class SharedResource {
private static int count = 0;

public static synchronized void increment() { // Static synchronized method


count++;
System.out.println(Thread.currentThread().getName() + " - Count: " + count);
}
}

class MyThread extends Thread {


public void run() {
SharedResource.increment();
}
}

public class StaticSyncExample {


public static void main(String[] args) {
Thread t1 = new MyThread();
Thread t2 = new MyThread();

t1.start();
t2.start();
}
}
Why Use Static Synchronization?
✔ Locks the entire class, ensuring that multiple threads do not access the static method at the same
time.
✔ Prevents data corruption in shared static resources.

7. When to Use Synchronized Methods?


Use synchronized methods when:
✔ You have shared resources that multiple threads access.
✔ You need thread safety without blocking too many operations.
✔ You want to avoid data inconsistency issues (e.g., race conditions).
When Not to Use Synchronized Methods?
❌ If synchronization is not necessary, using it slows down performance.
❌ For small sections of code, use synchronized blocks instead (to reduce the performance overhead).

24
8. Alternative: Using Synchronized Blocks
If synchronizing the entire method is too restrictive, use synchronized blocks instead.
class Counter {
private int count = 0;

public void increment() {


synchronized (this) { // Synchronized block
count++;
System.out.println(Thread.currentThread().getName() + " - Count: " + count);
}
}
}
✔ Synchronized blocks reduce the performance overhead by locking only the critical section of the code.

Using Synchronized Statement in Java


A synchronized statement (synchronized block) in Java is used to provide fine-grained control over
synchronization. Instead of locking an entire method, you can lock only a critical section of code.
🔹 Why use synchronized statements?
 Increases performance by reducing the lock area.
 Prevents unnecessary blocking of non-critical code.
 Provides better thread efficiency compared to synchronized methods.

2. Syntax of Synchronized Statement


synchronized (lockObject) {
// Critical section - only one thread can execute at a time
}
Here, lockObject is the object used for synchronization. Only one thread can access this block at a time.

3. Example Without Synchronization (Data Corruption)


class Counter {
private int count = 0;

public void increment() { // Not synchronized


count++;
System.out.println(Thread.currentThread().getName() + " - Count: " + count);
}
}

class MyThread extends Thread {


Counter counter;

public MyThread(Counter counter) {


this.counter = counter;
}

public void run() {


25
for (int i = 0; i < 5; i++) {
counter.increment();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
}
}
}

public class WithoutSyncBlock {


public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new MyThread(counter);
Thread t2 = new MyThread(counter);

t1.start();
t2.start();
}
}
Output (Incorrect Count due to Race Condition)
Thread-0 - Count: 1
Thread-1 - Count: 2
Thread-0 - Count: 3
Thread-1 - Count: 3 <-- Wrong count!
...
Multiple threads are modifying count simultaneously, leading to incorrect results.

4. Solution Using Synchronized Statement


We can fix the issue by using a synchronized block inside the method instead of synchronizing the entire
method.
class Counter {
private int count = 0;

public void increment() {


synchronized (this) { // Synchronized block
count++;
System.out.println(Thread.currentThread().getName() + " - Count: " + count);
}
}
}

class MyThread extends Thread {


Counter counter;

public MyThread(Counter counter) {


26
this.counter = counter;
}

public void run() {


for (int i = 0; i < 5; i++) {
counter.increment();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
}
}
}

public class WithSyncBlock {


public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new MyThread(counter);
Thread t2 = new MyThread(counter);

t1.start();
t2.start();
}
}
Output (Correct Count)
Thread-0 - Count: 1
Thread-1 - Count: 2
Thread-0 - Count: 3
Thread-1 - Count: 4
Thread-0 - Count: 5
...
Here, the synchronized block ensures that only one thread modifies count at a time.

5. Why Use Synchronized Blocks Instead of Synchronized Methods?


Feature Synchronized Method Synchronized Block
Lock Scope Locks entire method Locks only a part of the method
Performance Slower (more lock contention) Faster (minimizes lock area)
When entire method must be When only a critical section needs
Use Case
synchronized synchronization

6. Example: Synchronizing a Shared Resource (Bank Account)


class BankAccount {
private int balance = 1000;

public void withdraw(int amount) {

27
synchronized (this) { // Only this block is synchronized
if (balance >= amount) {
System.out.println(Thread.currentThread().getName() + " is withdrawing: " + amount);
balance -= amount;
System.out.println("Remaining Balance: " + balance);
} else {
System.out.println(Thread.currentThread().getName() + " - Not enough balance!");
}
}
}
}

class UserThread extends Thread {


BankAccount account;
int amount;

public UserThread(BankAccount account, int amount) {


this.account = account;
this.amount = amount;
}

public void run() {


account.withdraw(amount);
}
}

public class SyncBlockExample {


public static void main(String[] args) {
BankAccount account = new BankAccount();

Thread t1 = new UserThread(account, 600);


Thread t2 = new UserThread(account, 500);

t1.start();
t2.start();
}
}
Output
Thread-0 is withdrawing: 600
Remaining Balance: 400
Thread-1 - Not enough balance!
🔹 Here, the synchronized block ensures that only one user withdraws at a time, preventing over-
withdrawal.

7. Synchronizing a Static Method (Class-Level Lock)


If a method is static, we should use a class-level lock (ClassName.class).
class SharedResource {
28
private static int count = 0;

public static void increment() {


synchronized (SharedResource.class) { // Class-level lock
count++;
System.out.println(Thread.currentThread().getName() + " - Count: " + count);
}
}
}

class MyThread extends Thread {


public void run() {
SharedResource.increment();
}
}

public class StaticSyncBlock {


public static void main(String[] args) {
Thread t1 = new MyThread();
Thread t2 = new MyThread();

t1.start();
t2.start();
}
}
✔ Class-level synchronization ensures that even if multiple objects exist, only one thread can execute the
block.

8. When to Use Synchronized Statements?


✅ Use synchronized statements when you need to:
 Protect only a specific section of the code.
 Improve performance by reducing lock contention.
 Synchronize static methods or resources (ClassName.class).
❌ Do not use synchronized statements when:
 The entire method must be thread-safe (use synchronized methods instead).
 There are better alternatives (e.g., ReentrantLock).

Inter-Thread Communication
1. What is Inter-Thread Communication?
Inter-thread communication allows multiple threads to communicate and coordinate their actions in a
synchronized manner. It is primarily used in producer-consumer problems, where one thread produces
data, and another thread consumes it.
🔹 Key Methods for Inter-Thread Communication (From Object Class)
Method Description
wait() Causes the thread to wait until another thread calls notify() or notifyAll().
notify() Wakes up a single thread that is waiting on the object's monitor.

29
Method Description
notifyAll() Wakes up all threads that are waiting on the object's monitor.
💡 These methods must be called inside a synchronized block or synchronized method; otherwise, Java
will throw IllegalMonitorStateException.

2. Example: Producer-Consumer Problem (Using wait() and notify())


class SharedResource {
private int data;
private boolean available = false; // Flag to check if data is ready

// Producer method
public synchronized void produce(int value) {
while (available) { // If data is already available, wait
try {
wait(); // Wait until consumer consumes the data
} catch (InterruptedException e) {
System.out.println("Producer interrupted");
}
}
data = value;
System.out.println("Produced: " + value);
available = true;
notify(); // Notify the consumer that data is ready
}

// Consumer method
public synchronized void consume() {
while (!available) { // If no data is available, wait
try {
wait(); // Wait until producer produces data
} catch (InterruptedException e) {
System.out.println("Consumer interrupted");
}
}
System.out.println("Consumed: " + data);
available = false;
notify(); // Notify producer that data is consumed
}
}

// Producer Thread
class Producer extends Thread {
SharedResource resource;

public Producer(SharedResource resource) {


this.resource = resource;

30
}

public void run() {


for (int i = 1; i <= 5; i++) {
resource.produce(i);
try {
Thread.sleep(1000); // Simulating time to produce
} catch (InterruptedException e) {
System.out.println("Producer interrupted");
}
}
}
}

// Consumer Thread
class Consumer extends Thread {
SharedResource resource;

public Consumer(SharedResource resource) {


this.resource = resource;
}

public void run() {


for (int i = 1; i <= 5; i++) {
resource.consume();
try {
Thread.sleep(1500); // Simulating time to consume
} catch (InterruptedException e) {
System.out.println("Consumer interrupted");
}
}
}
}

// Main Class
public class InterThreadCommunication {
public static void main(String[] args) {
SharedResource resource = new SharedResource();
Producer producer = new Producer(resource);
Consumer consumer = new Consumer(resource);

producer.start();
consumer.start();
}
}

4. Using notifyAll() for Multiple Consumers


31
If multiple consumers are waiting, we use notifyAll() to wake up all threads.
public synchronized void produce(int value) {
while (available) {
try {
wait();
} catch (InterruptedException e) {
System.out.println("Producer interrupted");
}
}
data = value;
System.out.println("Produced: " + value);
available = true;
notifyAll(); // Wakes up all waiting threads
}

5. Alternative: Using Lock and Condition (Modern Approach)


Instead of wait() and notify(), we can use ReentrantLock and Condition (introduced in Java 5).
import java.util.concurrent.locks.*;

class SharedResource {
private int data;
private boolean available = false;
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();

public void produce(int value) {


lock.lock();
try {
while (available) {
condition.await(); // Wait until data is consumed
}
data = value;
System.out.println("Produced: " + value);
available = true;
condition.signal(); // Notify waiting consumer
} catch (InterruptedException e) {
System.out.println("Producer interrupted");
} finally {
lock.unlock();
}
}

public void consume() {


lock.lock();
try {
while (!available) {
condition.await(); // Wait until data is produced
32
}
System.out.println("Consumed: " + data);
available = false;
condition.signal(); // Notify waiting producer
} catch (InterruptedException e) {
System.out.println("Consumer interrupted");
} finally {
lock.unlock();
}
}
}
✔ Lock and Condition provide better control and avoid spurious wakeups compared to wait() and
notify().

6. Summary
Feature wait() & notify() Lock & Condition
Synchronization Uses intrinsic object lock Uses explicit lock (ReentrantLock)
notify() wakes only one thread, notifyAll() More precise control with signal() and
Wake-Up Control
wakes all signalAll()
Performance Slightly slower due to JVM overhead Faster and more efficient
Use Case Simple producer-consumer problems Complex synchronization scenarios

7. When to Use Inter-Thread Communication?


✅ Use wait() and notify() when:
 You need basic thread communication.
 You are working with older Java versions (< Java 5).
 You are fine with spurious wakeups.
✅ Use Lock and Condition when:
 You need precise control over thread wake-ups.
 You want better performance and avoid spurious wakeups.
 You are using Java 5 or later.

Deadlock in Java
Deadlock
A deadlock occurs in a multi-threaded program when two or more threads are waiting for each other to
release resources, and none can proceed. This leads to an infinite waiting state, effectively freezing the
program.

2. How Does Deadlock Happen?


Deadlocks typically occur when:
1. Multiple threads hold locks on different resources.
2. Each thread waits for a resource that another thread is holding.
3. The cycle of dependency causes a situation where no thread can proceed.
🔹 Example of a Deadlock Situation
 Thread A locks Resource 1 and waits for Resource 2 (held by Thread B).
 Thread B locks Resource 2 and waits for Resource 1 (held by Thread A).

33
 Neither thread can proceed, causing a deadlock.

3. Example: Deadlock in Java


class SharedResource {
static final Object RESOURCE_1 = new Object();
static final Object RESOURCE_2 = new Object();
}

// Thread 1 tries to lock RESOURCE_1 first, then RESOURCE_2


class ThreadA extends Thread {
public void run() {
synchronized (SharedResource.RESOURCE_1) {
System.out.println("Thread A: Locked RESOURCE_1");

try { Thread.sleep(100); } catch (InterruptedException e) {}

System.out.println("Thread A: Waiting for RESOURCE_2...");


synchronized (SharedResource.RESOURCE_2) {
System.out.println("Thread A: Locked RESOURCE_2");
}
}
}
}

// Thread 2 tries to lock RESOURCE_2 first, then RESOURCE_1


class ThreadB extends Thread {
public void run() {
synchronized (SharedResource.RESOURCE_2) {
System.out.println("Thread B: Locked RESOURCE_2");

try { Thread.sleep(100); } catch (InterruptedException e) {}

System.out.println("Thread B: Waiting for RESOURCE_1...");


synchronized (SharedResource.RESOURCE_1) {
System.out.println("Thread B: Locked RESOURCE_1");
}
}
}
}

public class DeadlockExample {


public static void main(String[] args) {
ThreadA threadA = new ThreadA();
ThreadB threadB = new ThreadB();

threadA.start();
threadB.start();
34
}
}

4. Output (Illustrating Deadlock)


Thread A: Locked RESOURCE_1
Thread B: Locked RESOURCE_2
Thread A: Waiting for RESOURCE_2...
Thread B: Waiting for RESOURCE_1...
 Both threads are waiting for each other to release a resource, causing a deadlock.

5. How to Prevent Deadlocks?


✅ Solution 1: Always Lock Resources in the Same Order
By ensuring that all threads acquire locks in the same order, deadlock can be prevented.
class ThreadA extends Thread {
public void run() {
synchronized (SharedResource.RESOURCE_1) {
System.out.println("Thread A: Locked RESOURCE_1");

synchronized (SharedResource.RESOURCE_2) {
System.out.println("Thread A: Locked RESOURCE_2");
}
}
}
}

class ThreadB extends Thread {


public void run() {
synchronized (SharedResource.RESOURCE_1) { // Same order as ThreadA
System.out.println("Thread B: Locked RESOURCE_1");

synchronized (SharedResource.RESOURCE_2) {
System.out.println("Thread B: Locked RESOURCE_2");
}
}
}
}
 Now, both threads lock resources in the same order, preventing a cycle.

35
I/O Streams in Java
1. What are I/O Streams?
I/O (Input/Output) streams in Java allow programs to read data from and write data to different sources,
such as files, memory, or network connections.
🔹 Types of Streams in Java
Java provides two main categories of I/O streams:
1. Byte Streams (InputStream & OutputStream) → Handle binary data (e.g., images, audio files).
2. Character Streams (Reader & Writer) → Handle text data (e.g., .txt, .csv files).

2. Byte Streams (For Binary Data)


These streams are used to handle raw byte data, making them suitable for files like images, videos, and
audio.
🔹 Key Classes for Byte Streams
Class Description
InputStream Abstract class for reading byte data.
OutputStream Abstract class for writing byte data.
FileInputStream Reads byte data from a file.
FileOutputStream Writes byte data to a file.
BufferedInputStream Improves performance using buffering.
BufferedOutputStream Enhances output efficiency.
🔹 Example: Reading a File Using Byte Stream
import java.io.FileInputStream;
import java.io.IOException;

public class ByteStreamExample {


public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("example.txt")) {
int byteData;
while ((byteData = fis.read()) != -1) {
System.out.print((char) byteData); // Convert byte to char
}
} catch (IOException e) {
System.out.println("Error reading file: " + e.getMessage());
}
}
}
 Uses FileInputStream to read raw bytes and convert them into characters.
 try-with-resources ensures the file is automatically closed.

3. Character Streams (For Text Data)


Character streams process text-based data (Unicode characters).
🔹 Key Classes for Character Streams
Class Description
Reader Abstract class for reading character data.
Writer Abstract class for writing character data.

36
Class Description
FileReader Reads character data from a file.
FileWriter Writes character data to a file.
BufferedReader Improves reading performance using buffering.
BufferedWriter Enhances writing efficiency.
🔹 Example: Reading a File Using Character Stream
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class CharacterStreamExample {


public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(new FileReader("example.txt"))) {
String line;
while ((line = br.readLine()) != null) { // Reads line by line
System.out.println(line);
}
} catch (IOException e) {
System.out.println("Error reading file: " + e.getMessage());
}
}
}
 Uses BufferedReader to read text files line by line, improving performance.

4. Buffered Streams (For Efficiency)


Buffered streams improve performance by reducing disk I/O operations.
🔹 Example: Writing a File Using BufferedWriter
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

public class BufferedWriterExample {


public static void main(String[] args) {
try (BufferedWriter bw = new BufferedWriter(new FileWriter("output.txt"))) {
bw.write("Hello, this is a BufferedWriter example.");
bw.newLine(); // Writes a new line
bw.write("It improves file writing performance!");
} catch (IOException e) {
System.out.println("Error writing file: " + e.getMessage());
}
}
}
 Uses buffering to reduce direct disk writes, improving efficiency.

5. Data Streams (For Primitive Data Types)


Used for reading and writing primitive data types (int, double, boolean, etc.).
37
🔹 Example: Writing & Reading Primitive Data Using Data Streams
import java.io.DataOutputStream;
import java.io.DataInputStream;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.IOException;

public class DataStreamExample {


public static void main(String[] args) {
String file = "data.bin";

// Writing primitive data to a file


try (DataOutputStream dos = new DataOutputStream(new FileOutputStream(file))) {
dos.writeInt(100);
dos.writeDouble(99.99);
dos.writeBoolean(true);
} catch (IOException e) {
System.out.println("Error writing file: " + e.getMessage());
}

// Reading primitive data from the file


try (DataInputStream dis = new DataInputStream(new FileInputStream(file))) {
System.out.println("Integer: " + dis.readInt());
System.out.println("Double: " + dis.readDouble());
System.out.println("Boolean: " + dis.readBoolean());
} catch (IOException e) {
System.out.println("Error reading file: " + e.getMessage());
}
}
}
 DataOutputStream writes primitive types.
 DataInputStream reads primitive values.

7. Summary: Types of Java I/O Streams


Stream Type Purpose Classes
Process binary data (e.g., images, InputStream, OutputStream, FileInputStream,
Byte Streams
audio) FileOutputStream
Character
Process text data Reader, Writer, FileReader, FileWriter
Streams
Buffered
Improve performance BufferedReader, BufferedWriter
Streams
Data Streams Read/write primitive data DataInputStream, DataOutputStream
Object Streams Read/write objects (serialization) ObjectInputStream, ObjectOutputStream

Concepts of Streams in Java


38
1. Introduction to Streams
A stream in Java is a continuous flow of data from a source to a destination. Java provides built-in classes
to handle input and output (I/O) operations efficiently.
Streams abstract low-level data transfer between:
 Programs and files
 Programs and memory
 Programs and network connections
🔹 Key Characteristics of Streams
✔ Unidirectional (Either input or output)
✔ Sequential flow of data
✔ Abstracts complex I/O operations

2. Types of Streams in Java


Java provides two main types of streams based on data type:
🔹 1. Byte Streams (For Binary Data)
 Used to read and write raw binary data (e.g., images, audio, video).
 Works with 8-bit bytes.
 Uses InputStream and OutputStream.
🔹 2. Character Streams (For Text Data)
 Used for text-based data (Unicode characters).
 Works with 16-bit Unicode characters.
 Uses Reader and Writer.
Stream Type Base Class (Abstract) Used For
Byte Input Stream InputStream Reading binary data
Byte Output Stream OutputStream Writing binary data
Character Input Stream Reader Reading text data
Character Output Stream Writer Writing text data

3. Byte Streams (Handling Binary Data)


Used for reading and writing binary files (images, videos, audio, etc.).
🔹 Key Classes for Byte Streams
Class Description
InputStream Base class for byte input.
OutputStream Base class for byte output.
FileInputStream Reads bytes from a file.
FileOutputStream Writes bytes to a file.
BufferedInputStream Improves performance by buffering input.
BufferedOutputStream Enhances output efficiency.
🔹 Example: Reading a File Using Byte Stream
import java.io.FileInputStream;
import java.io.IOException;

public class ByteStreamExample {


public static void main(String[] args) {
39
try (FileInputStream fis = new FileInputStream("image.jpg")) {
int byteData;
while ((byteData = fis.read()) != -1) {
System.out.print(byteData + " "); // Print raw byte data
}
} catch (IOException e) {
System.out.println("Error reading file: " + e.getMessage());
}
}
}
 Reads raw bytes from image.jpg and prints byte values.

4. Character Streams (Handling Text Data)


Used for reading and writing text files.
🔹 Key Classes for Character Streams
Class Description
Reader Base class for character input.
Writer Base class for character output.
FileReader Reads characters from a file.
FileWriter Writes characters to a file.
BufferedReader Reads text efficiently.
BufferedWriter Writes text efficiently.
🔹 Example: Reading a File Using Character Stream
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class CharacterStreamExample {


public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(new FileReader("example.txt"))) {
String line;
while ((line = br.readLine()) != null) { // Reads line by line
System.out.println(line);
}
} catch (IOException e) {
System.out.println("Error reading file: " + e.getMessage());
}
}
}
 Reads line by line using BufferedReader.

5. Buffered Streams (For Performance Enhancement)


Buffered streams use an internal buffer to improve I/O efficiency.
🔹 Example: Writing a File Using BufferedWriter
import java.io.BufferedWriter;
import java.io.FileWriter;
40
import java.io.IOException;

public class BufferedWriterExample {


public static void main(String[] args) {
try (BufferedWriter bw = new BufferedWriter(new FileWriter("output.txt"))) {
bw.write("Hello, this is a BufferedWriter example.");
bw.newLine(); // Writes a new line
bw.write("It improves file writing performance!");
} catch (IOException e) {
System.out.println("Error writing file: " + e.getMessage());
}
}
}
 Buffers data before writing to reduce disk access.

6. Data Streams (Handling Primitive Data)


Java provides Data Streams for reading and writing primitive types (int, double, boolean, etc.).
🔹 Example: Writing & Reading Primitive Data
import java.io.DataOutputStream;
import java.io.DataInputStream;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.IOException;

public class DataStreamExample {


public static void main(String[] args) {
String file = "data.bin";

// Writing primitive data to a file


try (DataOutputStream dos = new DataOutputStream(new FileOutputStream(file))) {
dos.writeInt(100);
dos.writeDouble(99.99);
dos.writeBoolean(true);
} catch (IOException e) {
System.out.println("Error writing file: " + e.getMessage());
}

// Reading primitive data from the file


try (DataInputStream dis = new DataInputStream(new FileInputStream(file))) {
System.out.println("Integer: " + dis.readInt());
System.out.println("Double: " + dis.readDouble());
System.out.println("Boolean: " + dis.readBoolean());
} catch (IOException e) {
System.out.println("Error reading file: " + e.getMessage());
}
}
}
41
 Efficiently reads and writes primitive data.

7. Object Streams (Serialization & Deserialization)


Java allows objects to be converted into byte streams (serialization) and restored (deserialization).
🔹 Example: Serializing & Deserializing an Object
import java.io.*;

class Person implements Serializable {


private static final long serialVersionUID = 1L;
String name;
int age;

public Person(String name, int age) {


this.name = name;
this.age = age;
}
}

public class ObjectStreamExample {


public static void main(String[] args) {
String file = "person.dat";

// Serialization
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file))) {
Person p = new Person("John Doe", 30);
oos.writeObject(p);
} catch (IOException e) {
System.out.println("Error writing object: " + e.getMessage());
}

// Deserialization
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
Person p = (Person) ois.readObject();
System.out.println("Deserialized Person: " + p.name + ", Age: " + p.age);
} catch (IOException | ClassNotFoundException e) {
System.out.println("Error reading object: " + e.getMessage());
}
}
}
 Stores and retrieves Java objects from files.

Byte and Character Streams in Java

1. Introduction to Streams
Java uses streams to perform input and output (I/O) operations efficiently. Streams provide a common
interface for reading and writing data between different sources (files, memory, network, etc.).
 Byte Streams – Handle binary data (images, audio, etc.).
42
 Character Streams – Handle text data (Unicode characters).

2. Byte Streams (Binary Data Handling)


Byte streams are used to read and write raw binary data.
They do not perform character encoding/decoding.
🔹 Byte Stream Classes
Type Input Stream Output Stream
Base Class InputStream OutputStream
File Handling FileInputStream FileOutputStream
Array Handling ByteArrayInputStream ByteArrayOutputStream
Buffered Stream BufferedInputStream BufferedOutputStream
Primitive Data Handling DataInputStream DataOutputStream
Object Handling ObjectInputStream ObjectOutputStream

🔹 Example: Using Byte Streams (File Copy)


import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class ByteStreamExample {


public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("input.bin");
FileOutputStream fos = new FileOutputStream("output.bin")) {

int byteData;
while ((byteData = fis.read()) != -1) { // Read byte by byte
fos.write(byteData); // Write byte by byte
}
System.out.println("File copied successfully using Byte Streams.");
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
}
}
}
📝 Explanation:
 Reads data byte by byte.
 Suitable for binary files (e.g., images, videos).
 Uses FileInputStream and FileOutputStream.

3. Character Streams (Text Data Handling)


Character streams read and write Unicode characters.
They automatically handle character encoding/decoding.
🔹 Character Stream Classes
Type Reader (Input) Writer (Output)
Base Class Reader Writer
File Handling FileReader FileWriter
43
Type Reader (Input) Writer (Output)
Buffered Stream BufferedReader BufferedWriter
Array Handling CharArrayReader CharArrayWriter
String Handling StringReader StringWriter

🔹 Example: Using Character Streams (Read Text File)


import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class CharacterStreamExample {


public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(new FileReader("example.txt"))) {
String line;
while ((line = br.readLine()) != null) { // Read line by line
System.out.println(line);
}
} catch (IOException e) {
System.out.println("Error reading file: " + e.getMessage());
}
}
}
📝 Explanation:
 Reads text line by line.
 Uses BufferedReader for efficient reading.
 Suitable for text files (e.g., .txt, .csv).

4. Key Differences: Byte vs Character Streams


Feature Byte Stream Character Stream
Base Class InputStream / OutputStream Reader / Writer
Data Type Binary data (bytes) Text data (characters)
Used For Images, audio, videos Text files, documents
Encoding Handling No encoding/decoding Supports Unicode encoding
Example Classes FileInputStream, FileOutputStream FileReader, FileWriter

5. Buffered Streams (For Performance)


Buffered Streams improve efficiency by reducing the number of I/O operations.
🔹 Buffered Byte Streams
 BufferedInputStream
 BufferedOutputStream
🔹 Buffered Character Streams
 BufferedReader
 BufferedWriter
🔸 Example: Using Buffered Character Stream
import java.io.BufferedWriter;
import java.io.FileWriter;
44
import java.io.IOException;

public class BufferedWriterExample {


public static void main(String[] args) {
try (BufferedWriter bw = new BufferedWriter(new FileWriter("output.txt"))) {
bw.write("Buffered Writer Example");
bw.newLine();
bw.write("Enhances file writing performance!");
System.out.println("Text written successfully.");
} catch (IOException e) {
System.out.println("Error writing file: " + e.getMessage());
}
}
}
📝 Key Points:
✔ Improves efficiency by reducing disk I/O operations.
✔ Best for large files or frequent reads/writes.

6. When to Use Byte vs. Character Streams


Use Case Recommended Stream Type
Copying an image or video Byte Stream (FileInputStream)
Reading a text file (UTF-8 encoded) Character Stream (BufferedReader)
Writing a log file Character Stream (BufferedWriter)
Processing binary data (e.g., encryption) Byte Stream (ByteArrayInputStream)

7. Summary
Feature Byte Stream Character Stream
Data Type Binary (0s and 1s) Text (Unicode characters)
Used For Images, audio, video Text, documents
Example Classes FileInputStream, FileOutputStream FileReader, FileWriter
Buffered Variant BufferedInputStream, BufferedOutputStream BufferedReader, BufferedWriter

Reading Console Input

1. Introduction
Java provides multiple ways to read input from the console, which is useful for interacting with users.
The main methods include:
1. Using Scanner (Preferred method)
2. Using BufferedReader
3. Using System.console()
4. Using DataInputStream (Legacy method, not recommended)

2. Using Scanner (Most Common Method)


The Scanner class (introduced in Java 5) makes it easy to read different types of user input.
🔹 Example: Reading Different Inputs
import java.util.Scanner;
45
public class ScannerExample {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);

// Read an integer
System.out.print("Enter an integer: ");
int num = scanner.nextInt();

// Read a float
System.out.print("Enter a float: ");
float decimal = scanner.nextFloat();

// Read a string (single word)


System.out.print("Enter a single word: ");
String word = scanner.next();

// Read a full line


scanner.nextLine(); // Consume the leftover newline
System.out.print("Enter a full sentence: ");
String sentence = scanner.nextLine();

// Print the user inputs


System.out.println("\nYou entered:");
System.out.println("Integer: " + num);
System.out.println("Float: " + decimal);
System.out.println("Word: " + word);
System.out.println("Sentence: " + sentence);

scanner.close(); // Close the scanner


}
}
✅ Output Example
Enter an integer: 5
Enter a float: 3.14
Enter a single word: Java
Enter a full sentence: Java is powerful!

You entered:
Integer: 5
Float: 3.14
Word: Java
Sentence: Java is powerful!
📌 Why use Scanner? ✔ Supports various data types (int, float, double, string, etc.).
✔ Simple syntax and easy to use.
✔ Handles parsing of primitive data types automatically.

46
3. Using BufferedReader (Efficient for Large Inputs)
BufferedReader is faster than Scanner for large input because it reads input as a stream.
🔹 Example: Reading Input Using BufferedReader
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class BufferedReaderExample {


public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

// Read a string
System.out.print("Enter your name: ");
String name = br.readLine();

// Read an integer
System.out.print("Enter your age: ");
int age = Integer.parseInt(br.readLine());

System.out.println("\nHello, " + name + "! You are " + age + " years old.");
}
}
✅ Output Example
Enter your name: Alice
Enter your age: 25

Hello, Alice! You are 25 years old.


📌 Why use BufferedReader? ✔ Faster than Scanner (used in high-performance applications).
✔ Best for reading large text input efficiently.
✔ Does not parse primitive types automatically (must use Integer.parseInt(), etc.).

4. Using System.console() (For Password Input)


System.console() is useful for reading passwords securely since it does not display them on the screen.
🔹 Example: Reading Password Securely
import java.io.Console;

public class ConsoleExample {


public static void main(String[] args) {
Console console = System.console();

if (console != null) {
String username = console.readLine("Enter username: ");
char[] password = console.readPassword("Enter password: ");

System.out.println("Welcome, " + username + "!");

47
// Clear the password array
java.util.Arrays.fill(password, ' ');
} else {
System.out.println("Console is not available.");
}
}
}
✅ Output Example
Enter username: John
Enter password: (hidden input)

Welcome, John!
📌 Why use System.console()? ✔ Secure (hides password input).
✔ Best for command-line applications.
⚠ Not available in some IDEs (like Eclipse, IntelliJ).

5. Using DataInputStream (Legacy Method, Not Recommended)


This method was used in older Java versions but is now deprecated.
🔹 Example (Legacy Code)
import java.io.DataInputStream;
import java.io.IOException;

public class DataInputStreamExample {


public static void main(String[] args) throws IOException {
DataInputStream dis = new DataInputStream(System.in);

System.out.print("Enter your name: ");


String name = dis.readLine(); // Deprecated method

System.out.println("Hello, " + name + "!");


}
}
📌 Why Avoid DataInputStream? ❌ readLine() method is deprecated.
❌ No automatic parsing of primitive types.
❌ Use Scanner or BufferedReader instead.

6. Comparison Table: Console Input Methods


Handles Data Types Handles Large Input Secure for
Method Best For
Automatically? Efficiently? Passwords?
General input
Scanner ✅ Yes ❌ No ❌ No
(numbers, text)
BufferedReader Large text input ❌ No (needs parsing) ✅ Yes ❌ No
Secure password
System.console() ❌ No ❌ No ✅ Yes
input
DataInputStream Legacy applications ❌ No (deprecated) ❌ No ❌ No

48
7. Best Practices for Console Input
🔹 Use Scanner for small, simple inputs.
🔹 Use BufferedReader for large inputs (performance-efficient).
🔹 Use System.console() for password input (secure).
🔹 Avoid DataInputStream, as it is deprecated.

8. Summary
Method Key Features
Scanner Best for small user inputs, handles multiple data types
BufferedReader Best for large text input, high performance
System.console() Best for secure password input
DataInputStream Deprecated, not recommended

Writing Console Output in Java

1. Introduction
Java provides several ways to write output to the console, primarily using:
1. System.out.print() & System.out.println()
2. System.out.printf() (for formatted output)
3. System.out.format() (alternative to printf)
4. PrintWriter (for advanced output handling)

2. Using System.out.print() and System.out.println()


These are the most commonly used methods for console output.
🔹 Example: Using print() and println()
public class PrintExample {
public static void main(String[] args) {
System.out.print("Hello, "); // Prints without newline
System.out.println("World!"); // Prints with a newline
System.out.println("Java is powerful!");
}
}
✅ Output
Hello, World!
Java is powerful!
📌 Difference between print() and println()
✔ print(): Prints text without a newline.
✔ println(): Prints text and moves to the next line.

3. Using System.out.printf() for Formatted Output


The printf() method allows formatting similar to C-style printf().
🔹 Example: Formatting Output with printf()
public class PrintfExample {
public static void main(String[] args) {
int age = 25;
double score = 95.75;
49
System.out.printf("Age: %d years\n", age);
System.out.printf("Score: %.2f points\n", score);
}
}
✅ Output
Age: 25 years
Score: 95.75 points
📌 Format Specifiers in printf()
Specifier Meaning
%d Integer (Decimal)
%f Floating-point Number
%.2f Floating-point with 2 decimal places
%s String
%c Character
%n Newline (same as \n)

4. Using System.out.format() (Same as printf())


format() is an alias for printf().
🔹 Example: Using format()
public class FormatExample {
public static void main(String[] args) {
String name = "Alice";
double salary = 50000.50;

System.out.format("Employee: %s earns $%.2f per year%n", name, salary);


}
}
✅ Output
Employee: Alice earns $50000.50 per year
📌 format() is identical to printf() but improves readability.

5. Using PrintWriter for Advanced Output


The PrintWriter class provides more control over output, including writing to both console and files.
🔹 Example: Using PrintWriter for Console Output
import java.io.PrintWriter;

public class PrintWriterExample {


public static void main(String[] args) {
PrintWriter writer = new PrintWriter(System.out, true);

writer.println("Hello from PrintWriter!");


writer.printf("Formatted Number: %.3f%n", 123.456);
}
}
✅ Output
50
Hello from PrintWriter!
Formatted Number: 123.456
📌 Why use PrintWriter?
✔ Supports auto-flushing (ensures output appears immediately).
✔ Can write both to console and files.
✔ More flexible than System.out.

6. Comparison Table: Console Output Methods


Method Usage Supports Formatting? Best For
System.out.print() Print without newline ❌ No Simple text output
System.out.println() Print with newline ❌ No Line-by-line output
System.out.printf() Formatted output ✅ Yes Numbers, strings, custom formats
System.out.format() Same as printf() ✅ Yes Readable formatted output
PrintWriter Advanced output ✅ Yes Writing to console or files

7. Best Practices for Console Output


🔹 Use println() for normal text output.
🔹 Use printf() for formatted text and numbers.
🔹 Use PrintWriter for advanced console and file writing.
🔹 Use \t for tab spacing and \n for newlines if needed.

8. Summary
Method Key Features
print() Prints without newline
println() Prints with newline
printf() Formatted output (numbers, strings, etc.)
format() Same as printf(), improves readability
PrintWriter Advanced writing, supports auto-flushing

File Handling in Java

1. Introduction to File Handling in Java


Java provides a rich set of I/O classes in the java.io package for handling files.
Common file operations include:
✔ Reading from a file
✔ Writing to a file
✔ Appending data to a file
✔ Deleting a file

2. File Class (java.io.File)


The File class is used to represent file and directory paths.
🔹 Example: Checking if a File Exists
import java.io.File;

public class FileExample {

51
public static void main(String[] args) {
File file = new File("example.txt");

if (file.exists()) {
System.out.println("File exists: " + file.getAbsolutePath());
} else {
System.out.println("File does not exist.");
}
}
}
✅ Output (if file exists)
File exists: C:\Users\Documents\example.txt
📌 File Class Methods:
Method Description
exists() Checks if file exists
getName() Returns file name
getAbsolutePath() Returns absolute path
canRead() Checks if file is readable
canWrite() Checks if file is writable
length() Returns file size in bytes
delete() Deletes the file

3. Writing to a File using FileWriter


The FileWriter class allows writing characters to a file.
🔹 Example: Writing Data to a File
import java.io.FileWriter;
import java.io.IOException;

public class WriteToFile {


public static void main(String[] args) {
try {
FileWriter writer = new FileWriter("output.txt");
writer.write("Hello, this is a sample file!\n");
writer.write("Java file handling is easy.");
writer.close(); // Close the file
System.out.println("File written successfully.");
} catch (IOException e) {
System.out.println("An error occurred: " + e.getMessage());
}
}
}
✅ Output
File written successfully.
📌 Always close the file (writer.close()) to save changes!

4. Reading from a File using FileReader


52
The FileReader class reads characters from a file.
🔹 Example: Reading Data from a File
import java.io.FileReader;
import java.io.IOException;

public class ReadFromFile {


public static void main(String[] args) {
try {
FileReader reader = new FileReader("output.txt");
int character;
while ((character = reader.read()) != -1) {
System.out.print((char) character);
}
reader.close();
} catch (IOException e) {
System.out.println("Error reading file: " + e.getMessage());
}
}
}
✅ Output (File Content)
Hello, this is a sample file!
Java file handling is easy.

5. Using BufferedWriter for Efficient Writing


The BufferedWriter class improves performance by buffering data.
🔹 Example: Writing Data with BufferedWriter
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

public class BufferedWriteExample {


public static void main(String[] args) {
try {
BufferedWriter writer = new BufferedWriter(new FileWriter("buffered_output.txt"));
writer.write("This is a buffered writer example.");
writer.newLine();
writer.write("It improves writing performance.");
writer.close();
System.out.println("File written successfully using BufferedWriter.");
} catch (IOException e) {
System.out.println("An error occurred: " + e.getMessage());
}
}
}

6. Using BufferedReader for Efficient Reading


The BufferedReader class reads data line-by-line, improving performance.
53
🔹 Example: Reading Data with BufferedReader
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class BufferedReadExample {


public static void main(String[] args) {
try {
BufferedReader reader = new BufferedReader(new FileReader("buffered_output.txt"));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
} catch (IOException e) {
System.out.println("Error reading file: " + e.getMessage());
}
}
}
✅ Output
This is a buffered writer example.
It improves writing performance.
📌 Use readLine() to read one line at a time instead of character-by-character.

7. Appending Data to an Existing File


To add content without overwriting, use FileWriter(filename, true)
🔹 Example: Appending Data
import java.io.FileWriter;
import java.io.IOException;

public class AppendToFile {


public static void main(String[] args) {
try {
FileWriter writer = new FileWriter("output.txt", true);
writer.write("\nAppending a new line!");
writer.close();
System.out.println("Data appended successfully.");
} catch (IOException e) {
System.out.println("Error appending to file: " + e.getMessage());
}
}
}
📌 Second argument (true) enables append mode.

8. Deleting a File
To delete a file, use delete() from the File class.

54
🔹 Example: Deleting a File
import java.io.File;

public class DeleteFileExample {


public static void main(String[] args) {
File file = new File("output.txt");
if (file.delete()) {
System.out.println("File deleted successfully.");
} else {
System.out.println("Failed to delete the file.");
}
}
}
📌 Ensure the file is closed before deleting.

9. Comparison of File Handling Classes


Class Used For Best Feature
File File operations (existence, size, delete) Check & delete files
FileWriter Writing characters Simple writing
FileReader Reading characters Simple reading
BufferedWriter Writing with buffering Faster writing
BufferedReader Reading line-by-line Faster reading
PrintWriter Writing with formatting printf() support

55

You might also like