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