diff --git a/Report.md b/Report.md new file mode 100644 index 0000000..0e9423a --- /dev/null +++ b/Report.md @@ -0,0 +1,68 @@ +# Assignment 8: Multithreading Basics - Theoretical Report + +## 1. `start()` vs `run()` + +```java +Calling run() +Running in: main +Calling start() +Running in: Thread-2 +``` +Q1: What output do you get from the program? Why? + +The output shows that t1.run() executes within the main thread, while t2.start() executes in a new, separate thread named Thread-2. This happens because run() is just a regular method call, whereas start() triggers the JVM to create a new call stack and invoke the run() method in that new thread. + +Q2: What’s the difference in behavior between calling start() and run()? + +The fundamental difference lies in how the JVM handles the execution. When you call t.run(), no new thread is created; instead, the method is executed synchronously within the current thread (the caller’s thread), much like any other normal method. This is a blocking operation. + +In contrast, when you call t.start(), the JVM performs the necessary heavy lifting to create a new thread in the system. Once the new thread is allocated, the JVM invokes the run() method asynchronously in that new thread’s context. This allows the caller thread to continue its execution without waiting for the task to finish, enabling true concurrency. + +## 2. Daemon Threads + +```java +Main thread ends. +Daemon thread running... +``` +(Note: The “Daemon thread running…” messages will stop almost immediately after “Main thread ends” is printed, and might only appear once or twice before the program terminates.) + +Q1: What output do you get from the program? Why? + +A: The program prints “Main thread ends” and then perhaps one or two “Daemon thread running…” messages, then exits. This is because the thread is marked as a Daemon. In Java, the JVM exits as soon as all User Threads (non-daemon threads) finish their execution. Since the only user thread here is the main thread, once it finishes, the JVM shuts down regardless of whether the Daemon thread is still running. + +Q2: What happens if you remove thread.setDaemon(true)? + +A: If the line is removed, the thread becomes a User Thread. The JVM will not exit until the thread completes its entire loop (all 20 iterations). Consequently, you would see the message “Daemon thread running…” printed 20 times before the program finally terminates. + +Q3: What are some real-life use cases of daemon threads? + +A: Daemon threads are used for background tasks that support the main application but are not essential for the application’s survival. Examples include: + +Garbage Collection (GC): The JVM runs a daemon thread to manage memory in the background. +Background Monitoring: Services that monitor system health or resource usage. +Auto-save features: Periodically saving work in an editor without blocking the user. +Cache Eviction: Periodically cleaning up expired items from a memory cache. + +## 3. A shorter way to create threads + +```java +Thread is running using a ...! +``` + +Q1: What output do you get from the program? + +A: The output is: Thread is running using a ...!. + +Q2: What is the () -> { ... } syntax called? + +A: This is called a Lambda Expression. It was introduced in Java 8 as a concise way to represent a functional interface (in this case, the Runnable interface). + +Q3: How is this code different from creating a class that extends Thread or implements Runnable? + +A: + +Lambda expressions significantly reduce verbosity. Instead of writing a full class definition or an anonymous inner class, you can provide the logic in a single line. + +Compared to extending the Thread class, using a Lambda (which implements Runnable under the hood) is superior because it follows the principle of “composition over inheritance.” Since Java does not support multiple inheritance, implementing Runnable via a Lambda allows your class to still extend another parent class if needed. + +Compared to the traditional way of implementing the Runnable interface with a formal class, the Lambda approach is much cleaner and more modern, especially for simple, stateless tasks where creating a separate file or a bulky block of code is unnecessary. \ No newline at end of file diff --git a/src/main/java/ChunkStatus.java b/src/main/java/ChunkStatus.java index 3e902dc..678a460 100644 --- a/src/main/java/ChunkStatus.java +++ b/src/main/java/ChunkStatus.java @@ -1,34 +1,26 @@ -// Educational simplification: -// each worker writes only to its own ChunkStatus, -// and the monitor only reads chunk states. -// volatile is used here to make progress updates more visible across threads. - -/** - * Represents the state and download progress of a single file chunk. - * Each worker thread updates its own ChunkStatus, while the monitor thread reads it. - */ -public class ChunkStatus { +public class ChunkStatus +{ private final int chunkId; private final double chunkSizeMB; private volatile double downloadedMB; private volatile boolean completed; - private volatile long startTimeMs; - private volatile long endTimeMs; + private long startTime; - /** - * Initializes a chunk with its unique ID and total allocated size. - * Progress-related fields are initialized to default values. - */ public ChunkStatus(int chunkId, double chunkSizeMB) { this.chunkId = chunkId; this.chunkSizeMB = chunkSizeMB; this.downloadedMB = 0.0; this.completed = false; - this.startTimeMs = 0; - this.endTimeMs = 0; } - // Getters and Setters + public void setStartTime(long startTime) { + this.startTime = startTime; + } + + public long getStartTime() { + return startTime; + } + public int getChunkId() { return chunkId; } @@ -42,12 +34,7 @@ public class ChunkStatus { } public void setDownloadedMB(double downloadedMB) { - // Guard to prevent downloaded size exceeding actual chunk size - if (downloadedMB >= this.chunkSizeMB) { - this.downloadedMB = this.chunkSizeMB; - } else { - this.downloadedMB = downloadedMB; - } + this.downloadedMB = downloadedMB; } public boolean isCompleted() { @@ -57,52 +44,4 @@ public class ChunkStatus { public void setCompleted(boolean completed) { this.completed = completed; } - - public long getStartTimeMs() { - return startTimeMs; - } - - public void setStartTimeMs(long startTimeMs) { - this.startTimeMs = startTimeMs; - } - - public long getEndTimeMs() { - return endTimeMs; - } - - public void setEndTimeMs(long endTimeMs) { - this.endTimeMs = endTimeMs; - } - - /** - * Helper method to calculate the duration of this specific chunk's download. - * Returns 0 if the chunk hasn't started or finished yet. - */ - public long getDownloadDurationMs() { - if (startTimeMs > 0 && endTimeMs > startTimeMs) { - return endTimeMs - startTimeMs; - } else if (startTimeMs > 0 && !completed) { - return System.currentTimeMillis() - startTimeMs; - } - return 0; - } - - /** - * Helper method to calculate the download percentage of this chunk. - */ - public double getProgressPercentage() { - if (chunkSizeMB == 0) return 100.0; - return (downloadedMB / chunkSizeMB) * 100.0; - } - - @Override - public String toString() { - return String.format("Chunk #%d: %.1f/%.1f MB (%.1f%%)%s", - chunkId, - downloadedMB, - chunkSizeMB, - getProgressPercentage(), - completed ? " [Completed]" : "" - ); - } -} +} \ No newline at end of file diff --git a/src/main/java/DownloadUI.java b/src/main/java/DownloadUI.java new file mode 100644 index 0000000..7b14962 --- /dev/null +++ b/src/main/java/DownloadUI.java @@ -0,0 +1,49 @@ +public class DownloadUI +{ + private final long startTimeMs; + + public DownloadUI() {this.startTimeMs = System.currentTimeMillis();} + + public void updateProgress(double downloadedMB, double totalSizeMB, int totalChunks, int completedChunks) + { + double percent = (downloadedMB / totalSizeMB) * 100.0; + if (percent > 100.0) {percent = 100.0;} + + double elapsedSeconds = (System.currentTimeMillis() - startTimeMs) / 1000.0; + double speedMBps = elapsedSeconds > 0 ? downloadedMB / elapsedSeconds : 0.0; + + double remainingMB = Math.max(0.0, totalSizeMB - downloadedMB); + double etaSeconds = speedMBps > 0 ? remainingMB / speedMBps : 0.0; + + String bar = buildProgressBar(percent, 30); + String eta = formatTime((long) etaSeconds); + + System.out.print("\r"); + System.out.printf( + "%s %.2f%% | %.2f/%.2f MB | %d/%d chunks | %.2f MB/s | ETA %s", + bar, percent, downloadedMB, totalSizeMB, completedChunks, totalChunks, speedMBps, eta + ); + } + + public void printFinalSeparator() {System.out.println();} + + private String buildProgressBar(double percent, int width) + { + int filled = (int) ((percent / 100.0) * width); + StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < width; i++) + { + sb.append(i < filled ? "#" : "-"); + } + sb.append("]"); + return sb.toString(); + } + + private String formatTime(long totalSeconds) + { + long hours = totalSeconds / 3600; + long minutes = (totalSeconds % 3600) / 60; + long seconds = totalSeconds % 60; + return String.format("%02d:%02d:%02d", hours, minutes, seconds); + } +} \ No newline at end of file diff --git a/src/main/java/DownloadWorker.java b/src/main/java/DownloadWorker.java index 546de2d..6c3a469 100644 --- a/src/main/java/DownloadWorker.java +++ b/src/main/java/DownloadWorker.java @@ -2,13 +2,10 @@ 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.

