Skip to Content
Course content

79: Threads and the Runnable Interface

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 LogParser that implements Runnable.
  • The LogParser constructor should take a String 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 main method, create three different LogParser instances for "server.log", "auth.log", and "error.log".
  • Wrap each LogParser in a Thread object 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.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.