Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

<!-- Your content goes here: -->



Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this related to the PR. Could you please undo this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this related to the PR. Could you please undo this change?

the following error will occur.

.github/PULL_REQUEST_TEMPLATE.md:5 MD012/no-multiple-blanks Multiple consecutive blank lines [Expected: 1; Actual: 2]
.github/PULL_REQUEST_TEMPLATE.md:6 MD012/no-multiple-blanks Multiple consecutive blank lines [Expected: 1; Actual: 3]

Should we go ahead and cancel it anyway?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think we should. I've just tried running our Markdown Lint action on our main branch (before your change). I think the rules are a bit different with that one.

<!-- DO NOT EDIT BELOW THIS LINE! -->
---

Expand Down
Original file line number Diff line number Diff line change
@@ -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"
]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# `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<String> texts;
ConcurrentMap<Character, Integer> letterCount;

ParallelLetterFrequency(String[] texts) {
this.texts = List.of(texts);
letterCount = new ConcurrentHashMap<>();
}

Map<Character, Integer> 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<Void> {
private static final int THRESHOLD = 10;
private final List<String> texts;
private final int start;
private final int end;
private final ConcurrentMap<Character, Integer> letterCount;

LetterCountTask(List<String> texts, int start, int end, ConcurrentMap<Character, Integer> 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
THRESHOLD is the criterion for task division.
`THRESHOLD` is the criterion for task division.


If the range of texts exceeds the THRESHOLD, the task is divided into two subtasks, and [`invokeAll`][invokeAll](leftTask, rightTask) is called to execute both tasks in parallel.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
If the range of texts exceeds the THRESHOLD, the task is divided into two subtasks, and [`invokeAll`][invokeAll](leftTask, rightTask) is called to execute both tasks in parallel.
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() to divide itself further until the range is smaller than or equal to the threshold.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Each subtask in LetterCountTask will continue calling compute() to divide itself further until the range is smaller than or equal to the threshold.
Each subtask in `LetterCountTask` will continue calling `compute()` 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 is used to identify all characters classified as alphabetic in Unicode, covering various languages like English, Korean, Japanese, Chinese, etc., returning true for alphabetic characters and false for numbers, special characters, spaces, and others.

Additionally, since uppercase and lowercase letters are treated as the same character (e.g., A and a), each character is converted to lowercase.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Additionally, since uppercase and lowercase letters are treated as the same character (e.g., A and a), each character is converted to lowercase.
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/6/docs/api/java/util/concurrent/ExecutorService.html
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make the links consistent, could you use the Java 8 Javadocs for the invokeAll link?

Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Introduction

There are multiple ways to solve the Parallel Letter Frequency problem.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest removing the empty line to turn back into paragraph.

Suggested change

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

A [`map`][map], being a key-value pair structure, is suitable for recording frequency by incrementing the value for each key.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

If the data being counted has a limited range (e.g., characters or integers), an `int[] array` or [`List<Integer>`][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() method.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

With parallelStream(), developers can use the ForkJoinPool model for workload division and parallel execution, without the need to manually manage threads or create custom thread pools.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
With parallelStream(), developers can use the ForkJoinPool model for workload division and parallel execution, without the need to manually manage threads or create custom thread pools.
With `parallelStream()`, developers can use the 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() uses the common ForkJoinPool by default, meaning multiple parallelStream instances share the same thread pool unless configured otherwise.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
However, parallelStream() uses the common ForkJoinPool by default, meaning multiple parallelStream instances share the same thread pool unless configured otherwise.
However, `parallelStream()` uses the common `ForkJoinPool` by default, meaning multiple `parallelStream` 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 approach is also provided below.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Therefore, a custom ForkJoinPool approach is also provided below.
Therefore, a custom `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<String> texts;
ConcurrentMap<Character, Integer> letterCount;

ParallelLetterFrequency(String[] texts) {
this.texts = List.of(texts);
letterCount = new ConcurrentHashMap<>();
}

Map<Character, Integer> 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<String> texts;
ConcurrentMap<Character, Integer> letterCount;

ParallelLetterFrequency(String[] texts) {
this.texts = List.of(texts);
letterCount = new ConcurrentHashMap<>();
}

Map<Character, Integer> 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<Void> {
private static final int THRESHOLD = 10;
private final List<String> texts;
private final int start;
private final int end;
private final ConcurrentMap<Character, Integer> letterCount;

LetterCountTask(List<String> texts, int start, int end, ConcurrentMap<Character, Integer> 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 approach is recommended.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
When tasks are simple or do not require a dedicated thread pool (such as in this case), the parallelStream approach is recommended.
When tasks are simple or do not require a dedicated thread pool (such as in this case), the `parallelStream` approach is recommended.

However, if the work is complex or there is a need to isolate thread pools from other concurrent tasks, the ForkJoinPool approach is preferable.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
However, if the work is complex or there is a need to isolate thread pools from other concurrent tasks, the ForkJoinPool approach is preferable.
However, if the work is complex or there is a need to isolate thread pools from other concurrent tasks, the `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
Original file line number Diff line number Diff line change
@@ -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<String> texts;
ConcurrentMap<Character, Integer> letterCount;

ParallelLetterFrequency(String[] texts) {
this.texts = List.of(texts);
letterCount = new ConcurrentHashMap<>();
}

Map<Character, Integer> 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Since we treat uppercase and lowercase letters as the same character (e.g., A and a), characters are converted to lowercase.
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-
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
texts.parallelStream().forEach(text -> {
for (char c: text.toLowerCase().toCharArray()) {
if (Character.isAlphabetic(c)) {
letterCount.merge(c, 1, Integer::sum);
}
}
});