From 5fbe9de273d22c35a50f60c5de2bbf9f3a76bcf8 Mon Sep 17 00:00:00 2001 From: 2025mohseni Date: Thu, 11 Jun 2026 02:38:34 +0330 Subject: [PATCH] Final version of project --- Report.md | 150 +++++++++++++++++++++++++++++ src/main/java/ConfigReader.java | 77 +++++---------- src/main/java/DownloadConfig.java | 75 ++++----------- src/main/java/DownloadWorker.java | 45 ++++----- src/main/java/Main.java | 120 ++++++++--------------- src/main/java/ProgressMonitor.java | 64 +++--------- 6 files changed, 267 insertions(+), 264 deletions(-) create mode 100644 Report.md diff --git a/Report.md b/Report.md new file mode 100644 index 0000000..88581dc --- /dev/null +++ b/Report.md @@ -0,0 +1,150 @@ +# Report - Multithreading Assignment + +## 1. Difference Between start() and run() + +In Java, both `start()` and `run()` are related to threads, but they behave differently. + +When we call the `run()` method directly, no new thread is created. The method runs like a normal method in the current thread, usually the main thread. + +When we call the `start()` method, Java creates a new thread and then executes the `run()` method inside that new thread. + +Example: +```java +Thread t1 = new Thread(() -> { +System.out.println("Thread 1: " + Thread.currentThread().getName()); +}); + +Thread t2 = new Thread(() -> { +System.out.println("Thread 2: " + Thread.currentThread().getName()); +}); + +t1.run(); +t2.start(); + +Possible output: + +text +Thread 1: main +Thread 2: Thread-0 + +In this output, `t1.run()` runs in the main thread, but `t2.start()` runs in a separate thread. + +So, the main difference is: + +- `run()` executes the code normally in the current thread. +- `start()` creates a new thread and runs the code concurrently. + +## 2. Daemon Threads + +A daemon thread is a background thread that does not prevent the program from exiting. + +If only daemon threads are still running and all normal user threads finish, the JVM stops the program automatically. + +Example: + +java +Thread daemonThread = new Thread(() -> { +while (true) { +System.out.println("Daemon thread is running..."); +try { +Thread.sleep(1000); +} catch (InterruptedException e) { +e.printStackTrace(); +} +} +}); + +daemonThread.setDaemon(true); +daemonThread.start(); + +System.out.println("Main thread finished."); + +Possible output: + +text +Main thread finished. +Daemon thread is running... + +The daemon thread may stop immediately after the main thread finishes because the JVM does not wait for daemon threads. + +If we remove this line: + +java +daemonThread.setDaemon(true); + +then the thread becomes a normal user thread. In that case, the program may not stop because the infinite loop keeps running. + +Real-life examples of daemon threads: + +- Garbage Collector in Java +- Background monitoring tasks +- Auto-save services +- Log cleanup tasks +- Background cache cleanup + +Daemon threads are useful for background tasks that should not block the application from closing. + +## 3. Lambda Expressions and Runnable + +The syntax `() -> { ... }` is called a lambda expression. + +In Java, lambda expressions can be used to write shorter code for functional interfaces such as `Runnable`. + +Traditional way: + +java +Runnable task = new Runnable() { +@Override +public void run() { +System.out.println("Task is running"); +} +}; + +Thread thread = new Thread(task); +thread.start(); + +Lambda way: + +java +Runnable task = () -> { +System.out.println("Task is running"); +}; + +Thread thread = new Thread(task); +thread.start(); + +Output: + +text +Task is running + +Both versions do the same thing, but the lambda version is shorter and cleaner. + +The lambda expression is useful because `Runnable` has only one abstract method: `run()`. + +Main differences: + +- Traditional implementation needs an anonymous class. +- Lambda expression uses shorter syntax. +- Lambda expression makes the code easier to read. +- Both can be used to create and run threads. + +## 4. Practical Part Explanation + +In the practical part of this assignment, a simulated download manager was implemented using multithreading. + +The file is divided into several chunks. Each chunk is downloaded by a separate worker thread. Each worker updates its own `ChunkStatus`. + +The `ProgressMonitor` runs separately and checks the progress of all chunks. It calculates the total downloaded size and prints the progress percentage until the download reaches 100%. + +The configuration values are read from `download_config.txt`, such as file size, number of chunks, delay range, download step range, and monitor delay. + +This project demonstrates the basic concepts of multithreading, including: + +- Creating multiple threads +- Running tasks concurrently +- Monitoring shared progress +- Waiting for threads to finish +- Simulating a real download manager + + diff --git a/src/main/java/ConfigReader.java b/src/main/java/ConfigReader.java index 5754060..2fd34b2 100644 --- a/src/main/java/ConfigReader.java +++ b/src/main/java/ConfigReader.java @@ -1,77 +1,45 @@ import java.io.BufferedReader; +import java.io.FileReader; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; public class ConfigReader { - - public static DownloadConfig readConfig(String fileName) { - InputStream inputStream = ConfigReader.class.getClassLoader().getResourceAsStream(fileName); - - if (inputStream == null) { - throw new IllegalArgumentException("Config file not found in resources: " + fileName); - } - - String configFileName = null; - int totalSizeMB = 0; + public static DownloadConfig readConfig(String filePath) { + String configFileName = "default_config"; + double totalSizeMB = 0; int chunkCount = 0; - int minStepDelayMs = 0; - int maxStepDelayMs = 0; + long minStepDelayMs = 0; + long maxStepDelayMs = 0; double minStepDownloadMB = 0; double maxStepDownloadMB = 0; + long monitorDelayMs = 500; - try (BufferedReader reader = new BufferedReader( - new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { - + try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) { String line; while ((line = reader.readLine()) != null) { line = line.trim(); - - if (line.isEmpty()) { - continue; - } - + if (line.isEmpty() || line.startsWith("#")) continue; String[] parts = line.split("=", 2); - if (parts.length != 2) { - continue; - } + if (parts.length != 2) continue; String key = parts[0].trim(); String value = parts[1].trim(); switch (key) { - case "fileName": - configFileName = value; - break; - case "totalSizeMB": - totalSizeMB = Integer.parseInt(value); - break; - case "chunkCount": - chunkCount = Integer.parseInt(value); - break; - case "minStepDelayMs": - minStepDelayMs = Integer.parseInt(value); - break; - case "maxStepDelayMs": - maxStepDelayMs = Integer.parseInt(value); - break; - case "minStepDownloadMB": - minStepDownloadMB = Double.parseDouble(value); - break; - case "maxStepDownloadMB": - maxStepDownloadMB = Double.parseDouble(value); - break; - default: - // Ignore unknown keys to keep parsing simple - break; + case "configFileName": configFileName = value; break; + case "totalSizeMB": totalSizeMB = Double.parseDouble(value); break; + case "chunkCount": chunkCount = Integer.parseInt(value); break; + case "minStepDelayMs": minStepDelayMs = Long.parseLong(value); break; + case "maxStepDelayMs": maxStepDelayMs = Long.parseLong(value); break; + case "minStepDownloadMB": minStepDownloadMB = Double.parseDouble(value); break; + case "maxStepDownloadMB": maxStepDownloadMB = Double.parseDouble(value); break; + case "monitorDelayMs": monitorDelayMs = Long.parseLong(value); break; } } - - } catch (IOException e) { - throw new RuntimeException("Error reading config file: " + fileName, e); + } catch (Exception e) { + System.err.println("Error reading config: " + e.getMessage()); } + // ترتیب دقیقاً مطابق با سازنده DownloadConfig return new DownloadConfig( configFileName, totalSizeMB, @@ -79,7 +47,8 @@ public class ConfigReader { minStepDelayMs, maxStepDelayMs, minStepDownloadMB, - maxStepDownloadMB + maxStepDownloadMB, + monitorDelayMs ); } } diff --git a/src/main/java/DownloadConfig.java b/src/main/java/DownloadConfig.java index dc1ba3a..80a027f 100644 --- a/src/main/java/DownloadConfig.java +++ b/src/main/java/DownloadConfig.java @@ -1,70 +1,37 @@ -/** - * Represents the configuration parameters for the simulated download manager. - * This class is immutable to ensure thread-safety when shared among worker threads. - */ public class DownloadConfig { - private final String fileName; - private final int totalSizeMB; + private final String configFileName; + private final double totalSizeMB; private final int chunkCount; - private final int minStepDelayMs; - private final int maxStepDelayMs; + private final long minStepDelayMs; + private final long maxStepDelayMs; private final double minStepDownloadMB; private final double maxStepDownloadMB; + private final long monitorDelayMs; - /** - * Constructs a new DownloadConfig with specified simulation parameters. - */ - public DownloadConfig(String fileName, int totalSizeMB, int chunkCount, - int minStepDelayMs, int maxStepDelayMs, - double minStepDownloadMB, double maxStepDownloadMB) { - this.fileName = fileName; + public DownloadConfig(String configFileName, double totalSizeMB, int chunkCount, + long minStepDelayMs, long maxStepDelayMs, + double minStepDownloadMB, double maxStepDownloadMB, + long monitorDelayMs) { + this.configFileName = configFileName; this.totalSizeMB = totalSizeMB; this.chunkCount = chunkCount; this.minStepDelayMs = minStepDelayMs; this.maxStepDelayMs = maxStepDelayMs; this.minStepDownloadMB = minStepDownloadMB; this.maxStepDownloadMB = maxStepDownloadMB; + this.monitorDelayMs = monitorDelayMs; } - // Getters - public String getFileName() { - return fileName; - } + public String getConfigFileName() { return configFileName; } + public double getTotalSizeMB() { return totalSizeMB; } + public int getChunkCount() { return chunkCount; } // معادل تعداد تردها + public long getMinStepDelayMs() { return minStepDelayMs; } + public long getMaxStepDelayMs() { return maxStepDelayMs; } + public double getMinStepDownloadMB() { return minStepDownloadMB; } + public double getMaxStepDownloadMB() { return maxStepDownloadMB; } + public long getMonitorDelayMs() { return monitorDelayMs; } - public int getTotalSizeMB() { - return totalSizeMB; - } - - public int getChunkCount() { - return chunkCount; - } - - public int getMinStepDelayMs() { - return minStepDelayMs; - } - - public int getMaxStepDelayMs() { - return maxStepDelayMs; - } - - public double getMinStepDownloadMB() { - return minStepDownloadMB; - } - - public double getMaxStepDownloadMB() { - return maxStepDownloadMB; - } - - @Override - public String toString() { - return "DownloadConfig{" + - "fileName='" + fileName + '\'' + - ", totalSizeMB=" + totalSizeMB + - ", chunkCount=" + chunkCount + - ", minStepDelayMs=" + minStepDelayMs + - ", maxStepDelayMs=" + maxStepDelayMs + - ", minStepDownloadMB=" + minStepDownloadMB + - ", maxStepDownloadMB=" + maxStepDownloadMB + - '}'; + public double getChunkSizeMB() { + return (chunkCount > 0) ? (totalSizeMB / chunkCount) : 0; } } diff --git a/src/main/java/DownloadWorker.java b/src/main/java/DownloadWorker.java index 546de2d..306022c 100644 --- a/src/main/java/DownloadWorker.java +++ b/src/main/java/DownloadWorker.java @@ -1,43 +1,40 @@ 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.

