Java Thread Pools සහ Executors: කාර්යක්ෂම Thread කළමනාකරණය SC

Java Thread Pools සහ Executors: කාර්යක්ෂම Thread කළමනාකරණය SC

ආයුබෝවන් යාළුවනේ! කොහොමද ඉතින් ඔයාලට? අද අපි කතා කරන්න යන්නේ Java Programming වලදී අපේ applications වල performance සහ responsiveness එක නිකන් නිකන්ම නෙවෙයි, මාර විදියටම වැඩි කරගන්න පුළුවන් සුපිරිම concept එකක් ගැන – ඒ තමයි Thread Pools සහ Executors.

අපි හැමෝම දන්නවා, අද කාලේ තියෙන application එකක් කියන්නේ එක වෙලාවකදී tasks ගොඩක් run වෙන complex systems. උදාහරණයක් විදියට, web server එකක requests දහස් ගාණක් එකපාර handle කරන්න වෙනවා. Desktop application එකකදී user interface එක freezing වෙන්නේ නැතුව, background එකේ heavy operations කරන්න ඕනේ. මේ වගේ scenarios වලදී, multi-threading කියන්නේ අපිට නැතුවම බැරි දෙයක්. ඒත්, threads හරියට manage කරන්නේ නැත්නම්, ඒක ලොකු headaches ගොඩකට හේතු වෙන්න පුළුවන්. ඒ headaches වලට තියෙන හොඳම විසඳුම තමයි Thread Pools කියන්නේ.

Threads මොනවද? සාමාන්‍ය ප්‍රශ්න.

මුලින්ම බලමු thread එකක් කියන්නේ මොකක්ද කියලා. සරලවම කිව්වොත්, thread එකක් කියන්නේ program එකක් ඇතුළේ එකපාර run වෙන්න පුළුවන් වෙනම execution path එකක්. අපේ program එකේ task එකක් run කරන්න ඕන වුණාම, අපි ඒකට thread එකක් පාවිච්චි කරනවා.

හැබැයි, threads කියන්නේ නිදහසේ හදන්න පුළුවන්, කිසිම resource එකක් යන්නේ නැති දෙයක් නෙවෙයි. නිකන් හිතලා බලන්න, ඔයාට එක පාරටම වැඩ 1000ක් කරන්න තියෙනවා කියලා. ඒ වැඩ 1000ටම අලුතින් assistants 1000ක් හොයනවා නම්, ඒ assistants ලා හොයන්න, training දෙන්න, වැඩ ඉවර වුණාම එයාලව අයින් කරන්න ලොකු වෙලාවක්, ශ්‍රමයක් යනවා වගේ තමයි thread එකක් හදනවා කියන්නෙත්.

  • Resource Overhead: Thread එකක් හදනවා කියන්නේ CPU time, memory වගේ system resources ගොඩක් වැය වෙන වැඩක්. Thread එකක් හදන්න, initialize කරන්න, destroy කරන්න ලොකු overhead එකක් යනවා.
  • Context Switching: Thread ගොඩක් එකපාර run කරනකොට, operating system එකට මේ threads අතර මාරු වෙන්න වෙනවා. මේකට කියන්නේ context switching කියලා. Thread ගොඩක් තියෙනකොට මේ context switching එක වැඩි වෙලා, application එකේ performance එක අඩු වෙනවා.
  • Resource Exhaustion: Threads ඕනෑවට වඩා හැදුවොත් system එකේ තියෙන memory, CPU වගේ resources ඉවර වෙලා application එක hang වෙන්න, එහෙමත් නැත්නම් OutOfMemoryError වගේ errors එන්න පුළුවන්.

මේ ප්‍රශ්න වලට විසඳුමක් විදියට තමයි Thread Pools කියන concept එක ආවේ.

Thread Pool එකක් කියන්නේ මොකක්ද?

