diff --git a/Report.md b/Report.md new file mode 100644 index 0000000..f9241f8 --- /dev/null +++ b/Report.md @@ -0,0 +1,160 @@ +# Theoretical Questions + +## 1. start() vs run() + +### Question 1: What output do you get from the program? Why? + +output: + +```text +Calling run() +Running in: main +Calling start() +Running in: Thread-2 +``` + +Explanation: + +When `t1.run()` is called, the `run()` method executes like a normal method call in the current thread, which is the `main` thread. Therefore, `Thread.currentThread().getName()` returns `"main"`. +When `t2.start()` is called, Java creates a new thread named `"Thread-2"` and executes the `run()` method inside that new thread. Therefore, the output shows `"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()` in the current thread. | +| Thread scheduler manages execution. | Behaves like a normal method call. | +| Enables true multithreading. | No multithreading occurs. | + +--- + +## 2. Daemon Threads + +### Question 1: What output do you get from the program? Why? + +Typical output: + +```text +Main thread ends. +``` + +Or a few lines such as: + +```text +Daemon thread running... +Main thread ends. +``` + +Explanation: + +The created thread is marked as a daemon thread using: + +```java +thread.setDaemon(true); +``` + +A daemon thread runs in the background and does not prevent the JVM from shutting down. Once the `main` thread finishes, there are no user (non-daemon) threads left, so the JVM terminates immediately. As a result, the daemon thread may not complete all 20 iterations. + +--- + +### Question 2: What happens if you remove thread.setDaemon(true)? + +The thread becomes a normal user thread. + +In that case, the JVM waits for the thread to finish before exiting. The program continues running for approximately 10 seconds (20 × 500 ms), and all 20 messages are printed. + +Example output: + +```text +Main thread ends. +Daemon thread running... +Daemon thread running... +... +``` + +The program exits only after the thread completes its work. + +--- + +### Question 3: What are some real-life use cases of daemon threads? + +Daemon threads are commonly used for background services, such as: + +* Garbage collection. +* Monitoring system resources. +* Periodic cleanup tasks. +* Cache maintenance. +* Logging services. +* Background status reporting. + +These tasks are supportive and should not keep the application alive when all user threads have finished. + +--- + +## 3. A Shorter Way to Create Threads + +### Question 1: What output do you get from the program? + +Output: + +```text +Thread is running using a ...! +``` + +The message is printed by the newly created thread after `thread.start()` is called. + +--- + +### Question 2: What is the () -> { ... } syntax called? + +This syntax is called a **Lambda Expression**. + +Lambda expressions provide a concise way to implement functional interfaces such as `Runnable`. + +--- + +### Question 3: How is this code different from creating a class that extends Thread or implements Runnable? + +#### Extending Thread + +```java +class MyThread extends Thread { + public void run() { + System.out.println("Hello"); + } +} +``` + +* Requires creating a separate class. +* Less flexible because Java does not support multiple inheritance. + +#### Implementing Runnable + +```java +class MyRunnable implements Runnable { + public void run() { + System.out.println("Hello"); + } +} +``` + +* Separates the task from the thread. +* More flexible and commonly preferred. + +#### Using a Lambda Expression + +```java +Thread thread = new Thread(() -> { + System.out.println("Hello"); +}); +``` + +* Shortest and most readable approach. +* No need for an extra class. +* Ideal when the task is simple and used only once. +* Internally still provides an implementation of the `Runnable` interface. + +Therefore, lambda expressions offer a concise and modern way to create threads while keeping the code easy to read and maintain. diff --git a/src/main/java/DownloadWorker.java b/src/main/java/DownloadWorker.java index 546de2d..06c33ac 100644 --- a/src/main/java/DownloadWorker.java +++ b/src/main/java/DownloadWorker.java @@ -12,6 +12,7 @@ public class DownloadWorker implements Runnable { private final ChunkStatus chunkStatus; private final DownloadConfig config; private final Random random; + private static final Object lock = new Object(); public DownloadWorker(ChunkStatus chunkStatus, DownloadConfig config) { this.chunkStatus = chunkStatus; @@ -20,24 +21,64 @@ public class DownloadWorker implements Runnable { } @Override - public void run() { - // TODO: Record the chunk start time in chunkStatus. + public void run() + { + chunkStatus.setStartTimeMs(System.currentTimeMillis()); + double downloaded = 0.0; - // TODO: Print a message that this chunk has started downloading. - - 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. + synchronized(lock) + { + System.out.printf( + "[%s] Started downloading Chunk #%d (%.1f MB)%n", + Thread.currentThread().getName(), + chunkStatus.getChunkId(), + chunkStatus.getChunkSizeMB() + ); } - // TODO: Mark the chunk as completed. - // TODO: Record the chunk end time in chunkStatus. - // TODO: Print a message that this chunk has finished downloading. - } + while (downloaded < chunkStatus.getChunkSizeMB()) + { + try + { + int delay = + config.getMinStepDelayMs() + + random.nextInt( + config.getMaxStepDelayMs() + - config.getMinStepDelayMs() + + 1 + ); + Thread.sleep(delay); + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + return; + } + + double stepDownload = + config.getMinStepDownloadMB() + + random.nextDouble() + * (config.getMaxStepDownloadMB() + - config.getMinStepDownloadMB()); + + downloaded += stepDownload; + + if (downloaded > chunkStatus.getChunkSizeMB()) + downloaded = chunkStatus.getChunkSizeMB(); + + chunkStatus.setDownloadedMB(downloaded); + } + + chunkStatus.setCompleted(true); + chunkStatus.setEndTimeMs(System.currentTimeMillis()); + + System.out.printf( + "[%s] Finished Chunk #%d in %.2f seconds%n", + Thread.currentThread().getName(), + chunkStatus.getChunkId(), + chunkStatus.getDownloadDurationMs() / 1000.0 + ); + } } diff --git a/src/main/java/Main.java b/src/main/java/Main.java index 82bd239..384bdcd 100644 --- a/src/main/java/Main.java +++ b/src/main/java/Main.java @@ -29,53 +29,60 @@ public class Main { List workerThreads = new ArrayList<>(); 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.printf( + "Assigned Chunk #%d -> %s%n", + chunk.getChunkId(), + workerThread.getName() + ); } // 4. Create and start monitor thread 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. + long parallelStart = System.currentTimeMillis(); + + for (Thread thread : workerThreads) + thread.start(); // 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(); - // } + try + { + for (Thread thread : workerThreads) + thread.join(); + } + catch (InterruptedException e) + { + System.out.println("Main thread interrupted."); + Thread.currentThread().interrupt(); + } - // 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. + try + { + monitorThread.join(); + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + } - // NOTE: - // this final report may show 0 progress because no worker has actually run yet. - // Until students complete the thread start/join TODOs above, + long parallelEnd = System.currentTimeMillis(); + + double parallelSeconds = (parallelEnd - parallelStart) / 1000.0; + + System.out.printf( + "%nMultithreaded execution time: %.2f seconds%n", + parallelSeconds + ); // 7. Print final report System.out.println(); @@ -103,5 +110,78 @@ public class Main { System.out.println("Completed chunks: " + completedChunks + "/" + chunks.size()); System.out.println("Downloaded total: " + downloadedMB + "/" + config.getTotalSizeMB() + " MB"); System.out.println("Simulation finished."); + + double sequentialSeconds = runSequentialSimulation(config); + + System.out.println(); + System.out.println("========== PERFORMANCE COMPARISON =========="); + + System.out.printf( + "Multithreaded Time : %.2f sec%n", + parallelSeconds + ); + + System.out.printf( + "Sequential Time : %.2f sec%n", + sequentialSeconds + ); + + double speedup = sequentialSeconds / parallelSeconds; + + double improvementPercent = + ((sequentialSeconds - parallelSeconds) + / sequentialSeconds) * 100.0; + + double timeSaved = sequentialSeconds - parallelSeconds; + + System.out.printf( + "Time Saved : %.2f sec%n", + timeSaved + ); + + System.out.printf( + "Speedup Factor : %.2fx%n", + speedup + ); + + System.out.printf( + "Performance Gain : %.2f%%%n", + improvementPercent + ); + + if (speedup > 1) + System.out.printf("Result: Multithreading was %.2fx faster than sequential execution.%n", speedup); + else + System.out.println("Result: No measurable improvement from multithreading."); + + System.out.println("============================================"); + } + + private static double runSequentialSimulation(DownloadConfig config) + { + System.out.println(); + System.out.println("=== Sequential Download Simulation ==="); + + List sequentialChunks = + ChunkUtils.createChunks( + config.getTotalSizeMB(), + config.getChunkCount() + ); + + long start = System.currentTimeMillis(); + + for (ChunkStatus chunk : sequentialChunks) + { + DownloadWorker worker = new DownloadWorker(chunk, config); + worker.run(); + } + + long end = System.currentTimeMillis(); + + double seconds = (end - start) / 1000.0; + + System.out.printf("Sequential execution time: %.2f seconds%n", seconds); + + return seconds; } } diff --git a/src/main/java/ProgressMonitor.java b/src/main/java/ProgressMonitor.java index 475c0e0..a1e6d73 100644 --- a/src/main/java/ProgressMonitor.java +++ b/src/main/java/ProgressMonitor.java @@ -16,51 +16,81 @@ public class ProgressMonitor implements Runnable { @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 + long monitorStart = System.currentTimeMillis(); - while (true) { + 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) { - percent = (totalDownloadedMB * 100.0) / totalSizeMB; + double percent = + totalSizeMB == 0 + ? 100.0 + : (totalDownloadedMB * 100.0 / totalSizeMB); + + long elapsedMs = System.currentTimeMillis() - monitorStart; + + double speedMBps = + elapsedMs > 0 + ? totalDownloadedMB / (elapsedMs / 1000.0) + : 0.0; + + double remainingMB = totalSizeMB - totalDownloadedMB; + + double etaSeconds = speedMBps > 0 + ? remainingMB / speedMBps + : 0; + + int barLength = 40; + int filled = (int)((percent / 100.0) * barLength); + StringBuilder bar = new StringBuilder(); + + bar.append("["); + + for (int i = 0; i < barLength; i++) + { + if (i < filled) + bar.append("="); + else + bar.append(" "); } + bar.append("]"); + System.out.printf( - "Progress for %s: %.1f/%.1f MB (%.2f%%), completed chunks: %d/%d%n", - fileName, + "\r%s %.2f%% | %.1f/%.1f MB | Speed: %.2f MB/s | ETA: %.1fs | Chunks %d/%d", + bar, + percent, totalDownloadedMB, (double) totalSizeMB, - percent, + speedMBps, + etaSeconds, completedChunks, chunks.size() ); + if (completedChunks == chunks.size()) + { + System.out.println(); + System.out.println("Download completed successfully."); + return; + } - // 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) + { System.out.println("Progress monitor interrupted."); return; }