Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
79: Threads and the Runnable Interface
I want to build a simple simulation of a file downloader. In a real app, you wouldn't want your entire UI to freeze while a 2GB file is downloading, right? You'd want that happening in the background. So, let's try to make a few "downloads" happen at the same time.
The "One Thing at a Time" Problem
First, I'll write a basic class that simulates a download by just sleeping the current thread for a second. I'll try to run three of these in a row in my main method.
public class FileDownloader {
public void download(String fileName) {
try {
System.out.println("Starting download: " + fileName);
Thread.sleep(1000); // Simulate work
System.out.println("Finished: " + fileName);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
FileDownloader downloader = new FileDownloader();
downloader.download("image.jpg");
downloader.download("video.mp4");
downloader.download("document.pdf");
}
}
When I run this, it takes exactly 3 seconds. It's sequential. "image.jpg" finishes, then "video.mp4" starts. That's not concurrency; that's just a list. To actually do these in parallel, I need to tell Java to run these on separate threads.
Trying the Thread Class
The most obvious way to do this is to make my class a Thread. If FileDownloader is a thread, it should be able to run its own logic independently. I'll move the download logic into the run() method, which is where the Thread class expects the "work" to be defined.
public class FileDownloader extends Thread {
private String fileName;
public FileDownloader(String fileName) {
this.fileName = fileName;
}
@Override
public void run() {
try {
System.out.println("Downloading " + fileName + " on " + Thread.currentThread().getName());
Thread.sleep(1000);
System.out.println("Finished " + fileName);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
FileDownloader t1 = new FileDownloader("image.jpg");
FileDownloader t2 = new FileDownloader("video.mp4");
FileDownloader t3 = new FileDownloader("document.pdf");
t1.run();
t2.run();
t3.run();
}
}
Wait. I ran this, and it's still taking 3 seconds. Why? I looked at my code and realized I called .run(). Calling run() directly is just like calling any other method in Java; it stays on the current thread (the main thread). To actually spawn a new OS-level thread, I have to call .start(). That tells the JVM to set up the new thread and then call run() internally.
I'll change those to t1.start(), t2.start(), and t3.start(). Now, the output is scrambled, and it all finishes in about 1 second. Success. But we've hit a architectural wall.
The Inheritance Wall
Here is the problem with extending Thread: Java doesn't support multiple inheritance. Imagine our FileDownloader needs to extend a BaseNetworkClient class to get access to authentication and socket logic. Since it already extends Thread, it can't extend BaseNetworkClient. We've used up our one and only "extends" slot just to get threading capabilities. That's a bad trade.
This is why we use the Runnable interface. Instead of saying "this class is a thread," we say "this class represents a task that can be run by a thread."
Decoupling the Task from the Thread
Let's rewrite this. I'll make FileDownloader implement Runnable. This means I'm just promising that this class has a run() method. Now, I can extend any other class I want, or nothing at all.
public class FileDownloader implements Runnable {
private String fileName;
public FileDownloader(String fileName) {
this.fileName = fileName;
}
@Override
public void run() {
System.out.println("Downloading " + fileName + " via Runnable!");
// imagine sleep and logic here...
}
public static void main(String[] args) {
// The task is separate from the worker
Runnable task1 = new FileDownloader("image.jpg");
Runnable task2 = new FileDownloader("video.mp4");
// We pass the task into a Thread object
Thread thread1 = new Thread(task1);
Thread thread2 = new Thread(task2);
thread1.start();
thread2.start();
}
}
This is much cleaner. The Runnable is the "what" (the job to be done), and the Thread is the "how" (the mechanism that executes the job). By separating them, our code is more flexible. I can pass that Runnable to a Thread, or later in the course, to an ExecutorService (a thread pool), without changing a single line of the FileDownloader logic.
📋 Practical Task
Multi-Threaded Log Parser Simulation
You are tasked with simulating a system that parses multiple large log files simultaneously. Instead of processing them one by one, you need to use the Runnable interface to process them in parallel.
Requirements:
- Create a class named
LogParserthat implementsRunnable. - The
LogParserconstructor should take aString fileName. - Inside the
run()method:- Print "Parsing [fileName]..."
- Use
Thread.sleep(1500)to simulate the time it takes to parse a large file. - Print "Completed parsing [fileName]."
- In your
mainmethod, create three differentLogParserinstances for "server.log", "auth.log", and "error.log". - Wrap each
LogParserin aThreadobject and start them all.
Verification: When you run the program, you should see all three "Parsing..." messages appear almost immediately, followed by a pause, and then the "Completed..." messages appearing in an unpredictable order.
There are no comments for now.