සරලවම කියනවා නම්, Thread Pool එකක් කියන්නේ කලින්ම හදලා තියාගත්ත (pre-initialized) threads එකතුවක්. මේ threads ටික ready-to-use තත්ත්වයේ තියෙනවා. අපිට task එකක් run කරන්න ඕන වුණාම, අලුතින් thread එකක් හදන්නේ නැතුව, මේ pool එකේ තියෙන thread එකක් අරගෙන task එක ඒකට දීලා run කරනවා. Task එක ඉවර වුණාම, ඒ thread එක pool එකටම ආපහු යනවා, ඊළඟ task එකක් run කරන්න.

Thread Pools වල වාසි:

  • Performance වැඩි වෙනවා: අලුතින් thread හදන එකේ overhead එක නැති වෙන නිසා, tasks ඉක්මනට run කරන්න පුළුවන්.
  • Resource භාවිතය කාර්යක්ෂම වෙනවා: System එකේ තියෙන resources සීමිතයි. Thread Pool එකක් මගින් එකවර run වෙන threads ගාණ control කරන්න පුළුවන් නිසා, resources අධික ලෙස භාවිත වීම වළක්වනවා.
  • Responsiveness වැඩි වෙනවා: Tasks ඉක්මනට execute වෙන නිසා, application එකේ responsiveness එක වැඩි වෙනවා. උදාහරණයක් විදියට, UI එක hang වෙන්නේ නැතුව operations කරන්න පුළුවන්.
  • Thread Management පහසු වෙනවා: Thread lifecycle එක (creation, execution, termination) ගැන අපිට හිතන්න අවශ්‍ය නැහැ. Thread Pool එක ඒ හැමදේම automate කරනවා.

Java වල Executors Framework එක

Java 5 වලින් පස්සේ java.util.concurrent package එකට අලුතින් එකතු වුණ Executors Framework එක තමයි Java වලදී Thread Pools හරියටම manage කරන්න අපිට තියෙන ප්‍රධානම tool එක. මේ framework එක මගින් threads handle කරන එක, tasks submit කරන එක වගේ දේවල් ගොඩක් පහසු කරලා තියෙනවා.

මේ framework එකේ ප්‍රධානම interface එක තමයි ExecutorService කියන්නේ. ඒ වගේම, Executors කියන utility class එක අපිට විවිධ වර්ගයේ ExecutorService instances පහසුවෙන් හදාගන්න පුදමයි.

ප්‍රධාන ExecutorService වර්ග:

  • Executors.newFixedThreadPool(int nThreads):
    • මේකෙන් හදන්නේ fixed size එකක් තියෙන Thread Pool එකක්. කියමුකෝ ඔයා 5 කියලා දුන්නොත්, thread 5ක් මේ pool එකේ හැදෙනවා.
    • ඕනෑම වෙලාවකදී මේ pool එකේ threads ගාණ වෙනස් වෙන්නේ නැහැ. Task එකක් ආවොත්, free thread එකක් තියෙනවා නම්, ඒක ඒ task එක run කරනවා. නැත්නම්, task එක queue එකක ඉඳලා thread එකක් free වෙනකම් ඉන්නවා.
    • CPU-bound tasks, එහෙමත් නැත්නම් එක පාරටම run වෙන්න පුළුවන් tasks ගාණ සීමිත තැන් වලට මේක ගොඩක් සුදුසුයි.
  • Executors.newCachedThreadPool():
    • මේක ගතික (dynamic) pool එකක්. අවශ්‍යතාව අනුව threads හදනවා.
    • Task එකක් ආවොත්, free thread එකක් නැත්නම්, අලුත් thread එකක් හදලා ඒ task එකට දෙනවා.
    • Task එකක් ඉවර වුණාම, ඒ thread එක pool එකටම යනවා. ඒ thread එක විනාඩි 60ක් වගේ වෙලාවක් idle එකේ හිටියොත්, ඒක pool එකෙන් ඉවත් කරනවා.
    • Short-lived, bursty tasks, එහෙමත් නැත්නම් tasks ගොඩක් එක පාරටම ඇවිත් ඉක්මනට ඉවර වෙන තැන් වලට මේක සුදුසුයි.
  • Executors.newSingleThreadExecutor():
    • මේකෙන් හදන්නේ එක thread එකක් විතරක් තියෙන pool එකක්.
    • මේකේ තියෙන වාසිය තමයි, tasks එකින් එක (sequentially) execute වෙන නිසා, tasks අතරේ race conditions වගේ ප්‍රශ්න අඩු කරගන්න පුළුවන් වෙන එක.
    • Log file එකකට දත්ත ලියන එක වගේ, order එක වැදගත් තැන් වලට මේක පාවිච්චි කරන්න පුළුවන්.
  • Executors.newScheduledThreadPool(int corePoolSize):
    • මේකෙන් tasks specify කරපු වෙලාවකට පස්සේ, එහෙමත් නැත්නම් period එකකට පස්සේ run කරන්න පුළුවන්. (Scheduled tasks)
    • Cron jobs වගේ දේවල් වලට මේක පාවිච්චි කරන්න පුළුවන්.

