From 55b3f70d7cfe3164b42e40e9e044ba9301551128 Mon Sep 17 00:00:00 2001 From: Saba_frm Date: Mon, 15 Jun 2026 20:44:47 +0330 Subject: [PATCH] develop --- .idea/misc.xml | 4 +- Report.md | 65 ++++++++++ src/main/java/ChunkStatus.java | 53 +++++--- src/main/java/DownloadWorker.java | 70 ++++++++++- src/main/java/Main.java | 188 ++++++++++++++++++++++------- src/main/java/ProgressMonitor.java | 109 +++++++++++++---- 6 files changed, 392 insertions(+), 97 deletions(-) create mode 100644 Report.md diff --git a/.idea/misc.xml b/.idea/misc.xml index fdc35ea..f6a588b 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -8,7 +8,5 @@ - - - + \ No newline at end of file diff --git a/Report.md b/Report.md new file mode 100644 index 0000000..c165767 --- /dev/null +++ b/Report.md @@ -0,0 +1,65 @@ +# Assignment Report: Multithreading Basics +### Course: Advanced Programming + +## Assignment: Eighth Assignment – Multithreading Basics + +Project: Simulated Download Manager + +1. Theoretical Questions + 1.1 Difference Between start() and run() + In Java, the run() method contains the code that a thread should execute. However, calling run() directly does not create a new thread; it works like a normal method call in the current thread. + +When start() is called, the JVM creates a new thread, which then executes the run() method internally. + +1.2 Daemon Threads +A daemon thread is a background thread. The JVM does not wait for daemon threads to finish. When all user threads finish, the JVM stops the program even if daemon threads are still running. + +If setDaemon(true) is removed, the thread becomes a normal user thread, and the JVM will wait for it to complete. + +1.3 Lambda Expressions for Threads +A lambda expression is a concise way to implement a functional interface like Runnable. Since Runnable has only one method (run()), we can use: + +() -> { ... } instead of creating a whole new class. + +2. Practical Implementation + 2.1 Project Overview + This project simulates a download manager where a file is divided into chunks, and each chunk is downloaded concurrently by separate worker threads. + +2.2 DownloadWorker +The DownloadWorker class implements Runnable. It simulates the download of a single chunk by: + +Using random step sizes for download progress. +Sleeping for random delays to simulate network latency. +Updating its chunk status until the download is complete. +2.3 ChunkStatus +This class stores the state of each chunk, including its ID, size, and downloaded amount. Variables are marked as volatile to ensure visibility across different threads. + +2.4 ProgressMonitor +The ProgressMonitor runs as a background thread to periodically check and print: + +Individual chunk progress. +Total download percentage. +A visual Progress Bar. +Average download speed and ETA. +2.5 Main Class +The Main class coordinates the process: + +Reads configuration. +Initializes chunks and worker threads. +Starts the monitor and workers. +Uses join() to wait for all threads to finish before printing the final report. +3. Bonus Features + 3.1 Progress Bar & ETA + A visual progress bar was added to the console output. The program also calculates the current speed in MB/s and estimates the remaining time (ETA) based on that speed. + +3.2 Sequential vs Multithreaded Comparison +The program compares running the workers sequentially (using .run()) versus concurrently (using .start()). It calculates the Speedup to show how much faster multithreading is for this task. + +4. Execution Instructions + To compile and run the project, use the following commands: + +bash +mvn compile +mvn exec:java -Dexec.mainClass="Main" +5. Conclusion + This project successfully demonstrates the power of multithreading in Java. By using separate threads for different chunks, the total download time is significantly reduced compared to a sequential approach. \ No newline at end of file diff --git a/src/main/java/ChunkStatus.java b/src/main/java/ChunkStatus.java index 3e902dc..c305577 100644 --- a/src/main/java/ChunkStatus.java +++ b/src/main/java/ChunkStatus.java @@ -7,7 +7,8 @@ * 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; @@ -19,7 +20,8 @@ public class ChunkStatus { * 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) { + public ChunkStatus(int chunkId, double chunkSizeMB) + { this.chunkId = chunkId; this.chunkSizeMB = chunkSizeMB; this.downloadedMB = 0.0; @@ -28,49 +30,58 @@ public class ChunkStatus { this.endTimeMs = 0; } - // Getters and Setters - public int getChunkId() { + public int getChunkId() + { return chunkId; } - public double getChunkSizeMB() { + public double getChunkSizeMB() + { return chunkSizeMB; } - public double getDownloadedMB() { + public double getDownloadedMB() + { return downloadedMB; } - public void setDownloadedMB(double downloadedMB) { - // Guard to prevent downloaded size exceeding actual chunk size - if (downloadedMB >= this.chunkSizeMB) { + public void setDownloadedMB(double downloadedMB) + { + if (downloadedMB >= this.chunkSizeMB) + { this.downloadedMB = this.chunkSizeMB; } else { this.downloadedMB = downloadedMB; } } - public boolean isCompleted() { + public boolean isCompleted() + { return completed; } - public void setCompleted(boolean completed) { + public void setCompleted(boolean completed) + { this.completed = completed; } - public long getStartTimeMs() { + public long getStartTimeMs() + { return startTimeMs; } - public void setStartTimeMs(long startTimeMs) { + public void setStartTimeMs(long startTimeMs) + { this.startTimeMs = startTimeMs; } - public long getEndTimeMs() { + public long getEndTimeMs() + { return endTimeMs; } - public void setEndTimeMs(long endTimeMs) { + public void setEndTimeMs(long endTimeMs) + { this.endTimeMs = endTimeMs; } @@ -78,8 +89,10 @@ public class ChunkStatus { * 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) { + public long getDownloadDurationMs() + { + if (startTimeMs > 0 && endTimeMs > startTimeMs) + { return endTimeMs - startTimeMs; } else if (startTimeMs > 0 && !completed) { return System.currentTimeMillis() - startTimeMs; @@ -90,13 +103,15 @@ public class ChunkStatus { /** * Helper method to calculate the download percentage of this chunk. */ - public double getProgressPercentage() { + public double getProgressPercentage() + { if (chunkSizeMB == 0) return 100.0; return (downloadedMB / chunkSizeMB) * 100.0; } @Override - public String toString() { + public String toString() + { return String.format("Chunk #%d: %.1f/%.1f MB (%.1f%%)%s", chunkId, downloadedMB, diff --git a/src/main/java/DownloadWorker.java b/src/main/java/DownloadWorker.java index 546de2d..d224f47 100644 --- a/src/main/java/DownloadWorker.java +++ b/src/main/java/DownloadWorker.java @@ -7,37 +7,99 @@ import java.util.Random; * The main multithreading and simulation logic should be completed * in the run() method.

