Categories: AppledsaDSA Sheet

Job Scheduler Implementation

Problem Statement (Asked By Apple)

Design a job scheduler that accepts a function f and an integer n. The scheduler should execute the function f after a delay of n milliseconds.

Problem link: https://leetcode.com/problems/task-scheduler/description/

Disclaimer: Try solving the problem on your own first! Use this solution only as a reference to learn and improve.


Solution:

We can implement a job scheduler in various ways, so it’s fine if your approach differs. Here, we’ll demonstrate one possible solution.

Initial Approach:

A straightforward way is to start a new thread for each delayed function. The thread will sleep for the specified time and then execute the function. This could look something like this:

Naive Implementation (Inefficient):

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class Scheduler {

    public void delay(Runnable task, int delayInMillis) {
        new Thread(() -> {
            try {
                Thread.sleep(delayInMillis);
                task.run();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }).start();
    }

    public static void main(String[] args) {
        Scheduler scheduler = new Scheduler();
        scheduler.delay(() -> System.out.println("Task executed!"), 1000);
    }
}

While functional, this approach has a significant flaw: each call to delay creates a new thread, which could lead to excessive thread creation and resource exhaustion.

Optimized Approach: Using a Single Dedicated Thread

Instead of creating a new thread for each task, we can use a single dedicated thread to manage all scheduled tasks. We’ll store the tasks in a list along with their scheduled execution times. The thread will periodically check the list for due tasks, execute them, and remove them.

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class Scheduler {
    private final List<Task> tasks = new ArrayList<>();
    private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();

    public Scheduler() {
        executor.scheduleAtFixedRate(this::poll, 0, 10, TimeUnit.MILLISECONDS);
    }

    private synchronized void poll() {
        long currentTime = System.currentTimeMillis();
        List<Task> toRemove = new ArrayList<>();

        for (Task task : tasks) {
            if (task.dueTime <= currentTime) {
                task.runnable.run();
                toRemove.add(task);
            }
        }
        tasks.removeAll(toRemove);
    }

    public synchronized void delay(Runnable task, int delayInMillis) {
        tasks.add(new Task(task, System.currentTimeMillis() + delayInMillis));
    }

    private static class Task {
        Runnable runnable;
        long dueTime;

        Task(Runnable runnable, long dueTime) {
            this.runnable = runnable;
            this.dueTime = dueTime;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Scheduler scheduler = new Scheduler();
        scheduler.delay(() -> System.out.println("Task 1 executed!"), 1000);
        scheduler.delay(() -> System.out.println("Task 2 executed!"), 2000);
    }
}

Explanation:

  1. Task Management:
    • Each task is stored in a Task object, which contains the function (Runnable) and the scheduled execution time (dueTime).
  2. Polling:
    • A single thread periodically checks (polls) the task list.
    • If the current time exceeds the task’s dueTime, the task is executed, and it’s removed from the list.
  3. Adding Tasks:
    • New tasks are added to the list along with their execution time.

Additional Improvements:

  • Use a Priority Queue:
    • Replace the List with a PriorityQueue to efficiently retrieve the next task to run.
  • Condition Variables:
    • Instead of polling, use condition variables to wake up the thread when a new task is added.
  • Thread Pool:
    • Use a thread pool to avoid potential starvation, allowing multiple tasks to run concurrently.

Time Complexity:

  • Polling: O(n) per poll (can be improved to O(logn) with a heap).
  • Adding a task: O(1) with a list, O(logn) with a heap.

Space Complexity: O(n), where n is the number of scheduled tasks.


Did this solution help you understand the concept better? Let me know your thoughts in the comments below! Don’t forget to follow us on Instagram @coderz.py for more DSA tips and solutions to ace your coding interviews.

Recent Posts

What is object oriented design patterns

A design pattern is a reusable solution to a commonly occurring problem in software design. They…

1 month ago

Factory Method Design Pattern in OODP

Factory Method is a creational design pattern that deals with the object creation. It separates…

1 month ago

Find Intersection of Two Singly Linked Lists

You are given two singly linked lists that intersect at some node. Your task is…

7 months ago

Minimum Cost to Paint Houses with K Colors

A builder plans to construct N houses in a row, where each house can be…

7 months ago

Longest Absolute Path in File System Representation

Find the length of the longest absolute path to a file within the abstracted file…

8 months ago

Efficient Order Log Storage

You manage an e-commerce website and need to keep track of the last N order…

8 months ago