ExecutorService එක පාවිච්චි කරන්නේ කොහොමද?

හරි, දැන් අපි බලමු ප්‍රායෝගිකව මේ ExecutorService එක අපේ code එකේ පාවිච්චි කරන්නේ කොහොමද කියලා.

tasks submit කරන්න ප්‍රධාන ක්‍රම දෙකක් තියෙනවා: Runnable සහ Callable.

1. Runnable Tasks Submit කිරීම

Runnable tasks කියන්නේ කිසිම value එකක් return කරන්නේ නැති tasks. ඒ වගේම, exception එකක් throw කරන්නත් බෑ (ඒකට handle කරන්න ඕනේ).

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

public class RunnableExample {
    public static void main(String[] args) {
        // Fixed size thread pool එකක් හදාගමු threads 3කින්
        ExecutorService executor = Executors.newFixedThreadPool(3);

        System.out.println("Tasks submit කිරීම ආරම්භ වේ...");

        // Tasks 5ක් submit කරමු
        for (int i = 0; i < 5; i++) {
            final int taskId = i; // Lambda expression එකට final variable එකක් අවශ්‍යයි
            executor.submit(() -> {
                System.out.println("Task " + taskId + " ආරම්භ විය. Thread: " + Thread.currentThread().getName());
                try {
                    // පොඩි වෙලාවක් නිදි කරවමු, task එක run වෙනවා වගේ පේන්න
                    Thread.sleep((long) (Math.random() * 2000)); // 0-2 seconds
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt(); // Interrupt flag එක set කරන්න
                    System.out.println("Task " + taskId + " interrupt විය.");
                }
                System.out.println("Task " + taskId + " අවසන් විය. Thread: " + Thread.currentThread().getName());
            });
        }

        System.out.println("සියලු tasks submit කරන ලදී. Executor shutdown කිරීමට සූදානම්.");

        // ExecutorService එක gracefully shutdown කරන්න ඕනේ
        // නව tasks submit කිරීම නවත්වයි, නමුත් දැනට run වන tasks ඉවර වෙනකම් බලයි.
        executor.shutdown();

        try {
            // සියලු tasks අවසන් වන තෙක් පොඩි වෙලාවක් (උදා: 60 seconds) බලන්න
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                // වෙලාව ඉවර වුණා නම්, ඉතුරු tasks interrupt කරලා shutdown කරන්න
                System.err.println("Tasks කාලය අවසන් විය, බලහත්කාරයෙන් shutdown කරයි.");
                executor.shutdownNow();
            }
        } catch (InterruptedException e) {
            // Current thread එක interrupt වුණොත්, executor එකත් shutdown කරන්න
            System.err.println("Main thread interrupt විය, executor බලහත්කාරයෙන් shutdown කරයි.");
            executor.shutdownNow();
        }

        System.out.println("Executor අවසන් විය.");
    }
}

2. Callable Tasks Submit කිරීම සහ Future භාවිතය

Callable tasks වලට value එකක් return කරන්න පුළුවන්. ඒ වගේම exception එකක් throw කරන්නත් පුළුවන්. Callable task එකකින් return වෙන value එක ගන්න අපි Future කියන interface එක පාවිච්චි කරනවා.

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

