diff --git a/exercises/practice/parallel-letter-frequency/.approaches/config.json b/exercises/practice/parallel-letter-frequency/.approaches/config.json new file mode 100644 index 000000000..dad02090c --- /dev/null +++ b/exercises/practice/parallel-letter-frequency/.approaches/config.json @@ -0,0 +1,27 @@ +{ + "introduction": { + "authors": [ + "masiljangajji" + ] + }, + "approaches": [ + { + "uuid": "dee2a79d-3e64-4220-b99f-55667549c12c", + "slug": "fork-join", + "title": "Fork/Join", + "blurb": "Parallel Computation Using Fork/Join", + "authors": [ + "masiljangajji" + ] + }, + { + "uuid": "75e9e93b-4da4-4474-8b6e-3c0cb9b3a9bb", + "slug": "parallel-stream", + "title": "Parallel Stream", + "blurb": "Parallel Computation Using Parallel Stream", + "authors": [ + "masiljangajji" + ] + } + ] +} diff --git a/exercises/practice/parallel-letter-frequency/.approaches/fork-join/content.md b/exercises/practice/parallel-letter-frequency/.approaches/fork-join/content.md new file mode 100644 index 000000000..de2cd7307 --- /dev/null +++ b/exercises/practice/parallel-letter-frequency/.approaches/fork-join/content.md @@ -0,0 +1,91 @@ +# `Fork/Join` + +```java +import java.util.Map; +import java.util.List; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.RecursiveTask; + +class ParallelLetterFrequency { + + List texts; + ConcurrentMap letterCount; + + ParallelLetterFrequency(String[] texts) { + this.texts = List.of(texts); + letterCount = new ConcurrentHashMap<>(); + } + + Map countLetters() { + if (texts.isEmpty()) { + return letterCount; + } + + ForkJoinPool forkJoinPool = new ForkJoinPool(); + forkJoinPool.invoke(new LetterCountTask(texts, 0, texts.size(), letterCount)); + forkJoinPool.shutdown(); + + return letterCount; + } + + private static class LetterCountTask extends RecursiveTask { + private static final int THRESHOLD = 10; + private final List texts; + private final int start; + private final int end; + private final ConcurrentMap letterCount; + + LetterCountTask(List texts, int start, int end, ConcurrentMap letterCount) { + this.texts = texts; + this.start = start; + this.end = end; + this.letterCount = letterCount; + } + + @Override + protected Void compute() { + if (end - start <= THRESHOLD) { + for (int i = start; i < end; i++) { + for (char c : texts.get(i).toLowerCase().toCharArray()) { + if (Character.isAlphabetic(c)) { + letterCount.merge(c, 1, Integer::sum); + } + } + } + } else { + int mid = (start + end) / 2; + LetterCountTask leftTask = new LetterCountTask(texts, start, mid, letterCount); + LetterCountTask rightTask = new LetterCountTask(texts, mid, end, letterCount); + invokeAll(leftTask, rightTask); + } + return null; + } + } +} +``` + +Using [`ConcurrentHashMap`][ConcurrentHashMap] ensures that frequency counting and updates are safely handled in a parallel environment. + +If there are no strings, a validation step prevents unnecessary processing. + +A [`ForkJoinPool`][ForkJoinPool] is then created. +The core of [`ForkJoinPool`][ForkJoinPool] is the Fork/Join mechanism, which divides tasks into smaller units and processes them in parallel. + +`THRESHOLD` is the criterion for task division. +If the range of texts exceeds the `THRESHOLD`, the task is divided into two subtasks, and [`invokeAll(leftTask, rightTask)`][invokeAll] is called to execute both tasks in parallel. +Each subtask in `LetterCountTask` will continue calling `compute()` (via `invokeAll(leftTask, rightTask)`) to divide itself further until the range is smaller than or equal to the `THRESHOLD`. +For tasks that are within the `THRESHOLD`, letter frequency is calculated. + +The [`Character.isAlphabetic`][isAlphabetic] method identifies all characters classified as alphabetic in Unicode, covering characters from various languages like English, Korean, Japanese, Chinese, etc., returning `true`. +Non-alphabetic characters, including numbers, special characters, and spaces, return `false`. + +Additionally, since uppercase and lowercase letters are treated as the same character (e.g., `A` and `a`), each character is converted to lowercase. + +After updating letter frequencies, the final map is returned. + +[ConcurrentHashMap]: https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ConcurrentHashMap.html +[ForkJoinPool]: https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ForkJoinPool.html +[isAlphabetic]: https://docs.oracle.com/javase/8/docs/api/java/lang/Character.html#isAlphabetic-int- +[invokeAll]: https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html diff --git a/exercises/practice/parallel-letter-frequency/.approaches/fork-join/snippet.txt b/exercises/practice/parallel-letter-frequency/.approaches/fork-join/snippet.txt new file mode 100644 index 000000000..7e782eb30 --- /dev/null +++ b/exercises/practice/parallel-letter-frequency/.approaches/fork-join/snippet.txt @@ -0,0 +1,7 @@ +for (int i = start; i < end; i++) { + for (char c : texts.get(i).toLowerCase().toCharArray()) { + if (Character.isAlphabetic(c)) { + letterCount.merge(c, 1, Integer::sum); + } + } +} \ No newline at end of file diff --git a/exercises/practice/parallel-letter-frequency/.approaches/introduction.md b/exercises/practice/parallel-letter-frequency/.approaches/introduction.md new file mode 100644 index 000000000..8ca8fbf92 --- /dev/null +++ b/exercises/practice/parallel-letter-frequency/.approaches/introduction.md @@ -0,0 +1,142 @@ +# Introduction + +There are multiple ways to solve the Parallel Letter Frequency problem. +One approach is to use [`Stream.parallelStream`][stream], and another involves using [`ForkJoinPool`][ForkJoinPool]. + +## General guidance + +To count occurrences of items, a map data structure is often used, though arrays and lists can work as well. +A [`map`][map], being a key-value pair structure, is suitable for recording frequency by incrementing the value for each key. +If the data being counted has a limited range (e.g., `Characters` or `Integers`), an `int[] array` or [`List`][list] can be used to record frequencies. + +Parallel processing typically takes place in a multi-[`thread`][thread] environment. +The Java 8 [`stream`][stream] API provides methods that make parallel processing easier, including the [`parallelStream()`][stream] method. +With [`parallelStream()`][stream], developers can use the [`ForkJoinPool`][ForkJoinPool] model for workload division and parallel execution, without the need to manually manage threads or create custom thread pools. + +The [`ForkJoinPool`][ForkJoinPool] class, optimized for dividing and managing tasks, makes parallel processing efficient. +However, [`parallelStream()`][stream] uses the common [`ForkJoinPool`][ForkJoinPool] by default, meaning multiple [`parallelStream`][stream] instances share the same thread pool unless configured otherwise. + +As a result, parallel streams may interfere with each other when sharing this thread pool, potentially affecting performance. +Although this doesn’t directly impact solving the Parallel Letter Frequency problem, it may introduce issues when thread pool sharing causes conflicts in other applications. +Therefore, a custom [`ForkJoinPool`][ForkJoinPool] approach is also provided below. + +## Approach: `parallelStream` + +```java +import java.util.Map; +import java.util.List; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ConcurrentHashMap; + +class ParallelLetterFrequency { + + List texts; + ConcurrentMap letterCount; + + ParallelLetterFrequency(String[] texts) { + this.texts = List.of(texts); + letterCount = new ConcurrentHashMap<>(); + } + + Map countLetters() { + if (!letterCount.isEmpty() || texts.isEmpty()) { + return letterCount; + } + texts.parallelStream().forEach(text -> { + for (char c: text.toLowerCase().toCharArray()) { + if (Character.isAlphabetic(c)) { + letterCount.merge(c, 1, Integer::sum); + } + } + }); + return letterCount; + } + +} +``` + +For more information, check the [`parallelStream` approach][approach-parallel-stream]. + +## Approach: `Fork/Join` + +```java +import java.util.Map; +import java.util.List; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.RecursiveTask; + +class ParallelLetterFrequency { + + List texts; + ConcurrentMap letterCount; + + ParallelLetterFrequency(String[] texts) { + this.texts = List.of(texts); + letterCount = new ConcurrentHashMap<>(); + } + + Map countLetters() { + if (!letterCount.isEmpty() || texts.isEmpty()) { + return letterCount; + } + + ForkJoinPool forkJoinPool = new ForkJoinPool(); + forkJoinPool.invoke(new LetterCountTask(texts, 0, texts.size(), letterCount)); + forkJoinPool.shutdown(); + + return letterCount; + } + + private static class LetterCountTask extends RecursiveTask { + private static final int THRESHOLD = 10; + private final List texts; + private final int start; + private final int end; + private final ConcurrentMap letterCount; + + LetterCountTask(List texts, int start, int end, ConcurrentMap letterCount) { + this.texts = texts; + this.start = start; + this.end = end; + this.letterCount = letterCount; + } + + @Override + protected Void compute() { + if (end - start <= THRESHOLD) { + for (int i = start; i < end; i++) { + for (char c : texts.get(i).toLowerCase().toCharArray()) { + if (Character.isAlphabetic(c)) { + letterCount.merge(c, 1, Integer::sum); + } + } + } + } else { + int mid = (start + end) / 2; + LetterCountTask leftTask = new LetterCountTask(texts, start, mid, letterCount); + LetterCountTask rightTask = new LetterCountTask(texts, mid, end, letterCount); + invokeAll(leftTask, rightTask); + } + return null; + } + } +} + +``` + +For more information, check the [`fork/join` approach][approach-fork-join]. + +## Which approach to use? + +When tasks are simple or do not require a dedicated thread pool (such as in this case), the [`parallelStream`][stream] approach is recommended. +However, if the work is complex or there is a need to isolate thread pools from other concurrent tasks, the [`ForkJoinPool`][ForkJoinPool] approach is preferable. + +[thread]: https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html +[stream]: https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html +[ForkJoinPool]: https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ForkJoinPool.html +[map]: https://docs.oracle.com/javase/8/docs/api/?java/util/Map.html +[list]: https://docs.oracle.com/javase/8/docs/api/?java/util/List.html +[approach-parallel-stream]: https://exercism.org/tracks/java/exercises/parallel-letter-frequency/approaches/parallel-stream +[approach-fork-join]: https://exercism.org/tracks/java/exercises/parallel-letter-frequency/approaches/fork-join diff --git a/exercises/practice/parallel-letter-frequency/.approaches/parallel-stream/content.md b/exercises/practice/parallel-letter-frequency/.approaches/parallel-stream/content.md new file mode 100644 index 000000000..6b532b767 --- /dev/null +++ b/exercises/practice/parallel-letter-frequency/.approaches/parallel-stream/content.md @@ -0,0 +1,49 @@ +# `parallelStream` + +```java +import java.util.Map; +import java.util.List; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ConcurrentHashMap; + +class ParallelLetterFrequency { + + List texts; + ConcurrentMap letterCount; + + ParallelLetterFrequency(String[] texts) { + this.texts = List.of(texts); + letterCount = new ConcurrentHashMap<>(); + } + + Map countLetters() { + if (texts.isEmpty()) { + return letterCount; + } + texts.parallelStream().forEach(text -> { + for (char c: text.toLowerCase().toCharArray()) { + if (Character.isAlphabetic(c)) { + letterCount.merge(c, 1, Integer::sum); + } + } + }); + return letterCount; + } + +} +``` + +Using [`ConcurrentHashMap`][ConcurrentHashMap] ensures that frequency counting and updates are safely handled in a parallel environment. + +If there are no strings to process, a validation step avoids unnecessary computation. + +To calculate letter frequency, a parallel stream is used. +The [`Character.isAlphabetic`][isAlphabetic] method identifies all characters classified as alphabetic in Unicode, covering characters from various languages like English, Korean, Japanese, Chinese, etc., returning `true`. +Non-alphabetic characters, including numbers, special characters, and spaces, return `false`. + +Since we treat uppercase and lowercase letters as the same character (e.g., `A` and `a`), characters are converted to lowercase. + +After updating letter frequencies, the final map is returned. + +[ConcurrentHashMap]: https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ConcurrentHashMap.html +[isAlphabetic]: https://docs.oracle.com/javase/8/docs/api/java/lang/Character.html#isAlphabetic-int- diff --git a/exercises/practice/parallel-letter-frequency/.approaches/parallel-stream/snippet.txt b/exercises/practice/parallel-letter-frequency/.approaches/parallel-stream/snippet.txt new file mode 100644 index 000000000..9cbb9cffa --- /dev/null +++ b/exercises/practice/parallel-letter-frequency/.approaches/parallel-stream/snippet.txt @@ -0,0 +1,7 @@ +texts.parallelStream().forEach(text -> { + for (char c: text.toLowerCase().toCharArray()) { + if (Character.isAlphabetic(c)) { + letterCount.merge(c, 1, Integer::sum); + } + } +}); \ No newline at end of file