From 8b92095e752644fd1985c35af5c4cfa3e6799216 Mon Sep 17 00:00:00 2001 From: Hesam Ghazi Date: Mon, 29 Jun 2026 20:38:19 +0330 Subject: [PATCH] Implementation of HW-8 Multithreading is complete --- .idea/.name | 1 + Report.md | 229 ++++++++++++++++++++++++++ src/main/java/ConsoleUI.java | 158 ++++++++++++++++++ src/main/java/DownloadStatistics.java | 89 ++++++++++ src/main/java/DownloadWorker.java | 89 ++++++++-- src/main/java/Main.java | 151 ++++++++++++----- src/main/java/ProgressMonitor.java | 76 ++++++--- 7 files changed, 709 insertions(+), 84 deletions(-) create mode 100644 .idea/.name create mode 100644 Report.md create mode 100644 src/main/java/ConsoleUI.java create mode 100644 src/main/java/DownloadStatistics.java diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..002da1d --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Main.java \ No newline at end of file diff --git a/Report.md b/Report.md new file mode 100644 index 0000000..c21e8bb --- /dev/null +++ b/Report.md @@ -0,0 +1,229 @@ +# Advanced Programming Assignment 8 + + +* Name: *Hesam Ghazi* +* Student Number: *403222015* +* Course: *Advanced Programming* + +--- + +# Theoretical Questions + +## 1. `start()` vs `run()` + +### Question 1: What output do you get from the program? Why? + +The program prints something similar to the following: + +``` +Calling run() +Running in: main +Calling start() +Running in: Thread-2 +``` + +When `t1.run()` is called, the `run()` method executes like a normal Java method. It does **not** create a new thread, so it runs inside the current thread, which is the **main** thread. + +When `t2.start()` is called, the Java Virtual Machine creates a **new thread**, and that thread executes the `run()` method independently. Therefore, `Thread.currentThread().getName()` returns `"Thread-2"`. + +--- + +### Question 2: What's the difference in behavior between calling `start()` and `run()`? + +| `start()` | `run()` | +| -------------------------------------------------- | ------------------------------------------- | +| Creates a new thread. | Does not create a new thread. | +| Executes `run()` concurrently. | Executes `run()` like a normal method call. | +| The JVM schedules the new thread. | Runs immediately in the current thread. | +| Allows multiple threads to execute simultaneously. | No parallelism or concurrency occurs. | + +For example: + +```java +Thread thread = new Thread(new MyRunnable()); + +thread.run(); // Executes on the current thread (main) +thread.start(); // Executes on a new thread +``` + +Using `start()` is essential when we want to perform tasks concurrently. + +--- + +# 2. Daemon Threads + +### Question 1: What output do you get from the program? Why? + +A typical output is: + +``` +Main thread ends. +Daemon thread running... +Daemon thread running... +``` + +Sometimes only the following is printed: + +``` +Main thread ends. +``` + +This happens because daemon threads are **background threads**. When the main (user) thread finishes and no other user threads remain, the JVM automatically terminates all daemon threads, even if they have not completed their work. + +Since the daemon thread sleeps for 500 ms in every iteration, it may be terminated before printing all twenty messages. + +--- + +### Question 2: What happens if you remove `thread.setDaemon(true)`? + +If `thread.setDaemon(true)` is removed, the thread becomes a **user thread**. + +In this case, the JVM waits until the thread completes all twenty iterations before terminating the program. + +The output becomes similar to: + +``` +Main thread ends. +Daemon thread running... +Daemon thread running... +... +Daemon thread running... +``` + +The program will continue running for approximately **10 seconds** (20 × 500 ms). + +--- + +### Question 3: What are some real-life use cases of daemon threads? + +Daemon threads are commonly used for background services that support the application but are not essential to its primary functionality. + +Examples include: + +* Garbage collection +* Automatic cache cleanup +* Logging services +* Monitoring system resources +* Background file synchronization +* Session timeout checking +* Periodic health checks +* Scheduled maintenance tasks + +These tasks should stop automatically when the application exits, making daemon threads an appropriate choice. + +--- + +# 3. A Shorter Way to Create Threads + +### Question 1: What output do you get from the program? + +The program prints: + +``` +Thread is running using a ...! +``` + +This message is printed by the newly created thread after `thread.start()` is called. + +--- + +### Question 2: What is the `() -> { ... }` syntax called? + +The syntax + +```java +() -> { + ... +} +``` + +is called a **Lambda Expression**. + +Lambda expressions were introduced in **Java 8** as a concise way to implement functional interfaces such as `Runnable`. + +The previous code + +```java +Thread thread = new Thread(() -> { + System.out.println("Thread is running using a ...!"); +}); +``` + +is equivalent to + +```java +Thread thread = new Thread(new Runnable() { + @Override + public void run() { + System.out.println("Thread is running using a ...!"); + } +}); +``` + +--- + +### Question 3: How is this code different from creating a class that extends `Thread` or implements `Runnable`? + +There are three common approaches for creating a thread. + +### 1. Extending `Thread` + +```java +class MyThread extends Thread { + @Override + public void run() { + System.out.println("Running..."); + } +} +``` + +**Advantages** + +* Easy for simple examples. + +**Disadvantages** + +* Java supports only single inheritance. +* The task is tightly coupled with the thread. + +--- + +### 2. Implementing `Runnable` + +```java +class MyRunnable implements Runnable { + @Override + public void run() { + System.out.println("Running..."); + } +} +``` + +**Advantages** + +* Better object-oriented design. +* Allows inheritance from another class. +* The task and the thread are separated. + +--- + +### 3. Using a Lambda Expression + +```java +Thread thread = new Thread(() -> { + System.out.println("Running..."); +}); +``` + +**Advantages** + +* Very concise and readable. +* Eliminates unnecessary boilerplate code. +* Ideal for short tasks. +* Still implements the `Runnable` interface internally. + +--- + +## Conclusion + +Lambda expressions are the preferred choice for short and simple thread tasks because they produce cleaner and more maintainable code. For larger or reusable tasks, implementing `Runnable` is generally considered the best practice due to better separation of responsibilities. Extending `Thread` is the least flexible approach and is usually reserved for special cases. diff --git a/src/main/java/ConsoleUI.java b/src/main/java/ConsoleUI.java new file mode 100644 index 0000000..9ff600d --- /dev/null +++ b/src/main/java/ConsoleUI.java @@ -0,0 +1,158 @@ +import java.util.List; + +public final class ConsoleUI { + + private static final int BAR_WIDTH = 40; + + private ConsoleUI() { + } + + /** + * Clears the console. + */ + public static void clearScreen() { + System.out.print("\033[H\033[2J"); + System.out.flush(); + } + + /** + * Prints the application header. + */ + public static void printHeader(String fileName, int totalSizeMB, int chunkCount) { + + System.out.println("============================================================"); + System.out.println(" SIMULATED DOWNLOAD MANAGER"); + System.out.println("============================================================"); + System.out.printf("File : %s%n", fileName); + System.out.printf("Size : %d MB%n", totalSizeMB); + System.out.printf("Chunks : %d%n", chunkCount); + System.out.println("============================================================"); + System.out.println(); + } + + /** + * Prints overall download statistics. + */ + public static void printOverallProgress( + double downloaded, + double total, + double speed, + double etaSeconds) { + + double percent = total == 0 + ? 100 + : downloaded * 100.0 / total; + + System.out.println("Overall Progress"); + System.out.println(buildProgressBar(percent)); + + System.out.printf("Downloaded : %.1f / %.1f MB%n", downloaded, total); + System.out.printf("Progress : %.1f%%%n", percent); + System.out.printf("Speed : %.2f MB/s%n", speed); + System.out.printf("ETA : %s%n", formatTime(etaSeconds)); + + System.out.println(); + } + + /** + * Prints each chunk's progress. + */ + public static void printChunks(List chunks) { + + System.out.println("Chunks"); + + for (ChunkStatus chunk : chunks) { + + double percent = chunk.getProgressPercentage(); + + System.out.printf( + "Chunk %-2d %s %6.1f%% %s%n", + chunk.getChunkId(), + buildProgressBar(percent), + percent, + chunk.isCompleted() ? "✓" : "" + ); + } + + System.out.println(); + } + + /** + * Prints the final report. + */ + public static void printFinalReport(List chunks) { + + System.out.println(); + System.out.println("============================================================"); + System.out.println(" FINAL REPORT"); + System.out.println("============================================================"); + + for (ChunkStatus chunk : chunks) { + + System.out.printf( + "Chunk %d : %.1f / %.1f MB | %.2f sec | %s%n", + chunk.getChunkId(), + chunk.getDownloadedMB(), + chunk.getChunkSizeMB(), + chunk.getDownloadDurationMs() / 1000.0, + chunk.isCompleted() ? "Completed" : "Incomplete" + ); + } + + System.out.println("============================================================"); + } + + /** + * Builds a text progress bar. + */ + public static String buildProgressBar(double percent) { + + percent = Math.max(0, Math.min(100, percent)); + + int filled = (int) Math.round((percent / 100.0) * BAR_WIDTH); + + StringBuilder sb = new StringBuilder(); + + sb.append("["); + + for (int i = 0; i < BAR_WIDTH; i++) { + + if (i < filled) { + sb.append("█"); + } else { + sb.append("░"); + } + + } + + sb.append("]"); + + return sb.toString(); + } + + /** + * Formats seconds as HH:MM:SS. + */ + public static String formatTime(double seconds) { + + if (seconds <= 0 || Double.isInfinite(seconds) || Double.isNaN(seconds)) { + return "--:--:--"; + } + + int total = (int) Math.round(seconds); + + int hours = total / 3600; + int minutes = (total % 3600) / 60; + int secs = total % 60; + + return String.format("%02d:%02d:%02d", hours, minutes, secs); + } + + /** + * Thread-safe logging method. + */ + public static synchronized void log(String message) { + System.out.println(message); + } + +} \ No newline at end of file diff --git a/src/main/java/DownloadStatistics.java b/src/main/java/DownloadStatistics.java new file mode 100644 index 0000000..a9d4a46 --- /dev/null +++ b/src/main/java/DownloadStatistics.java @@ -0,0 +1,89 @@ +/** + * Utility class for calculating download statistics such as + * percentage, speed, and estimated remaining time (ETA). + */ +public final class DownloadStatistics { + + private DownloadStatistics() { + // Utility class + } + + /** + * Calculates the overall download percentage. + * + * @param downloadedMB Downloaded amount in MB + * @param totalMB Total file size in MB + * @return Percentage between 0 and 100 + */ + public static double calculatePercentage(double downloadedMB, double totalMB) { + + if (totalMB <= 0) { + return 100.0; + } + + return (downloadedMB / totalMB) * 100.0; + } + + /** + * Calculates the average download speed. + * + * @param downloadedMB Downloaded amount in MB + * @param elapsedMillis Elapsed time in milliseconds + * @return Speed in MB/s + */ + public static double calculateSpeed(double downloadedMB, long elapsedMillis) { + + if (elapsedMillis <= 0) { + return 0.0; + } + + return downloadedMB / (elapsedMillis / 1000.0); + } + + /** + * Calculates the estimated remaining time. + * + * @param downloadedMB Downloaded amount + * @param totalMB Total size + * @param speedMBps Current speed + * @return Remaining time in seconds + */ + public static double calculateETA( + double downloadedMB, + double totalMB, + double speedMBps) { + + if (speedMBps <= 0) { + return Double.POSITIVE_INFINITY; + } + + double remaining = totalMB - downloadedMB; + + if (remaining <= 0) { + return 0; + } + + return remaining / speedMBps; + } + + /** + * Formats a duration into HH:MM:SS. + * + * @param seconds Time in seconds + * @return Formatted time string + */ + public static String formatTime(double seconds) { + + if (Double.isInfinite(seconds) || Double.isNaN(seconds)) { + return "--:--:--"; + } + + int totalSeconds = (int) Math.round(seconds); + + int hours = totalSeconds / 3600; + int minutes = (totalSeconds % 3600) / 60; + int secs = totalSeconds % 60; + + return String.format("%02d:%02d:%02d", hours, minutes, secs); + } +} \ No newline at end of file diff --git a/src/main/java/DownloadWorker.java b/src/main/java/DownloadWorker.java index 546de2d..d0f172a 100644 --- a/src/main/java/DownloadWorker.java +++ b/src/main/java/DownloadWorker.java @@ -3,9 +3,9 @@ import java.util.Random; /** * Simulates downloading a single chunk of a file. * - *