public class CallableExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);

        System.out.println("Callable tasks submit කිරීම ආරම්භ වේ...");

        // Callable task එකක් define කරමු
        Callable<String> dataProcessingTask = () -> {
            System.out.println("Data Processing Task ආරම්භ විය. Thread: " + Thread.currentThread().getName());
            Thread.sleep(3000); // Process කරන්න පොඩි වෙලාවක් ගනී කියලා හිතමු
            System.out.println("Data Processing Task අවසන් විය.");
            return "Processed Data Successfully!"; // Result එක return කරනවා
        };

        Callable<Integer> calculationTask = () -> {
            System.out.println("Calculation Task ආරම්භ විය. Thread: " + Thread.currentThread().getName());
            Thread.sleep(1500); // Calculate කරන්න පොඩි වෙලාවක් ගනී කියලා හිතමු
            int result = 10 * 20;
            System.out.println("Calculation Task අවසන් විය.");
            return result;
        };

        // Tasks submit කරලා Future object ගන්න
        Future<String> futureData = executor.submit(dataProcessingTask);
        Future<Integer> futureCalculation = executor.submit(calculationTask);

        // Results ලබාගැනීම (get() method එක block වෙනවා result එනකම්)
        try {
            System.out.println("Data Processing Result: " + futureData.get());
            System.out.println("Calculation Result: " + futureCalculation.get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

        executor.shutdown();
        try {
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                executor.shutdownNow();
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
        }
        System.out.println("Executor අවසන් විය.");
    }
}

Future සහ Asynchronous Task Management

Future object එක කියන්නේ submit කරපු task එකක result එක හෝ ඒ task එකේ තත්ත්වය (status) ගැන විස්තර දැනගන්න පුළුවන් representation එකක්. future.get() කියන method එකෙන් task එකේ result එක ගන්න පුළුවන්. හැබැයි මේ get() method එක block වෙනවා, ඒ කියන්නේ task එක ඉවර වෙනකම් මේ method එකේ execution එක නවතිනවා. ඒ නිසා, get() call එක කරනකොට ඒ ගැන සැලකිලිමත් වෙන්න ඕනේ, main thread එක block නොවෙන්න.

Future interface එකේ තව method කිහිපයක් තියෙනවා:

  • isDone(): Task එක ඉවරද කියලා බලන්න පුළුවන් (true/false).
  • isCancelled(): Task එක cancel කරලාද කියලා බලන්න පුළුවන්.
  • cancel(boolean mayInterruptIfRunning): Task එක cancel කරන්න පුළුවන්.

මේවා පාවිච්චි කරලා අපිට asynchronous tasks වඩාත් හොඳින් manage කරන්න පුළුවන්.

නැවත සාරාංශ කරන්න

ඉතින් යාළුවනේ, Java වල applications වල performance, responsiveness, සහ resource utilization වගේ දේවල් වැඩි දියුණු කරන්න Thread Pools සහ Executors Framework කියන්නේ අත්‍යවශ්‍ය tools set එකක්. නිවැරදිව මේවා පාවිච්චි කරන එකෙන් අපිට scalable, efficient applications හදන්න පුළුවන්.

මුලින්ම මේ concepts ටික පොඩ්ඩක් සංකීර්ණ වගේ පෙනුණත්, practice කරනකොට හරිම සරලයි. අනිවාර්යෙන්ම මේ code examples ටික ඔයාගේ IDE එකේ run කරලා බලන්න. පොඩි පොඩි වෙනස්කම් කරලා මොකද වෙන්නේ කියලා බලන්න. ඒකෙන් ඔයාට මේ ගැන හොඳ අවබෝධයක් ලැබෙයි.

මොනවා හරි ප්‍රශ්න තියෙනවා නම්, එහෙමත් නැත්නම් මේ ගැන ඔයාගේ අදහස් තියෙනවා නම්, පහළ තියෙන comment section එකේ අපිට කියන්න. අපි කතා කරමු!

ඊළඟ ලිපියෙන් හම්බවෙමු! සුබ දවසක්!