Skip to content

Java Multithreading Real World Example | Downloading PDFs In Different Threads | Real Time

Posted on:April 27, 2023 at 03:22 PM

Learning about Java multithreading can be really boring. Looking at the online videos on YouTube & Udemy, they all use simplistic examples that are just boring.

Why not use a real world example to illustrate how it really works?


We all love to download files don’t we. But what if we need to download thousands of PDFs for lets say work purposes? Maybe we also want to rename these files in bulk too. By using a single thread, it will take ages to download those pdfs. That’s where multi threading comes in.d

In this example, we define a DownloadManager class that implements the Runnable interface. The DownloadManager constructor takes two parameters: the URL of the file to be downloaded and the name of the file to be saved as.

In the run() method, we download the file using the provided URL and save it with the provided file name. We use a while loop to read from the input stream and write to the output stream in chunks of 1024 bytes. Finally, we close the input and output streams and print a success message to the console.

In the main() method, we create an array of 2 DownloadManager instances and start a new thread for each instance using a loop. Each thread downloads a different file, allowing multiple files to be downloaded simultaneously.

How Does This Work?


class DownloadManager implements Runnable {
   private String fileUrl;
   private String fileName;

   public DownloadManager(String fileUrl, String fileName) {
      this.fileUrl = fileUrl;
      this.fileName = fileName;

   public void run() {
      try {
         URL url = new URL(fileUrl);
         Here, we create an InputStream object using the FileInputStream class and pass in the path to the input file as a parameter. This creates a stream of bytes that represent the contents of the file.
         InputStream inputStream = url.openStream();

        we create a new FileOutputStream object called outputStream, and pass in the name of the file we want to write to as a parameter (example.txt). This creates a new file with the specified name if it doesn't already exist, or opens an existing file if it does.
         FileOutputStream outputStream = new FileOutputStream(fileName);

         // Next, we create a byte array called buffer with a size of 1024 bytes. This buffer will be used to read data from the input stream in chunks.
         byte[] buffer = new byte[1024];

        Then, we use a while loop to read data from the input stream in chunks and write it to the output stream.
        The read() method of the InputStream class reads up to buffer.length bytes of data from the input stream into the buffer and returns the number of bytes read.
        If the end of the stream is reached, the read() method returns -1.*/
         int length;
         while ((length = != -1) {

            Inside the while loop, we check if the read() method returned -1, which indicates that we've reached the end of the input stream. If it hasn't returned -1, we write the data that was read into the buffer to the output stream using the write() method of the OutputStream class. The write() method takes three parameters: the byte array containing the data to write, the offset in the array where the data starts, and the number of bytes to write. In this case, we pass in the buffer array, a starting offset of 0, and the number of bytes that were read from the input stream (bytesRead).

            This loop continues until the entire input stream has been read and written to the output stream.
            outputStream.write(buffer, 0, length);
         Finally, once all the data has been read from the input stream and written to the output stream, we close both streams to release any system resources that were being used.
         Closing the streams is important to ensure that any buffers or resources used by the streams are released and to avoid resource leaks.
         System.out.println(fileName + " downloaded successfully.");
      } catch (Exception e) {

   public static void main(String[] args) {
      DownloadManager[] downloads = new DownloadManager[3];
      // You can change the PDF links to any hosted PDF links you need
      downloads[0] = new DownloadManager("", "file1.pdf");
      downloads[1] = new DownloadManager("", "file2.pdf");
      for (int i = 0; i < downloads.length; i++) {
         Thread thread = new Thread(downloads[i]);

What is this random run method that isn’t called anywhere in the main method?

That’s a great question! In multithreading, the run() method is not called directly from the main() method. Instead, it is called automatically when you start a new thread using the start() method.

In the example I provided earlier, we created a Thread object for each instance of the MyThread class and called the start() method on each thread. When we call start(), it creates a new thread of execution and calls the run() method of the Runnable instance provided to the Thread constructor. In this case, that Runnable instance is an instance of the MyThread class.

Here’s the relevant code from the previous example:

Thread thread = new Thread(new MyThread());

The Thread constructor takes a Runnable instance as a parameter. We pass in a new instance of the MyThread class, which implements the Runnable interface. Then, we call start() on the Thread object, which starts the new thread and calls the run() method of the MyThread instance.

So, to summarize: the run() method is the entry point for code that will run in a new thread, but it is not called directly from the main() method. Instead, it is called automatically when a new thread is started using the start() method.