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