Implementation of HW-8 Multithreading is complete

This commit is contained in:
2026-06-29 20:38:19 +03:30
parent 9e5c715088
commit 8b92095e75
7 changed files with 709 additions and 84 deletions
Generated
+1
View File
@@ -0,0 +1 @@
Main.java
+229
View File
@@ -0,0 +1,229 @@
# Advanced Programming Assignment 8
* Name: *Hesam Ghazi*
* Student Number: *403222015*
* Course: *Advanced Programming*
---
# Theoretical Questions
## 1. `start()` vs `run()`
### Question 1: What output do you get from the program? Why?
The program prints something similar to the following:
```
Calling run()
Running in: main
Calling start()
Running in: Thread-2
```
When `t1.run()` is called, the `run()` method executes like a normal Java method. It does **not** create a new thread, so it runs inside the current thread, which is the **main** thread.
When `t2.start()` is called, the Java Virtual Machine creates a **new thread**, and that thread executes the `run()` method independently. Therefore, `Thread.currentThread().getName()` returns `"Thread-2"`.
---
### Question 2: What's the difference in behavior between calling `start()` and `run()`?
| `start()` | `run()` |
| -------------------------------------------------- | ------------------------------------------- |
| Creates a new thread. | Does not create a new thread. |
| Executes `run()` concurrently. | Executes `run()` like a normal method call. |
| The JVM schedules the new thread. | Runs immediately in the current thread. |
| Allows multiple threads to execute simultaneously. | No parallelism or concurrency occurs. |
For example:
```java
Thread thread = new Thread(new MyRunnable());
thread.run(); // Executes on the current thread (main)
thread.start(); // Executes on a new thread
```
Using `start()` is essential when we want to perform tasks concurrently.
---
# 2. Daemon Threads
### Question 1: What output do you get from the program? Why?
A typical output is:
```
Main thread ends.
Daemon thread running...
Daemon thread running...
```
Sometimes only the following is printed:
```
Main thread ends.
```
This happens because daemon threads are **background threads**. When the main (user) thread finishes and no other user threads remain, the JVM automatically terminates all daemon threads, even if they have not completed their work.
Since the daemon thread sleeps for 500 ms in every iteration, it may be terminated before printing all twenty messages.
---
### Question 2: What happens if you remove `thread.setDaemon(true)`?
If `thread.setDaemon(true)` is removed, the thread becomes a **user thread**.
In this case, the JVM waits until the thread completes all twenty iterations before terminating the program.
The output becomes similar to:
```
Main thread ends.
Daemon thread running...
Daemon thread running...
...
Daemon thread running...
```
The program will continue running for approximately **10 seconds** (20 × 500 ms).
---
### Question 3: What are some real-life use cases of daemon threads?
Daemon threads are commonly used for background services that support the application but are not essential to its primary functionality.
Examples include:
* Garbage collection
* Automatic cache cleanup
* Logging services
* Monitoring system resources
* Background file synchronization
* Session timeout checking
* Periodic health checks
* Scheduled maintenance tasks
These tasks should stop automatically when the application exits, making daemon threads an appropriate choice.
---
# 3. A Shorter Way to Create Threads
### Question 1: What output do you get from the program?
The program prints:
```
Thread is running using a ...!
```
This message is printed by the newly created thread after `thread.start()` is called.
---
### Question 2: What is the `() -> { ... }` syntax called?
The syntax
```java
() -> {
...
}
```
is called a **Lambda Expression**.
Lambda expressions were introduced in **Java 8** as a concise way to implement functional interfaces such as `Runnable`.
The previous code
```java
Thread thread = new Thread(() -> {
System.out.println("Thread is running using a ...!");
});
```
is equivalent to
```java
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Thread is running using a ...!");
}
});
```
---
### Question 3: How is this code different from creating a class that extends `Thread` or implements `Runnable`?
There are three common approaches for creating a thread.
### 1. Extending `Thread`
```java
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Running...");
}
}
```
**Advantages**
* Easy for simple examples.
**Disadvantages**
* Java supports only single inheritance.
* The task is tightly coupled with the thread.
---
### 2. Implementing `Runnable`
```java
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Running...");
}
}
```
**Advantages**
* Better object-oriented design.
* Allows inheritance from another class.
* The task and the thread are separated.
---
### 3. Using a Lambda Expression
```java
Thread thread = new Thread(() -> {
System.out.println("Running...");
});
```
**Advantages**
* Very concise and readable.
* Eliminates unnecessary boilerplate code.
* Ideal for short tasks.
* Still implements the `Runnable` interface internally.
---
## Conclusion
Lambda expressions are the preferred choice for short and simple thread tasks because they produce cleaner and more maintainable code. For larger or reusable tasks, implementing `Runnable` is generally considered the best practice due to better separation of responsibilities. Extending `Thread` is the least flexible approach and is usually reserved for special cases.
+158
View File
@@ -0,0 +1,158 @@
import java.util.List;
public final class ConsoleUI {
private static final int BAR_WIDTH = 40;
private ConsoleUI() {
}
/**
* Clears the console.
*/
public static void clearScreen() {
System.out.print("\033[H\033[2J");
System.out.flush();
}
/**
* Prints the application header.
*/
public static void printHeader(String fileName, int totalSizeMB, int chunkCount) {
System.out.println("============================================================");
System.out.println(" SIMULATED DOWNLOAD MANAGER");
System.out.println("============================================================");
System.out.printf("File : %s%n", fileName);
System.out.printf("Size : %d MB%n", totalSizeMB);
System.out.printf("Chunks : %d%n", chunkCount);
System.out.println("============================================================");
System.out.println();
}
/**
* Prints overall download statistics.
*/
public static void printOverallProgress(
double downloaded,
double total,
double speed,
double etaSeconds) {
double percent = total == 0
? 100
: downloaded * 100.0 / total;
System.out.println("Overall Progress");
System.out.println(buildProgressBar(percent));
System.out.printf("Downloaded : %.1f / %.1f MB%n", downloaded, total);
System.out.printf("Progress : %.1f%%%n", percent);
System.out.printf("Speed : %.2f MB/s%n", speed);
System.out.printf("ETA : %s%n", formatTime(etaSeconds));
System.out.println();
}
/**
* Prints each chunk's progress.
*/
public static void printChunks(List<ChunkStatus> chunks) {
System.out.println("Chunks");
for (ChunkStatus chunk : chunks) {
double percent = chunk.getProgressPercentage();
System.out.printf(
"Chunk %-2d %s %6.1f%% %s%n",
chunk.getChunkId(),
buildProgressBar(percent),
percent,
chunk.isCompleted() ? "" : ""
);
}
System.out.println();
}
/**
* Prints the final report.
*/
public static void printFinalReport(List<ChunkStatus> chunks) {
System.out.println();
System.out.println("============================================================");
System.out.println(" FINAL REPORT");
System.out.println("============================================================");
for (ChunkStatus chunk : chunks) {
System.out.printf(
"Chunk %d : %.1f / %.1f MB | %.2f sec | %s%n",
chunk.getChunkId(),
chunk.getDownloadedMB(),
chunk.getChunkSizeMB(),
chunk.getDownloadDurationMs() / 1000.0,
chunk.isCompleted() ? "Completed" : "Incomplete"
);
}
System.out.println("============================================================");
}
/**
* Builds a text progress bar.
*/
public static String buildProgressBar(double percent) {
percent = Math.max(0, Math.min(100, percent));
int filled = (int) Math.round((percent / 100.0) * BAR_WIDTH);
StringBuilder sb = new StringBuilder();
sb.append("[");
for (int i = 0; i < BAR_WIDTH; i++) {
if (i < filled) {
sb.append("");
} else {
sb.append("");
}
}
sb.append("]");
return sb.toString();
}
/**
* Formats seconds as HH:MM:SS.
*/
public static String formatTime(double seconds) {
if (seconds <= 0 || Double.isInfinite(seconds) || Double.isNaN(seconds)) {
return "--:--:--";
}
int total = (int) Math.round(seconds);
int hours = total / 3600;
int minutes = (total % 3600) / 60;
int secs = total % 60;
return String.format("%02d:%02d:%02d", hours, minutes, secs);
}
/**
* Thread-safe logging method.
*/
public static synchronized void log(String message) {
System.out.println(message);
}
}
+89
View File
@@ -0,0 +1,89 @@
/**
* Utility class for calculating download statistics such as
* percentage, speed, and estimated remaining time (ETA).
*/
public final class DownloadStatistics {
private DownloadStatistics() {
// Utility class
}
/**
* Calculates the overall download percentage.
*
* @param downloadedMB Downloaded amount in MB
* @param totalMB Total file size in MB
* @return Percentage between 0 and 100
*/
public static double calculatePercentage(double downloadedMB, double totalMB) {
if (totalMB <= 0) {
return 100.0;
}
return (downloadedMB / totalMB) * 100.0;
}
/**
* Calculates the average download speed.
*
* @param downloadedMB Downloaded amount in MB
* @param elapsedMillis Elapsed time in milliseconds
* @return Speed in MB/s
*/
public static double calculateSpeed(double downloadedMB, long elapsedMillis) {
if (elapsedMillis <= 0) {
return 0.0;
}
return downloadedMB / (elapsedMillis / 1000.0);
}
/**
* Calculates the estimated remaining time.
*
* @param downloadedMB Downloaded amount
* @param totalMB Total size
* @param speedMBps Current speed
* @return Remaining time in seconds
*/
public static double calculateETA(
double downloadedMB,
double totalMB,
double speedMBps) {
if (speedMBps <= 0) {
return Double.POSITIVE_INFINITY;
}
double remaining = totalMB - downloadedMB;
if (remaining <= 0) {
return 0;
}
return remaining / speedMBps;
}
/**
* Formats a duration into HH:MM:SS.
*
* @param seconds Time in seconds
* @return Formatted time string
*/
public static String formatTime(double seconds) {
if (Double.isInfinite(seconds) || Double.isNaN(seconds)) {
return "--:--:--";
}
int totalSeconds = (int) Math.round(seconds);
int hours = totalSeconds / 3600;
int minutes = (totalSeconds % 3600) / 60;
int secs = totalSeconds % 60;
return String.format("%02d:%02d:%02d", hours, minutes, secs);
}
}
+73 -16
View File
@@ -3,9 +3,9 @@ import java.util.Random;
/** /**
* Simulates downloading a single chunk of a file. * Simulates downloading a single chunk of a file.
* *
* <p>This class is intentionally provided as a skeleton for students. * Each DownloadWorker is responsible for exactly one ChunkStatus object.
* The main multithreading and simulation logic should be completed * Since every worker only modifies its own ChunkStatus, no explicit
* in the run() method.</p> * synchronization is required.
*/ */
public class DownloadWorker implements Runnable { public class DownloadWorker implements Runnable {
@@ -21,23 +21,80 @@ public class DownloadWorker implements Runnable {
@Override @Override
public void run() { public void run() {
// TODO: Record the chunk start time in chunkStatus.
// Record start time
chunkStatus.setStartTimeMs(System.currentTimeMillis());
double downloaded = 0.0; double downloaded = 0.0;
// TODO: Print a message that this chunk has started downloading. System.out.printf(
"[%s] Chunk %d started (%.1f MB)%n",
Thread.currentThread().getName(),
chunkStatus.getChunkId(),
chunkStatus.getChunkSizeMB()
);
while (downloaded < chunkStatus.getChunkSizeMB()) { while (downloaded < chunkStatus.getChunkSizeMB()) {
// TODO: Generate a random sleep delay between min and max delay.
// TODO: Sleep for that delay. try {
// TODO: Generate a random download amount for this step. // Random delay
// TODO: Increase downloaded, but do not go beyond chunk size. int delay = random.nextInt(
// TODO: Save the updated downloaded value into chunkStatus. config.getMaxStepDelayMs() - config.getMinStepDelayMs() + 1
// TODO: Optionally print step-by-step progress. ) + config.getMinStepDelayMs();
Thread.sleep(delay);
} catch (InterruptedException e) {
System.out.printf(
"[%s] Chunk %d interrupted.%n",
Thread.currentThread().getName(),
chunkStatus.getChunkId()
);
Thread.currentThread().interrupt();
return;
}
// Random download amount
double step =
config.getMinStepDownloadMB()
+ random.nextDouble()
* (config.getMaxStepDownloadMB()
- config.getMinStepDownloadMB());
downloaded += step;
// Prevent downloading beyond chunk size
if (downloaded > chunkStatus.getChunkSizeMB()) {
downloaded = chunkStatus.getChunkSizeMB();
}
// Save progress
chunkStatus.setDownloadedMB(downloaded);
// Optional step-by-step debug output
System.out.printf(
"[%s] Chunk %d: %.1f/%.1f MB (%.1f%%)%n",
Thread.currentThread().getName(),
chunkStatus.getChunkId(),
chunkStatus.getDownloadedMB(),
chunkStatus.getChunkSizeMB(),
chunkStatus.getProgressPercentage()
);
} }
// TODO: Mark the chunk as completed. // Mark completed
// TODO: Record the chunk end time in chunkStatus. chunkStatus.setCompleted(true);
// TODO: Print a message that this chunk has finished downloading.
}
} // Record finish time
chunkStatus.setEndTimeMs(System.currentTimeMillis());
System.out.printf(
"[%s] Chunk %d completed in %.2f seconds.%n",
Thread.currentThread().getName(),
chunkStatus.getChunkId(),
chunkStatus.getDownloadDurationMs() / 1000.0
);
}
}
+106 -45
View File
@@ -2,11 +2,16 @@ 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("=================================================");
System.out.println(" SIMULATED DOWNLOAD MANAGER");
System.out.println("=================================================");
// 1. Read config // 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) {
@@ -14,94 +19,150 @@ public class Main {
return; return;
} }
System.out.println("File name: " + config.getFileName()); System.out.println("File Name : " + config.getFileName());
System.out.println("Total size (MB): " + config.getTotalSizeMB()); System.out.println("File Size : " + config.getTotalSizeMB() + " MB");
System.out.println("Chunk count: " + config.getChunkCount()); System.out.println("Chunk Count: " + config.getChunkCount());
System.out.println("Delay Range: "
+ config.getMinStepDelayMs()
+ " - "
+ config.getMaxStepDelayMs()
+ " ms");
System.out.println("Step Range : "
+ config.getMinStepDownloadMB()
+ " - "
+ config.getMaxStepDownloadMB()
+ " MB");
System.out.println(); System.out.println();
long simulationStart = System.currentTimeMillis();
// 2. Create chunks // 2. Create chunks
List<ChunkStatus> chunks = ChunkUtils.createChunks( List<ChunkStatus> chunks = ChunkUtils.createChunks(
config.getTotalSizeMB(), config.getTotalSizeMB(),
config.getChunkCount() config.getChunkCount()
); );
System.out.println("Created Chunks:");
for (ChunkStatus chunk : chunks) {
System.out.printf(
" Chunk %d -> %.1f MB%n",
chunk.getChunkId(),
chunk.getChunkSizeMB()
);
}
System.out.println();
// 3. Create worker threads // 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.printf(
// Students may print helpful debug information here, "Assigned Chunk %d to %s%n",
// for example which chunk is assigned to which worker thread. chunk.getChunkId(),
workerThread.getName()
);
} }
System.out.println();
// 4. Create and start monitor thread // 4. Create and start monitor thread
ProgressMonitor monitor = new ProgressMonitor(config, chunks); ProgressMonitor monitor = new ProgressMonitor(config, chunks);
Thread monitorThread = new Thread(monitor, "Progress-Monitor");
// TODO: Thread monitorThread = new Thread(
// Start the monitor thread before starting the workers monitor,
// so that progress can be displayed while downloading happens. "Progress-Monitor"
// );
// Example idea:
// monitorThread.start(); System.out.println("Starting monitor thread...");
monitorThread.start();
// 5. Start worker threads // 5. Start worker threads
// TODO: System.out.println("Starting worker threads...");
// Start each worker thread in workerThreads.
// Use a loop and call start() on each thread. for (Thread workerThread : workerThreads) {
workerThread.start();
}
// 6. Wait for workers to finish // 6. Wait for workers to finish
// TODO: try {
// 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: for (Thread workerThread : workerThreads) {
// After all workers finish, the monitor thread may also need to stop. workerThread.join();
// 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: // Wait for monitor to finish automatically
// this final report may show 0 progress because no worker has actually run yet. monitorThread.join();
// Until students complete the thread start/join TODOs above,
} catch (InterruptedException e) {
System.out.println("Main thread interrupted.");
Thread.currentThread().interrupt();
return;
}
long simulationEnd = System.currentTimeMillis();
// 7. Print final report // 7. Print final report
System.out.println(); System.out.println();
System.out.println("=== Final Report ==="); System.out.println("=================================================");
System.out.println(" FINAL REPORT");
System.out.println("=================================================");
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++;
} }
System.out.println( System.out.printf(
"Chunk " + chunk.getChunkId() "Chunk %-2d | Downloaded: %6.1f / %6.1f MB | %-10s | Duration: %.2f sec%n",
+ ": " + chunk.getDownloadedMB() chunk.getChunkId(),
+ "/" + chunk.getChunkSizeMB() chunk.getDownloadedMB(),
+ " MB" chunk.getChunkSizeMB(),
chunk.isCompleted() ? "Completed" : "Incomplete",
chunk.getDownloadDurationMs() / 1000.0
); );
} }
System.out.println(); System.out.println();
System.out.println("Completed chunks: " + completedChunks + "/" + chunks.size());
System.out.println("Downloaded total: " + downloadedMB + "/" + config.getTotalSizeMB() + " MB"); System.out.println(
"Completed Chunks: "
+ completedChunks
+ "/"
+ chunks.size()
);
System.out.printf(
"Downloaded Total: %.1f / %d MB%n",
downloadedMB,
config.getTotalSizeMB()
);
System.out.printf(
"Execution Time : %.2f seconds%n",
(simulationEnd - simulationStart) / 1000.0
);
System.out.println();
System.out.println("Simulation finished."); System.out.println("Simulation finished.");
} }
} }
+53 -23
View File
@@ -16,18 +16,11 @@ public class ProgressMonitor implements Runnable {
@Override @Override
public void run() { 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
long startTime = System.currentTimeMillis();
while (true) { while (true) {
double totalDownloadedMB = 0.0; double totalDownloadedMB = 0.0;
int completedChunks = 0; int completedChunks = 0;
@@ -39,31 +32,68 @@ public class ProgressMonitor implements Runnable {
} }
} }
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", long elapsedMillis = System.currentTimeMillis() - startTime;
fileName, double elapsedSeconds = elapsedMillis / 1000.0;
double remainingMB = totalSizeMB - totalDownloadedMB;
double percent = DownloadStatistics.calculatePercentage(
totalDownloadedMB, totalDownloadedMB,
(double) totalSizeMB, totalSizeMB
percent, );
completedChunks,
double speed = DownloadStatistics.calculateSpeed(
totalDownloadedMB,
elapsedMillis
);
double eta = DownloadStatistics.calculateETA(
totalDownloadedMB,
totalSizeMB,
speed
);
// Refresh console
ConsoleUI.clearScreen();
ConsoleUI.printHeader(
fileName,
totalSizeMB,
chunks.size() chunks.size()
); );
ConsoleUI.printOverallProgress(
totalDownloadedMB,
totalSizeMB,
speed,
eta
);
// TODO: ConsoleUI.printChunks(chunks);
// If all chunks are completed, print a final message and exit the loop
// Finished?
if (completedChunks == chunks.size()) {
System.out.println("Download completed successfully!");
System.out.printf(
"Total download time: %.2f seconds%n",
elapsedSeconds
);
ConsoleUI.printFinalReport(chunks);
return;
}
try { try {
Thread.sleep(monitorDelayMs); Thread.sleep(monitorDelayMs);
} catch (InterruptedException e) { } catch (InterruptedException e) {
System.out.println("Progress monitor interrupted."); Thread.currentThread().interrupt();
return; return;
} }
} }
} }
} }