This class is intentionally provided as a skeleton for students. - * The main multithreading and simulation logic should be completed - * in the run() method.

+ * Each DownloadWorker is responsible for exactly one ChunkStatus object. + * Since every worker only modifies its own ChunkStatus, no explicit + * synchronization is required. */ public class DownloadWorker implements Runnable { @@ -21,23 +21,80 @@ public class DownloadWorker implements Runnable { @Override public void run() { - // TODO: Record the chunk start time in chunkStatus. + + // Record start time + chunkStatus.setStartTimeMs(System.currentTimeMillis()); + double downloaded = 0.0; - // TODO: Print a message that this chunk has started downloading. + System.out.printf( + "[%s] Chunk %d started (%.1f MB)%n", + Thread.currentThread().getName(), + chunkStatus.getChunkId(), + chunkStatus.getChunkSizeMB() + ); while (downloaded < chunkStatus.getChunkSizeMB()) { - // TODO: Generate a random sleep delay between min and max delay. - // TODO: Sleep for that delay. - // TODO: Generate a random download amount for this step. - // TODO: Increase downloaded, but do not go beyond chunk size. - // TODO: Save the updated downloaded value into chunkStatus. - // TODO: Optionally print step-by-step progress. + + try { + // Random delay + int delay = random.nextInt( + config.getMaxStepDelayMs() - config.getMinStepDelayMs() + 1 + ) + config.getMinStepDelayMs(); + + Thread.sleep(delay); + + } catch (InterruptedException e) { + + System.out.printf( + "[%s] Chunk %d interrupted.%n", + Thread.currentThread().getName(), + chunkStatus.getChunkId() + ); + + Thread.currentThread().interrupt(); + return; + } + + // Random download amount + double step = + config.getMinStepDownloadMB() + + random.nextDouble() + * (config.getMaxStepDownloadMB() + - config.getMinStepDownloadMB()); + + downloaded += step; + + // Prevent downloading beyond chunk size + if (downloaded > chunkStatus.getChunkSizeMB()) { + downloaded = chunkStatus.getChunkSizeMB(); + } + + // Save progress + chunkStatus.setDownloadedMB(downloaded); + + // Optional step-by-step debug output + System.out.printf( + "[%s] Chunk %d: %.1f/%.1f MB (%.1f%%)%n", + Thread.currentThread().getName(), + chunkStatus.getChunkId(), + chunkStatus.getDownloadedMB(), + chunkStatus.getChunkSizeMB(), + chunkStatus.getProgressPercentage() + ); } - // TODO: Mark the chunk as completed. - // TODO: Record the chunk end time in chunkStatus. - // TODO: Print a message that this chunk has finished downloading. - } + // Mark completed + chunkStatus.setCompleted(true); -} + // Record finish time + chunkStatus.setEndTimeMs(System.currentTimeMillis()); + + System.out.printf( + "[%s] Chunk %d completed in %.2f seconds.%n", + Thread.currentThread().getName(), + chunkStatus.getChunkId(), + chunkStatus.getDownloadDurationMs() / 1000.0 + ); + } +} \ No newline at end of file diff --git a/src/main/java/Main.java b/src/main/java/Main.java index 82bd239..84ee209 100644 --- a/src/main/java/Main.java +++ b/src/main/java/Main.java @@ -2,11 +2,16 @@ import java.util.ArrayList; import java.util.List; public class Main { + public static void main(String[] args) { - System.out.println("=== Simulated Download Manager ==="); + + System.out.println("================================================="); + System.out.println(" SIMULATED DOWNLOAD MANAGER"); + System.out.println("================================================="); // 1. Read config DownloadConfig config; + try { config = ConfigReader.readConfig("download_config.txt"); } catch (Exception e) { @@ -14,94 +19,150 @@ public class Main { return; } - System.out.println("File name: " + config.getFileName()); - System.out.println("Total size (MB): " + config.getTotalSizeMB()); - System.out.println("Chunk count: " + config.getChunkCount()); + System.out.println("File Name : " + config.getFileName()); + System.out.println("File Size : " + config.getTotalSizeMB() + " MB"); + System.out.println("Chunk Count: " + config.getChunkCount()); + System.out.println("Delay Range: " + + config.getMinStepDelayMs() + + " - " + + config.getMaxStepDelayMs() + + " ms"); + System.out.println("Step Range : " + + config.getMinStepDownloadMB() + + " - " + + config.getMaxStepDownloadMB() + + " MB"); System.out.println(); + long simulationStart = System.currentTimeMillis(); + // 2. Create chunks List chunks = ChunkUtils.createChunks( config.getTotalSizeMB(), config.getChunkCount() ); + System.out.println("Created Chunks:"); + + for (ChunkStatus chunk : chunks) { + System.out.printf( + " Chunk %d -> %.1f MB%n", + chunk.getChunkId(), + chunk.getChunkSizeMB() + ); + } + + System.out.println(); + // 3. Create worker threads List workerThreads = new ArrayList<>(); for (ChunkStatus chunk : chunks) { + DownloadWorker worker = new DownloadWorker(chunk, config); - Thread workerThread = new Thread(worker, "Worker-" + chunk.getChunkId()); + + Thread workerThread = new Thread( + worker, + "Worker-" + chunk.getChunkId() + ); workerThreads.add(workerThread); - // TODO: - // Students may print helpful debug information here, - // for example which chunk is assigned to which worker thread. + System.out.printf( + "Assigned Chunk %d to %s%n", + chunk.getChunkId(), + workerThread.getName() + ); } + System.out.println(); + // 4. Create and start monitor thread ProgressMonitor monitor = new ProgressMonitor(config, chunks); - Thread monitorThread = new Thread(monitor, "Progress-Monitor"); - // TODO: - // Start the monitor thread before starting the workers - // so that progress can be displayed while downloading happens. - // - // Example idea: - // monitorThread.start(); + Thread monitorThread = new Thread( + monitor, + "Progress-Monitor" + ); + + System.out.println("Starting monitor thread..."); + monitorThread.start(); // 5. Start worker threads - // TODO: - // Start each worker thread in workerThreads. - // Use a loop and call start() on each thread. + System.out.println("Starting worker threads..."); + + for (Thread workerThread : workerThreads) { + workerThread.start(); + } // 6. Wait for workers to finish - // TODO: - // Wait for all worker threads to complete by calling join(). - // This should be done inside a try-catch block for InterruptedException. - // - // Hint: - // for (Thread thread : workerThreads) { - // thread.join(); - // } + try { - // TODO: - // After all workers finish, the monitor thread may also need to stop. - // Depending on how ProgressMonitor is implemented, students may: - // - wait for it to finish on its own, or - // - add a stopping mechanism in ProgressMonitor later. - // - // If your monitor finishes automatically, you may join it here. + for (Thread workerThread : workerThreads) { + workerThread.join(); + } - // NOTE: - // this final report may show 0 progress because no worker has actually run yet. - // Until students complete the thread start/join TODOs above, + // Wait for monitor to finish automatically + monitorThread.join(); + + } catch (InterruptedException e) { + + System.out.println("Main thread interrupted."); + + Thread.currentThread().interrupt(); + return; + } + + long simulationEnd = System.currentTimeMillis(); // 7. Print final report System.out.println(); - System.out.println("=== Final Report ==="); + System.out.println("================================================="); + System.out.println(" FINAL REPORT"); + System.out.println("================================================="); int completedChunks = 0; double downloadedMB = 0.0; for (ChunkStatus chunk : chunks) { + downloadedMB += chunk.getDownloadedMB(); if (chunk.isCompleted()) { completedChunks++; } - System.out.println( - "Chunk " + chunk.getChunkId() - + ": " + chunk.getDownloadedMB() - + "/" + chunk.getChunkSizeMB() - + " MB" + System.out.printf( + "Chunk %-2d | Downloaded: %6.1f / %6.1f MB | %-10s | Duration: %.2f sec%n", + chunk.getChunkId(), + chunk.getDownloadedMB(), + chunk.getChunkSizeMB(), + chunk.isCompleted() ? "Completed" : "Incomplete", + chunk.getDownloadDurationMs() / 1000.0 ); } System.out.println(); - System.out.println("Completed chunks: " + completedChunks + "/" + chunks.size()); - System.out.println("Downloaded total: " + downloadedMB + "/" + config.getTotalSizeMB() + " MB"); + + System.out.println( + "Completed Chunks: " + + completedChunks + + "/" + + chunks.size() + ); + + System.out.printf( + "Downloaded Total: %.1f / %d MB%n", + downloadedMB, + config.getTotalSizeMB() + ); + + System.out.printf( + "Execution Time : %.2f seconds%n", + (simulationEnd - simulationStart) / 1000.0 + ); + + System.out.println(); System.out.println("Simulation finished."); } -} +} \ No newline at end of file diff --git a/src/main/java/ProgressMonitor.java b/src/main/java/ProgressMonitor.java index 475c0e0..2fff93c 100644 --- a/src/main/java/ProgressMonitor.java +++ b/src/main/java/ProgressMonitor.java @@ -16,18 +16,11 @@ public class ProgressMonitor implements Runnable { @Override public void run() { - // TODO: - // Repeatedly check chunk progress until all chunks are completed. - // In each loop: - // 1. Read the downloaded size from every chunk - // 2. Add all downloaded amounts to totalDownloadedMB - // 3. Count completed chunks - // 4. Print a progress message - // 5. If completedChunks == chunks.size(), print a final monitor message and stop - // 6. Otherwise sleep for monitorDelayMs and continue + long startTime = System.currentTimeMillis(); while (true) { + double totalDownloadedMB = 0.0; int completedChunks = 0; @@ -39,31 +32,68 @@ public class ProgressMonitor implements Runnable { } } - double percent = 0.0; - if (totalSizeMB > 0) { - percent = (totalDownloadedMB * 100.0) / totalSizeMB; - } - System.out.printf( - "Progress for %s: %.1f/%.1f MB (%.2f%%), completed chunks: %d/%d%n", - fileName, + + long elapsedMillis = System.currentTimeMillis() - startTime; + double elapsedSeconds = elapsedMillis / 1000.0; + + + + double remainingMB = totalSizeMB - totalDownloadedMB; + + double percent = DownloadStatistics.calculatePercentage( totalDownloadedMB, - (double) totalSizeMB, - percent, - completedChunks, + totalSizeMB + ); + + double speed = DownloadStatistics.calculateSpeed( + totalDownloadedMB, + elapsedMillis + ); + + double eta = DownloadStatistics.calculateETA( + totalDownloadedMB, + totalSizeMB, + speed + ); + + // Refresh console + ConsoleUI.clearScreen(); + + ConsoleUI.printHeader( + fileName, + totalSizeMB, chunks.size() ); + ConsoleUI.printOverallProgress( + totalDownloadedMB, + totalSizeMB, + speed, + eta + ); - // TODO: - // If all chunks are completed, print a final message and exit the loop + ConsoleUI.printChunks(chunks); + + // Finished? + if (completedChunks == chunks.size()) { + + System.out.println("Download completed successfully!"); + System.out.printf( + "Total download time: %.2f seconds%n", + elapsedSeconds + ); + + ConsoleUI.printFinalReport(chunks); + return; + } try { Thread.sleep(monitorDelayMs); } catch (InterruptedException e) { - System.out.println("Progress monitor interrupted."); + Thread.currentThread().interrupt(); return; } } } -} +} \ No newline at end of file -- 2.54.0