Week 3 – File IO and Multithreading
🎯 Learning Objectives
By the end of this week, you should be able to:
- Understand Java File IO architecture
- Read and write data using different IO classes
- Use BufferedReader and BufferedWriter efficiently
- Understand serialization and object persistence
- Explain what a thread is and why it is needed
- Create threads using Thread and Runnable
- Understand synchronization basics
- Use ExecutorService for thread management
- Identify race conditions and concurrency problems
Part 1 – Understanding File IO in Java
1️⃣ Why File IO?
So far, your programs store data in memory.
But what happens when:
- The application restarts?
- The JVM crashes?
- The system shuts down?
All in-memory data is lost.
File IO allows:
- Persistent storage
- Log writing
- Data import/export
- Configuration management
Backend systems heavily depend on file handling for:
- Logs
- Temporary data
- Batch processing
- Configuration files
2️⃣ Java IO Architecture Overview
Java IO is mainly divided into:
Byte Streams
Used for binary data:
- Images
- PDFs
- Audio
- Serialized objects
Main classes:
FileInputStreamFileOutputStream
Character Streams
Used for text data:
FileReaderFileWriterBufferedReaderBufferedWriter
Character streams are preferred for:
- Reading text files
- Logs
- CSV files
3️⃣ Reading Files in Java
Using FileReader (Basic)
FileReader reader = new FileReader("data.txt");
int character;
while ((character = reader.read()) != -1) {
System.out.print((char) character);
}
reader.close();
⚠️ Problem:
- Reads one character at a time.
- Inefficient for large files.
4️⃣ Using BufferedReader (Recommended)
BufferedReader br = new BufferedReader(new FileReader("data.txt"));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
br.close();
Advantages:
- Faster
- Reads line by line
- More practical for backend systems
5️⃣ Writing Files
Using FileWriter
FileWriter writer = new FileWriter("output.txt");
writer.write("Hello Backend World");
writer.close();
Using BufferedWriter
BufferedWriter bw = new BufferedWriter(new FileWriter("output.txt"));
bw.write("Hello Backend World");
bw.newLine();
bw.close();
BufferedWriter is preferred for performance.
6️⃣ Try-With-Resources (Important)
Always close resources.
Modern Java:
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
This prevents memory leaks.
7️⃣ Serialization
Serialization converts an object into byte stream.
Used when:
- Saving object state
- Sending over network
- Caching
Example:
class Student implements Serializable {
private String name;
private int age;
}
Writing object:
ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("student.ser")
);
out.writeObject(student);
out.close();
Reading object:
ObjectInputStream in = new ObjectInputStream(
new FileInputStream("student.ser")
);
Student s = (Student) in.readObject();
in.close();
Important:
- Class must implement
Serializable - serialVersionUID recommended
Part 2 – Multithreading
8️⃣ What is a Thread?
A thread is a lightweight unit of execution.
Every Java program has at least one thread:
- Main Thread
Multithreading allows:
- Parallel execution
- Better CPU utilization
- Faster processing
Backend examples:
- Handling multiple user requests
- Async logging
- Background tasks
9️⃣ Creating Threads
Method 1 – Extending Thread
class MyThread extends Thread {
public void run() {
System.out.println("Thread running");
}
}
MyThread t = new MyThread();
t.start();
Method 2 – Implementing Runnable (Preferred)
class MyTask implements Runnable {
public void run() {
System.out.println("Task running");
}
}
Thread t = new Thread(new MyTask());
t.start();
Why Runnable is preferred:
- Java does not support multiple inheritance
- More flexible design
🔟 Thread Lifecycle
States:
- NEW
- RUNNABLE
- BLOCKED
- WAITING
- TERMINATED
Important: Calling run() directly does NOT create a new thread. Always call start().
1️⃣1️⃣ Race Conditions
Occurs when:
- Multiple threads access shared resource
- At least one modifies it
- No synchronization
Example:
class Counter {
int count = 0;
void increment() {
count++;
}
}
Multiple threads → unpredictable result.
1️⃣2️⃣ Synchronization
To avoid race condition:
synchronized void increment() {
count++;
}
Or:
synchronized(this) {
count++;
}
Synchronization ensures:
- Only one thread enters critical section
1️⃣3️⃣ ExecutorService (Modern Approach)
Instead of manually managing threads:
ExecutorService executor = Executors.newFixedThreadPool(3);
executor.submit(() -> {
System.out.println("Task running");
});
executor.shutdown();
Advantages:
- Better resource management
- Thread pooling
- Production ready
1️⃣4️⃣ When NOT to Use Threads
Avoid threads when:
- Task is trivial
- You don’t understand shared state
- You don’t need parallelism
Concurrency increases complexity.
🧠 Backend Engineering Perspective
In real backend systems:
- Spring Boot uses thread pools
- Web servers handle multiple requests via threads
- Logging frameworks are async
- Databases handle concurrent transactions
Understanding threads helps you:
- Avoid deadlocks
- Avoid performance bottlenecks
- Write thread-safe code
📝 Summary
This week you learned:
- File reading and writing
- Buffered streams
- Serialization
- Thread creation
- Runnable vs Thread
- Synchronization
- ExecutorService
You are now transitioning from: “Java programmer”
to
“Backend engineer who understands execution model”
⚠️ Common Mistakes
- Forgetting to close files
- Calling run() instead of start()
- Ignoring synchronization
- Overusing threads
- Sharing mutable state without protection
📌 Before Moving to Exercises
Make sure you can answer:
- Why is BufferedReader better than FileReader?
- What happens if two threads increment a counter?
- Why is Runnable preferred over extending Thread?
- What problem does ExecutorService solve?
- When should you use synchronization?
If you cannot answer clearly, revisit the sections above.