*/ -public class DownloadWorker implements Runnable { +public class DownloadWorker implements Runnable +{ private final ChunkStatus chunkStatus; private final DownloadConfig config; private final Random random; - public DownloadWorker(ChunkStatus chunkStatus, DownloadConfig config) { + public DownloadWorker(ChunkStatus chunkStatus, DownloadConfig config) + { this.chunkStatus = chunkStatus; this.config = config; this.random = new Random(); } @Override - public void run() { + public void run() + { + + long startTime = System.currentTimeMillis(); // TODO: Record the chunk start time in chunkStatus. + chunkStatus.setStartTimeMs(System.currentTimeMillis()); double downloaded = 0.0; // TODO: Print a message that this chunk has started downloading. + System.out.println("Chunk #" + chunkStatus.getChunkId() + " started downloading."); - while (downloaded < 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. + + int delay = config.getMinStepDelayMs() + + random.nextInt(config.getMaxStepDelayMs() - config.getMinStepDelayMs() + 1); + + try + { + Thread.sleep(delay); + } catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + return; + } + + double step = config.getMinStepDownloadMB() + + random.nextDouble() * (config.getMaxStepDownloadMB() - config.getMinStepDownloadMB()); + + downloaded += step; + + if (downloaded > chunkStatus.getChunkSizeMB()) + { + downloaded = chunkStatus.getChunkSizeMB(); + } + + chunkStatus.setDownloadedMB(downloaded); + + System.out.printf( + "Chunk #%d progress: %.2f / %.2f MB (%.2f%%)%n", + chunkStatus.getChunkId(), + downloaded, + 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. + + chunkStatus.setCompleted(true); + + chunkStatus.setEndTimeMs(System.currentTimeMillis()); + + long endTime = chunkStatus.getEndTimeMs(); + long start = chunkStatus.getStartTimeMs(); + + double seconds = (endTime - start) / 1000.0; + + double averageSpeed = 0.0; + if (seconds > 0) + { + averageSpeed = chunkStatus.getChunkSizeMB() / seconds; + } + + System.out.printf( + "Chunk #%d finished in %.2f seconds | Avg speed: %.2f MB/s%n", + chunkStatus.getChunkId(), + seconds, + averageSpeed + ); } } diff --git a/src/main/java/Main.java b/src/main/java/Main.java index 82bd239..76ded2a 100644 --- a/src/main/java/Main.java +++ b/src/main/java/Main.java @@ -1,15 +1,22 @@ 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) { + } + catch (Exception e) + { System.out.println("Failed to read configuration: " + e.getMessage()); return; } @@ -19,75 +26,138 @@ public class Main { System.out.println("Chunk count: " + config.getChunkCount()); System.out.println(); - // 2. Create chunks + System.out.println("======================================"); + System.out.println("Running Sequential Simulation"); + System.out.println("======================================"); + + SimulationResult sequentialResult = runSimulation(config, true); + + System.out.println(); + System.out.println("======================================"); + System.out.println("Running Multithreaded Simulation"); + System.out.println("======================================"); + + SimulationResult multithreadedResult = runSimulation(config, false); + + System.out.println(); + System.out.println("======================================"); + System.out.println("Performance Comparison"); + System.out.println("======================================"); + + System.out.printf("Sequential time: %.2f seconds%n", sequentialResult.totalSeconds); + System.out.printf("Sequential average speed: %.2f MB/s%n", sequentialResult.averageSpeed); + + System.out.printf("Multithreaded time: %.2f seconds%n", multithreadedResult.totalSeconds); + System.out.printf("Multithreaded average speed: %.2f MB/s%n", multithreadedResult.averageSpeed); + + if (multithreadedResult.totalSeconds > 0) + { + double speedup = sequentialResult.totalSeconds / multithreadedResult.totalSeconds; + System.out.printf("Speedup: %.2fx faster%n", speedup); + } + + System.out.println("======================================"); + System.out.println("Simulation finished."); + } + + private static SimulationResult runSimulation(DownloadConfig config, boolean sequentialMode) + { + + long programStartTime = System.currentTimeMillis(); + 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( + "Assigned Chunk " + chunk.getChunkId() + + " (" + chunk.getChunkSizeMB() + " MB)" + + " to " + workerThread.getName() + ); } - // 4. Create and start monitor thread + System.out.println(); + 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. + if (sequentialMode) + { - // 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(); - // } + System.out.println("Mode: SEQUENTIAL"); + System.out.println(); - // 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, + for (Thread workerThread : workerThreads) + { + workerThread.run(); + } + + } + else + { + + System.out.println("Mode: MULTITHREADED"); + System.out.println(); + + for (Thread workerThread : workerThreads) + { + workerThread.start(); + } + + try + { + for (Thread workerThread : workerThreads) + { + workerThread.join(); + } + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + System.out.println("Main thread interrupted while waiting for workers."); + return new SimulationResult(0.0, 0.0); + } + } + + try + { + monitorThread.join(); + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + System.out.println("Main thread interrupted while waiting for monitor."); + return new SimulationResult(0.0, 0.0); + } + + long programEndTime = System.currentTimeMillis(); + double totalSeconds = (programEndTime - programStartTime) / 1000.0; - // 7. Print final report System.out.println(); System.out.println("=== Final Report ==="); int completedChunks = 0; double downloadedMB = 0.0; - for (ChunkStatus chunk : chunks) { + for (ChunkStatus chunk : chunks) + { downloadedMB += chunk.getDownloadedMB(); - if (chunk.isCompleted()) { + if (chunk.isCompleted()) + { completedChunks++; } @@ -100,8 +170,34 @@ public class Main { } System.out.println(); + + System.out.println("Mode: " + (sequentialMode ? "Sequential" : "Multithreaded")); System.out.println("Completed chunks: " + completedChunks + "/" + chunks.size()); System.out.println("Downloaded total: " + downloadedMB + "/" + config.getTotalSizeMB() + " MB"); - System.out.println("Simulation finished."); + + System.out.printf("Total download time: %.2f seconds%n", totalSeconds); + + double overallSpeed = 0.0; + if (totalSeconds > 0) + { + overallSpeed = config.getTotalSizeMB() / totalSeconds; + } + + System.out.printf("Overall average speed: %.2f MB/s%n", overallSpeed); + + return new SimulationResult(totalSeconds, overallSpeed); } -} + + private static class SimulationResult + { + + private final double totalSeconds; + private final double averageSpeed; + + public SimulationResult(double totalSeconds, double averageSpeed) + { + this.totalSeconds = totalSeconds; + this.averageSpeed = averageSpeed; + } + } +} \ No newline at end of file diff --git a/src/main/java/ProgressMonitor.java b/src/main/java/ProgressMonitor.java index 475c0e0..0cb9430 100644 --- a/src/main/java/ProgressMonitor.java +++ b/src/main/java/ProgressMonitor.java @@ -1,69 +1,128 @@ 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 long monitorStartTime; - 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; } + private String createProgressBar(double percent) + { + int width = 30; + int filled = (int) (percent / 100 * width); + + StringBuilder bar = new StringBuilder("["); + for (int i = 0; i < width; i++) + { + if (i < filled) { + bar.append("#"); + } else { + bar.append("-"); + } + } + bar.append("]"); + + return bar.toString(); + } + @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() + { - while (true) { + monitorStartTime = System.currentTimeMillis(); + + while (true) + { double totalDownloadedMB = 0.0; int completedChunks = 0; - for (ChunkStatus chunk : chunks) { + for (ChunkStatus chunk : chunks) + { totalDownloadedMB += chunk.getDownloadedMB(); - if (chunk.isCompleted()) { + if (chunk.isCompleted()) + { completedChunks++; } } double percent = 0.0; - if (totalSizeMB > 0) { + if (totalSizeMB > 0) + { percent = (totalDownloadedMB * 100.0) / totalSizeMB; } + long currentTime = System.currentTimeMillis(); + double elapsedSeconds = (currentTime - monitorStartTime) / 1000.0; + + double currentSpeed = 0.0; + if (elapsedSeconds > 0) + { + currentSpeed = totalDownloadedMB / elapsedSeconds; + } + + double remainingMB = totalSizeMB - totalDownloadedMB; + + double etaSeconds = 0.0; + if (currentSpeed > 0) + { + etaSeconds = remainingMB / currentSpeed; + } + + System.out.println("--------------------------------------------------"); + for (ChunkStatus chunk : chunks) + { + System.out.printf( + "Chunk %d: %.1f MB downloaded %s%n", + chunk.getChunkId(), + chunk.getDownloadedMB(), + chunk.getChunkSizeMB(), + chunk.isCompleted() ? "(Completed ✅)" : "" + ); + } + System.out.printf( - "Progress for %s: %.1f/%.1f MB (%.2f%%), completed chunks: %d/%d%n", + "Total Progress for %s: %s %.2f%% (%d/%d chunks)%n", fileName, - totalDownloadedMB, - (double) totalSizeMB, + createProgressBar(percent), percent, completedChunks, - chunks.size() + chunks.size(), + currentSpeed, + etaSeconds ); + if (completedChunks == chunks.size()) + { + System.out.println(">>> Monitor: All chunks finished. Download complete."); + System.out.println("=================================================="); + break; + } - // TODO: - // If all chunks are completed, print a final message and exit the loop - - try { + try + { Thread.sleep(monitorDelayMs); - } catch (InterruptedException e) { + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); System.out.println("Progress monitor interrupted."); return; } } + } + } -- 2.54.0