1 Commits
Author SHA1 Message Date
Saba_frm 55b3f70d7c develop 2026-06-15 20:44:47 +03:30
6 changed files with 392 additions and 97 deletions
+1 -3
View File
@@ -8,7 +8,5 @@
</list> </list>
</option> </option>
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" project-jdk-name="21" project-jdk-type="JavaSDK" />
<output url="file://$PROJECT_DIR$/out" />
</component>
</project> </project>
+65
View File
@@ -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.
+34 -19
View File
@@ -7,7 +7,8 @@
* Represents the state and download progress of a single file chunk. * Represents the state and download progress of a single file chunk.
* Each worker thread updates its own ChunkStatus, while the monitor thread reads it. * 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 int chunkId;
private final double chunkSizeMB; private final double chunkSizeMB;
private volatile double downloadedMB; private volatile double downloadedMB;
@@ -19,7 +20,8 @@ public class ChunkStatus {
* Initializes a chunk with its unique ID and total allocated size. * Initializes a chunk with its unique ID and total allocated size.
* Progress-related fields are initialized to default values. * Progress-related fields are initialized to default values.
*/ */
public ChunkStatus(int chunkId, double chunkSizeMB) { public ChunkStatus(int chunkId, double chunkSizeMB)
{
this.chunkId = chunkId; this.chunkId = chunkId;
this.chunkSizeMB = chunkSizeMB; this.chunkSizeMB = chunkSizeMB;
this.downloadedMB = 0.0; this.downloadedMB = 0.0;
@@ -28,49 +30,58 @@ public class ChunkStatus {
this.endTimeMs = 0; this.endTimeMs = 0;
} }
// Getters and Setters public int getChunkId()
public int getChunkId() { {
return chunkId; return chunkId;
} }
public double getChunkSizeMB() { public double getChunkSizeMB()
{
return chunkSizeMB; return chunkSizeMB;
} }
public double getDownloadedMB() { public double getDownloadedMB()
{
return downloadedMB; return downloadedMB;
} }
public void setDownloadedMB(double downloadedMB) { public void setDownloadedMB(double downloadedMB)
// Guard to prevent downloaded size exceeding actual chunk size {
if (downloadedMB >= this.chunkSizeMB) { if (downloadedMB >= this.chunkSizeMB)
{
this.downloadedMB = this.chunkSizeMB; this.downloadedMB = this.chunkSizeMB;
} else { } else {
this.downloadedMB = downloadedMB; this.downloadedMB = downloadedMB;
} }
} }
public boolean isCompleted() { public boolean isCompleted()
{
return completed; return completed;
} }
public void setCompleted(boolean completed) { public void setCompleted(boolean completed)
{
this.completed = completed; this.completed = completed;
} }
public long getStartTimeMs() { public long getStartTimeMs()
{
return startTimeMs; return startTimeMs;
} }
public void setStartTimeMs(long startTimeMs) { public void setStartTimeMs(long startTimeMs)
{
this.startTimeMs = startTimeMs; this.startTimeMs = startTimeMs;
} }
public long getEndTimeMs() { public long getEndTimeMs()
{
return endTimeMs; return endTimeMs;
} }
public void setEndTimeMs(long endTimeMs) { public void setEndTimeMs(long endTimeMs)
{
this.endTimeMs = endTimeMs; this.endTimeMs = endTimeMs;
} }
@@ -78,8 +89,10 @@ public class ChunkStatus {
* Helper method to calculate the duration of this specific chunk's download. * Helper method to calculate the duration of this specific chunk's download.
* Returns 0 if the chunk hasn't started or finished yet. * Returns 0 if the chunk hasn't started or finished yet.
*/ */
public long getDownloadDurationMs() { public long getDownloadDurationMs()
if (startTimeMs > 0 && endTimeMs > startTimeMs) { {
if (startTimeMs > 0 && endTimeMs > startTimeMs)
{
return endTimeMs - startTimeMs; return endTimeMs - startTimeMs;
} else if (startTimeMs > 0 && !completed) { } else if (startTimeMs > 0 && !completed) {
return System.currentTimeMillis() - startTimeMs; return System.currentTimeMillis() - startTimeMs;
@@ -90,13 +103,15 @@ public class ChunkStatus {
/** /**
* Helper method to calculate the download percentage of this chunk. * Helper method to calculate the download percentage of this chunk.
*/ */
public double getProgressPercentage() { public double getProgressPercentage()
{
if (chunkSizeMB == 0) return 100.0; if (chunkSizeMB == 0) return 100.0;
return (downloadedMB / chunkSizeMB) * 100.0; return (downloadedMB / chunkSizeMB) * 100.0;
} }
@Override @Override
public String toString() { public String toString()
{
return String.format("Chunk #%d: %.1f/%.1f MB (%.1f%%)%s", return String.format("Chunk #%d: %.1f/%.1f MB (%.1f%%)%s",
chunkId, chunkId,
downloadedMB, downloadedMB,
+66 -4
View File
@@ -7,37 +7,99 @@ import java.util.Random;
* The main multithreading and simulation logic should be completed * The main multithreading and simulation logic should be completed
* in the run() method.</p> * in the run() method.</p>
*/ */
public class DownloadWorker implements Runnable { public class DownloadWorker implements Runnable
{
private final ChunkStatus chunkStatus; private final ChunkStatus chunkStatus;
private final DownloadConfig config; private final DownloadConfig config;
private final Random random; private final Random random;
public DownloadWorker(ChunkStatus chunkStatus, DownloadConfig config) { public DownloadWorker(ChunkStatus chunkStatus, DownloadConfig config)
{
this.chunkStatus = chunkStatus; this.chunkStatus = chunkStatus;
this.config = config; this.config = config;
this.random = new Random(); this.random = new Random();
} }
@Override @Override
public void run() { public void run()
{
long startTime = System.currentTimeMillis();
// TODO: Record the chunk start time in chunkStatus. // TODO: Record the chunk start time in chunkStatus.
chunkStatus.setStartTimeMs(System.currentTimeMillis());
double downloaded = 0.0; double downloaded = 0.0;
// TODO: Print a message that this chunk has started downloading. // 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: Generate a random sleep delay between min and max delay.
// TODO: Sleep for that delay. // TODO: Sleep for that delay.
// TODO: Generate a random download amount for this step. // TODO: Generate a random download amount for this step.
// TODO: Increase downloaded, but do not go beyond chunk size. // TODO: Increase downloaded, but do not go beyond chunk size.
// TODO: Save the updated downloaded value into chunkStatus. // TODO: Save the updated downloaded value into chunkStatus.
// TODO: Optionally print step-by-step progress. // 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: Mark the chunk as completed.
// TODO: Record the chunk end time in chunkStatus. // TODO: Record the chunk end time in chunkStatus.
// TODO: Print a message that this chunk has finished downloading. // 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
);
} }
} }
+141 -45
View File
@@ -1,15 +1,22 @@
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public class Main { public class Main
public static void main(String[] args) { {
public static void main(String[] args)
{
System.out.println("=== Simulated Download Manager ==="); System.out.println("=== Simulated Download Manager ===");
// 1. Read config
DownloadConfig config; DownloadConfig config;
try {
try
{
config = ConfigReader.readConfig("download_config.txt"); config = ConfigReader.readConfig("download_config.txt");
} catch (Exception e) { }
catch (Exception e)
{
System.out.println("Failed to read configuration: " + e.getMessage()); System.out.println("Failed to read configuration: " + e.getMessage());
return; return;
} }
@@ -19,75 +26,138 @@ public class Main {
System.out.println("Chunk count: " + config.getChunkCount()); System.out.println("Chunk count: " + config.getChunkCount());
System.out.println(); 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<ChunkStatus> chunks = ChunkUtils.createChunks( List<ChunkStatus> chunks = ChunkUtils.createChunks(
config.getTotalSizeMB(), config.getTotalSizeMB(),
config.getChunkCount() config.getChunkCount()
); );
// 3. Create worker threads
List<Thread> workerThreads = new ArrayList<>(); List<Thread> workerThreads = new ArrayList<>();
for (ChunkStatus chunk : chunks) { for (ChunkStatus chunk : chunks)
{
DownloadWorker worker = new DownloadWorker(chunk, config); DownloadWorker worker = new DownloadWorker(chunk, config);
Thread workerThread = new Thread(worker, "Worker-" + chunk.getChunkId()); Thread workerThread = new Thread(worker, "Worker-" + chunk.getChunkId());
workerThreads.add(workerThread); workerThreads.add(workerThread);
// TODO: System.out.println(
// Students may print helpful debug information here, "Assigned Chunk " + chunk.getChunkId()
// for example which chunk is assigned to which worker thread. + " (" + chunk.getChunkSizeMB() + " MB)"
+ " to " + workerThread.getName()
);
} }
// 4. Create and start monitor thread System.out.println();
ProgressMonitor monitor = new ProgressMonitor(config, chunks); ProgressMonitor monitor = new ProgressMonitor(config, chunks);
Thread monitorThread = new Thread(monitor, "Progress-Monitor"); Thread monitorThread = new Thread(monitor, "Progress-Monitor");
// TODO: monitorThread.start();
// 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 if (sequentialMode)
// TODO: {
// Start each worker thread in workerThreads.
// Use a loop and call start() on each thread.
// 6. Wait for workers to finish System.out.println("Mode: SEQUENTIAL");
// TODO: System.out.println();
// 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: for (Thread workerThread : workerThreads)
// this final report may show 0 progress because no worker has actually run yet. {
// Until students complete the thread start/join TODOs above, 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();
System.out.println("=== Final Report ==="); System.out.println("=== Final Report ===");
int completedChunks = 0; int completedChunks = 0;
double downloadedMB = 0.0; double downloadedMB = 0.0;
for (ChunkStatus chunk : chunks) { for (ChunkStatus chunk : chunks)
{
downloadedMB += chunk.getDownloadedMB(); downloadedMB += chunk.getDownloadedMB();
if (chunk.isCompleted()) { if (chunk.isCompleted())
{
completedChunks++; completedChunks++;
} }
@@ -100,8 +170,34 @@ public class Main {
} }
System.out.println(); System.out.println();
System.out.println("Mode: " + (sequentialMode ? "Sequential" : "Multithreaded"));
System.out.println("Completed chunks: " + completedChunks + "/" + chunks.size()); System.out.println("Completed chunks: " + completedChunks + "/" + chunks.size());
System.out.println("Downloaded total: " + downloadedMB + "/" + config.getTotalSizeMB() + " MB"); 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;
}
} }
} }
+84 -25
View File
@@ -1,69 +1,128 @@
import java.util.List; import java.util.List;
public class ProgressMonitor implements Runnable { public class ProgressMonitor implements Runnable
{
private final String fileName; private final String fileName;
private final int totalSizeMB; private final int totalSizeMB;
private final List<ChunkStatus> chunks; private final List<ChunkStatus> chunks;
private final long monitorDelayMs; private final long monitorDelayMs;
private long monitorStartTime;
public ProgressMonitor(DownloadConfig config, List<ChunkStatus> chunks) { public ProgressMonitor(DownloadConfig config, List<ChunkStatus> chunks)
{
this.fileName = config.getFileName(); this.fileName = config.getFileName();
this.totalSizeMB = config.getTotalSizeMB(); this.totalSizeMB = config.getTotalSizeMB();
this.chunks = chunks; this.chunks = chunks;
this.monitorDelayMs = 500; 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 @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; double totalDownloadedMB = 0.0;
int completedChunks = 0; int completedChunks = 0;
for (ChunkStatus chunk : chunks) { for (ChunkStatus chunk : chunks)
{
totalDownloadedMB += chunk.getDownloadedMB(); totalDownloadedMB += chunk.getDownloadedMB();
if (chunk.isCompleted()) { if (chunk.isCompleted())
{
completedChunks++; completedChunks++;
} }
} }
double percent = 0.0; double percent = 0.0;
if (totalSizeMB > 0) { if (totalSizeMB > 0)
{
percent = (totalDownloadedMB * 100.0) / totalSizeMB; 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( System.out.printf(
"Progress for %s: %.1f/%.1f MB (%.2f%%), completed chunks: %d/%d%n", "Chunk %d: %.1f MB downloaded %s%n",
chunk.getChunkId(),
chunk.getDownloadedMB(),
chunk.getChunkSizeMB(),
chunk.isCompleted() ? "(Completed ✅)" : ""
);
}
System.out.printf(
"Total Progress for %s: %s %.2f%% (%d/%d chunks)%n",
fileName, fileName,
totalDownloadedMB, createProgressBar(percent),
(double) totalSizeMB,
percent, percent,
completedChunks, 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: try
// If all chunks are completed, print a final message and exit the loop {
try {
Thread.sleep(monitorDelayMs); Thread.sleep(monitorDelayMs);
} catch (InterruptedException e) { }
catch (InterruptedException e)
{
Thread.currentThread().interrupt();
System.out.println("Progress monitor interrupted."); System.out.println("Progress monitor interrupted.");
return; return;
} }
} }
} }
} }