+ * This class is now fully integrated with DownloadConfig and ChunkStatus. */ -public class DownloadWorker implements Runnable { - +public class DownloadWorker implements Runnable +{ private final ChunkStatus chunkStatus; private final DownloadConfig config; private final Random random; @@ -20,24 +17,48 @@ public class DownloadWorker implements Runnable { } @Override - public void run() { - // TODO: Record the chunk start time in chunkStatus. + public void run() + { + chunkStatus.setStartTime(System.currentTimeMillis()); + double downloaded = 0.0; + double chunkSize = chunkStatus.getChunkSizeMB(); - // TODO: Print a message that this chunk has started downloading. + String threadName = Thread.currentThread().getName(); + System.out.println("[" + threadName + "] Started downloading chunk of size: " + chunkSize + " MB"); - 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. + while (downloaded < chunkSize) + { + try + { + long minDelay = config.getMinStepDelayMs(); + long maxDelay = config.getMaxStepDelayMs(); + long delay = minDelay + (long) (random.nextDouble() * (maxDelay - minDelay)); + + Thread.sleep(delay); + + double minStep = config.getMinStepDownloadMB(); + double maxStep = config.getMaxStepDownloadMB(); + double stepSize = minStep + (random.nextDouble() * (maxStep - minStep)); + + downloaded += stepSize; + + if (downloaded > chunkSize) {downloaded = chunkSize;} + + chunkStatus.setDownloadedMB(downloaded); + + System.out.printf("[%s] Progress: %.2f / %.2f MB\n", threadName, downloaded, chunkSize); + + } + catch (InterruptedException e) + { + System.err.println("[" + threadName + "] Interrupted!"); + Thread.currentThread().interrupt(); + break; + } } - // TODO: Mark the chunk as completed. - // TODO: Record the chunk end time in chunkStatus. - // TODO: Print a message that this chunk has finished downloading. + chunkStatus.setCompleted(true); + System.out.println("[" + threadName + "] Finished downloading chunk."); } - -} +} \ No newline at end of file diff --git a/src/main/java/Main.java b/src/main/java/Main.java index 82bd239..b475aea 100644 --- a/src/main/java/Main.java +++ b/src/main/java/Main.java @@ -1,107 +1,65 @@ import java.util.ArrayList; import java.util.List; -public class Main { - public static void main(String[] args) { +public class Main +{ + public static void main(String[] args) + { System.out.println("=== Simulated Download Manager ==="); - // 1. Read config DownloadConfig config; - try { + try + { config = ConfigReader.readConfig("download_config.txt"); - } catch (Exception e) { - System.out.println("Failed to read configuration: " + e.getMessage()); + } + catch (Exception e) + { + System.err.println("Failed to read configuration: " + e.getMessage()); 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("Total size (MB) : " + config.getTotalSizeMB()); + System.out.println("Chunk count : " + config.getChunkCount()); System.out.println(); - // 2. Create chunks List chunks = ChunkUtils.createChunks( config.getTotalSizeMB(), config.getChunkCount() ); - // 3. Create worker threads List workerThreads = new ArrayList<>(); - for (ChunkStatus chunk : chunks) { + for (ChunkStatus chunk : chunks) + { DownloadWorker worker = new DownloadWorker(chunk, config); 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.println("[Setup] Chunk " + chunk.getChunkId() + + " (" + chunk.getChunkSizeMB() + " MB) assigned to " + workerThread.getName()); } - // 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(); + 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("\n[System] Starting all download workers...\n"); + for (Thread t : workerThreads) {t.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(); - // } - - // 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. - - // NOTE: - // this final report may show 0 progress because no worker has actually run yet. - // Until students complete the thread start/join TODOs above, - - // 7. Print final report - System.out.println(); - System.out.println("=== Final Report ==="); - - 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" - ); + try + { + for (Thread t : workerThreads) {t.join();} + monitorThread.join(); + } + catch (InterruptedException e) + { + System.err.println("Main thread interrupted."); + Thread.currentThread().interrupt(); } - System.out.println(); - System.out.println("Completed chunks: " + completedChunks + "/" + chunks.size()); - System.out.println("Downloaded total: " + downloadedMB + "/" + config.getTotalSizeMB() + " MB"); - System.out.println("Simulation finished."); + System.out.println("\n=== Main Simulation Finished ==="); + PerformanceBenchmarker.runComparison(config); } -} +} \ No newline at end of file diff --git a/src/main/java/PerformanceBenchmarker.java b/src/main/java/PerformanceBenchmarker.java new file mode 100644 index 0000000..b32d771 --- /dev/null +++ b/src/main/java/PerformanceBenchmarker.java @@ -0,0 +1,81 @@ +import java.util.ArrayList; +import java.util.List; + +public class PerformanceBenchmarker +{ + public static void runComparison(DownloadConfig config) + { + System.out.println("\n=== Performance Benchmark ==="); + + long sequentialTime = runSequential(config); + long multithreadTime = runMultithreaded(config); + + System.out.println("\n--- Results ---"); + System.out.println("Sequential time : " + sequentialTime + " ms"); + System.out.println("Multithreaded time: " + multithreadTime + " ms"); + + if (multithreadTime > 0) + { + double speedup = (double) sequentialTime / multithreadTime; + System.out.printf("Speedup ratio : %.2fx%n", speedup); + } + } + + private static long runSequential(DownloadConfig config) + { + System.out.println("\n[Benchmark] Running sequential simulation..."); + + List chunks = ChunkUtils.createChunks( + config.getTotalSizeMB(), + config.getChunkCount() + ); + + long start = System.currentTimeMillis(); + + for (ChunkStatus chunk : chunks) + { + DownloadWorker worker = new DownloadWorker(chunk, config); + worker.run(); + } + + long end = System.currentTimeMillis(); + long duration = end - start; + + System.out.println("[Benchmark] Sequential finished in " + duration + " ms"); + return duration; + } + + private static long runMultithreaded(DownloadConfig config) + { + System.out.println("\n[Benchmark] Running multithreaded simulation..."); + + List chunks = ChunkUtils.createChunks( + config.getTotalSizeMB(), + config.getChunkCount() + ); + + List threads = new ArrayList<>(); + + long start = System.currentTimeMillis(); + + for (ChunkStatus chunk : chunks) + { + DownloadWorker worker = new DownloadWorker(chunk, config); + Thread t = new Thread(worker); + threads.add(t); + t.start(); + } + + for (Thread t : threads) + { + try {t.join();} + catch (InterruptedException e) {Thread.currentThread().interrupt();} + } + + long end = System.currentTimeMillis(); + long duration = end - start; + + System.out.println("[Benchmark] Multithreaded finished in " + duration + " ms"); + return duration; + } +} \ No newline at end of file diff --git a/src/main/java/ProgressMonitor.java b/src/main/java/ProgressMonitor.java index 475c0e0..9c4c27a 100644 --- a/src/main/java/ProgressMonitor.java +++ b/src/main/java/ProgressMonitor.java @@ -1,69 +1,54 @@ import java.util.List; -public class ProgressMonitor implements Runnable { - +public class ProgressMonitor implements Runnable +{ private final String fileName; private final int totalSizeMB; private final List chunks; private final long monitorDelayMs; + private final DownloadUI ui; - public ProgressMonitor(DownloadConfig config, List chunks) { + public ProgressMonitor(DownloadConfig config, List chunks) + { this.fileName = config.getFileName(); this.totalSizeMB = config.getTotalSizeMB(); this.chunks = chunks; this.monitorDelayMs = 500; + this.ui = new DownloadUI(); } @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 + public void run() + { + System.out.println("[Monitor] Tracking download: " + fileName); - - while (true) { + while (true) + { double totalDownloadedMB = 0.0; int completedChunks = 0; - for (ChunkStatus chunk : chunks) { + for (ChunkStatus chunk : chunks) + { totalDownloadedMB += chunk.getDownloadedMB(); - - if (chunk.isCompleted()) { - completedChunks++; - } + if (chunk.isCompleted()) {completedChunks++;} } - double percent = 0.0; - if (totalSizeMB > 0) { - percent = (totalDownloadedMB * 100.0) / totalSizeMB; + ui.updateProgress(totalDownloadedMB, (double) totalSizeMB, chunks.size(), completedChunks); + + if (completedChunks == chunks.size()) + { + ui.printFinalSeparator(); + System.out.println("[Monitor] Download completed."); + break; } - System.out.printf( - "Progress for %s: %.1f/%.1f MB (%.2f%%), completed chunks: %d/%d%n", - fileName, - totalDownloadedMB, - (double) totalSizeMB, - percent, - completedChunks, - chunks.size() - ); - - - // TODO: - // If all chunks are completed, print a final message and exit the loop - - try { - Thread.sleep(monitorDelayMs); - } catch (InterruptedException e) { - System.out.println("Progress monitor interrupted."); - return; + try {Thread.sleep(monitorDelayMs);} + catch (InterruptedException e) + { + System.out.println("\n[Monitor] Interrupted."); + Thread.currentThread().interrupt(); + break; } } } -} +} \ No newline at end of file