- */ public class DownloadWorker implements Runnable { - private final ChunkStatus chunkStatus; private final DownloadConfig config; - private final Random random; + private final Random random = new Random(); public DownloadWorker(ChunkStatus chunkStatus, DownloadConfig config) { this.chunkStatus = chunkStatus; this.config = config; - this.random = new Random(); } @Override public void run() { - // 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. + try { + while (downloaded < chunkStatus.getChunkSizeMB()) { + long delay = config.getMinStepDelayMs() + + (long)(random.nextDouble() * (config.getMaxStepDelayMs() - config.getMinStepDelayMs())); - 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. + Thread.sleep(delay); + + double step = config.getMinStepDownloadMB() + + (random.nextDouble() * (config.getMaxStepDownloadMB() - config.getMinStepDownloadMB())); + + downloaded += step; + if (downloaded > chunkStatus.getChunkSizeMB()) { + downloaded = chunkStatus.getChunkSizeMB(); + } + chunkStatus.setDownloadedMB(downloaded); + } + chunkStatus.setCompleted(true); + chunkStatus.setEndTimeMs(System.currentTimeMillis()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } - - // TODO: Mark the chunk as completed. - // TODO: Record the chunk end time in chunkStatus. - // TODO: Print a message that this chunk has finished downloading. } - } diff --git a/src/main/java/Main.java b/src/main/java/Main.java index 82bd239..95ad691 100644 --- a/src/main/java/Main.java +++ b/src/main/java/Main.java @@ -3,105 +3,61 @@ import java.util.List; public class Main { public static void main(String[] args) { - System.out.println("=== Simulated Download Manager ==="); + DownloadConfig config = ConfigReader.readConfig("src/main/resources/download_config.txt"); - // 1. Read config - DownloadConfig config; - try { - config = ConfigReader.readConfig("download_config.txt"); - } catch (Exception e) { - System.out.println("Failed to read configuration: " + e.getMessage()); - return; + List chunks = new ArrayList<>(); + + double chunkSize = config.getTotalSizeMB() / config.getChunkCount(); + + for (int i = 0; i < config.getChunkCount(); i++) { + chunks.add(new ChunkStatus(i + 1, chunkSize)); } - 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(); + ProgressMonitor monitor = new ProgressMonitor(chunks, config); - // 2. Create chunks - List chunks = ChunkUtils.createChunks( - config.getTotalSizeMB(), - config.getChunkCount() - ); + Thread monitorThread = new Thread(monitor, "Progress-Monitor"); + monitorThread.setDaemon(true); + monitorThread.start(); - // 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( + new DownloadWorker(chunk, config), + "Worker-" + chunk.getChunkId() + ); workerThreads.add(workerThread); - - // TODO: - // Students may print helpful debug information here, - // for example which chunk is assigned to which worker thread. + workerThread.start(); } - // 4. Create and start monitor thread - ProgressMonitor monitor = new ProgressMonitor(config, chunks); - Thread monitorThread = new Thread(monitor, "Progress-Monitor"); + try { + for (Thread thread : workerThreads) { + thread.join(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + System.err.println("Main thread interrupted while waiting for workers: " + e.getMessage()); + } - // TODO: - // Start the monitor thread before starting the workers - // so that progress can be displayed while downloading happens. - // - // Example idea: - // monitorThread.start(); - - // 5. Start worker threads - // TODO: - // Start each worker thread in workerThreads. - // Use a loop and call start() on each thread. - - // 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; + double totalDownloaded = 0; for (ChunkStatus chunk : chunks) { - downloadedMB += chunk.getDownloadedMB(); - - if (chunk.isCompleted()) { - completedChunks++; - } - - System.out.println( - "Chunk " + chunk.getChunkId() - + ": " + chunk.getDownloadedMB() - + "/" + chunk.getChunkSizeMB() - + " MB" - ); + totalDownloaded += chunk.getDownloadedMB(); } + double totalSize = config.getTotalSizeMB(); + double progress = (totalDownloaded / totalSize) * 100; + 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.printf( + "Progress: %.2f%% (%.2f / %.2f MB)%n", + progress, + totalDownloaded, + totalSize + ); + + System.out.println("All workers finished."); } } diff --git a/src/main/java/ProgressMonitor.java b/src/main/java/ProgressMonitor.java index 475c0e0..f714b0c 100644 --- a/src/main/java/ProgressMonitor.java +++ b/src/main/java/ProgressMonitor.java @@ -1,68 +1,32 @@ import java.util.List; public class ProgressMonitor implements Runnable { - - private final String fileName; - private final int totalSizeMB; private final List chunks; - private final long monitorDelayMs; + private final DownloadConfig config; + private volatile boolean running = true; - public ProgressMonitor(DownloadConfig config, List chunks) { - this.fileName = config.getFileName(); - this.totalSizeMB = config.getTotalSizeMB(); + public ProgressMonitor(List chunks, DownloadConfig config) { this.chunks = chunks; - this.monitorDelayMs = 500; + this.config = config; } + public void stop() { this.running = false; } + @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 + while (running) { + double totalDownloaded = chunks.stream().mapToDouble(ChunkStatus::getDownloadedMB).sum(); + double percent = (totalDownloaded / config.getTotalSizeMB()) * 100; + System.out.printf("\rProgress: %.2f%% (%.2f / %.2f MB)", + percent, totalDownloaded, config.getTotalSizeMB()); - while (true) { - double totalDownloadedMB = 0.0; - int completedChunks = 0; - - for (ChunkStatus chunk : chunks) { - totalDownloadedMB += chunk.getDownloadedMB(); - - if (chunk.isCompleted()) { - completedChunks++; - } - } - - 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, - totalDownloadedMB, - (double) totalSizeMB, - percent, - completedChunks, - chunks.size() - ); - - - // TODO: - // If all chunks are completed, print a final message and exit the loop + if (totalDownloaded >= config.getTotalSizeMB()) break; try { - Thread.sleep(monitorDelayMs); + Thread.sleep(config.getMonitorDelayMs()); } catch (InterruptedException e) { - System.out.println("Progress monitor interrupted."); - return; + break; } } }