Parallelism

Dev.Hammy·2024년 1월 18일

Java API

목록 보기
14/15

병렬 컴퓨팅(Parallel computing)에는 문제를 하위 문제로 나누고 해당 문제를 동시에(병렬로, 각 하위 문제가 별도의 스레드에서 실행됨) 해결한 다음 솔루션 결과를 하위 문제에 결합하는 작업이 포함됩니다. Java SE는 포크(fork)/조인(join) 프레임워크를 제공하므로 애플리케이션에서 병렬 컴퓨팅을 보다 쉽게 구현할 수 있습니다. 그러나 이 프레임워크를 사용하면 문제를 세분화(subdivided)(분할(partitioned))하는 방법을 지정해야 합니다. 집계 작업을 통해 Java 런타임은 솔루션 분할 및 결합을 수행합니다.

컬렉션을 사용하는 애플리케이션에서 병렬 처리를 구현할 때의 한 가지 어려움은 컬렉션이 스레드로부터 안전하지 않다(not thread-safe)는 것입니다. 즉, 스레드 간섭(thread interference)이나 메모리 일관성(memory consistency) 오류가 발생하지 않으면(without introducing errors) 여러 스레드가 컬렉션을 조작(manipulate)할 수 없다는 의미입니다. 컬렉션 프레임워크는 임의 컬렉션에 자동 동기화(automatic synchronization)를 추가하여 스레드로부터 안전하게 만드는(thread-safe) 동기화 래퍼(synchronization wrappers)를 제공합니다. 그러나 동기화로 인해 스레드 경합(thread contention)이 발생합니다. 스레드 경합은 스레드가 병렬로 실행되는 것을 방지하기 때문에 피하고 싶습니다. 집계 작업 및 병렬 스트림을 사용하면 작업하는 동안 컬렉션을 수정하지 않는 한 스레드로부터 안전하지 않은 컬렉션으로 병렬 처리를 구현할 수 있습니다.


  1. 스레드 간섭 (Thread Interference):

    • 스레드 간섭은 여러 스레드가 공유된 자원(변수, 객체 등)에 동시에 접근하여 수정하려고 할 때 발생합니다.
    • 여러 스레드가 동시에 공유된 자원에 값을 변경하려고 할 때, 각 스레드의 작업이 서로 간섭하여 예상치 못한 결과를 초래할 수 있습니다.
    • 스레드 간의 동기화가 이루어지지 않으면, 스레드 간섭으로 인해 데이터 일관성이 깨질 수 있습니다.
  2. 스레드 경합 (Thread Contention):

    • 스레드 경합은 여러 스레드가 어떤 자원을 경쟁적으로 획득하려고 할 때 발생합니다.
    • 일반적으로는 여러 스레드가 공유 자원(락, 리소스 등)에 대한 동시 접근을 경쟁할 때, 경합이 발생합니다.
    • 경합이 발생하면 락을 획득한 스레드만이 해당 자원을 사용할 수 있고, 나머지는 기다려야 합니다.
  3. 메모리 일관성 오류 (Memory Consistency Error):

    • 메모리 일관성 오류는 다중 스레드 환경에서 메모리의 변경 사항이 일관되게 반영되지 않을 때 발생합니다.
    • 다중 스레드에서 공유된 데이터를 변경할 때, 캐시 등의 이유로 인해 한 스레드의 변경 사항이 다른 스레드에 즉시 반영되지 않을 수 있습니다.
    • 메모리 일관성을 보장하기 위해 동기화 메커니즘(락, volatile 등)을 사용해야 하며, 그렇지 않으면 예상치 못한 결과가 발생할 수 있습니다.

이러한 문제들은 다중 스레드 환경에서의 동기화와 관련이 있으며, 적절한 동기화 메커니즘을 사용하여 데이터 일관성을 보장해야 합니다. Java에서는 synchronized 키워드, ReentrantLock, volatile 등이 동기화를 위한 메커니즘으로 사용됩니다.


병렬 처리는 작업을 직렬로(serially) 수행하는 것보다 자동으로 빠르지는 않습니다. 단, 충분한 데이터와 프로세서 코어가 있는 경우에는 가능합니다. 집계 작업을 사용하면 병렬 처리를 더 쉽게 구현할 수 있지만 애플리케이션이 병렬 처리에 적합한지 확인하는 것은 여전히 사용자의 책임입니다.

이 섹션에서는 다음 주제를 다룹니다.

  • Executing Streams in Parallel 병렬로 스트림 실행
  • Concurrent Reduction
  • Ordering
  • Side Effects
    • Laziness
    • Interference
    • Stateful Lambda Expressions 상태 저장 람다 표현식

ParallelismExamples 예제에서 이 섹션에 설명된 코드 발췌문(excerpts)을 찾을 수 있습니다.

Executing Streams in Parallel

스트림을 직렬 또는 병렬로 실행할 수 있습니다. 스트림이 병렬로 실행되면 Java 런타임은 스트림을 여러 하위 스트림(substreams)으로 분할(partitions)합니다. 집계 작업은 이러한 하위 스트림을 반복(iterate)하고 병렬로 처리한 다음 결과를 결합합니다.

스트림을 생성하면 달리 지정하지 않는 한 항상 직렬(serial) 스트림입니다. 병렬 스트림을 생성하려면 Collection.parallelStream 작업을 호출합니다. 또는 BaseStream.parallel 작업을 호출합니다. 예를 들어 다음 statement은 모든 남성 구성원의 평균 연령을 동시에(in parallel) 계산합니다.

double average = roster.parallelStream().filter(p -> p.getGender() == Person.Sex.MALE).mapToInt(Person::getAge).average().getAsDouble();

Concurrent Reduction

구성원을 성별로 그룹화하는 다음 예제(Reduction 섹션에 설명되어 있음)를 다시 고려하세요. 이 예에서는 roster 컬렉션을 맵으로 줄이는 collect 작업을 호출합니다.

Map<Person.Sex, List<Person>> byGender = roster.stream().collect(Collectors.groupingBy(Person::getGender));

다음은 병렬로 동등한 것입니다:

ConcurrentMap<Person.Sex, List<Person>> byGender = roster.parallelStream().collect(Collectors.groupingByConcurrent(Person::getGender));

이를 concurrent reduction라고 합니다. collect 작업이 포함된 특정 파이프라인에 대해 다음 사항이 모두 참인 경우 Java 런타임은 concurrent reduction를 수행합니다.

  • stream은 parallel합니다.
  • collect 작업의 매개변수인 collector는 Collector.Characteristics.CONCURRENT 특성을 갖습니다. collector의 특성을 확인(determine)하려면 Collector.characteristics 메서드를 호출합니다.
  • 스트림이 순서가 지정되지 않았거나 collector에 Collector.Characteristics.UNORDERED 특성이 지정되어 있을 수 있다. 스트림의 순서가 지정되지 않았는지 확인하려면 BaseStream.unordered 작업을 호출하세요.

참고: 이 예에서는 Map 대신 ConcurrentMap의 인스턴스를 반환하고 groupingBy 대신 groupingByConcurrent 작업을 호출합니다. (ConcurrentMap에 대한 자세한 내용은 Concurrent Collections 섹션을 참조하세요.) groupingByConcurrent 작업과 달리 groupingBy 작업은 병렬 스트림에서 제대로 수행되지 않습니다. (이것은 두 개의 맵을 키로 병합하여 작동하기 때문에 계산 비용이 많이 듭니다.(computationally expensive)) 마찬가지로 Collectors.toConcurrentMap 작업은 Collectors.toMap 작업보다 병렬 스트림에서 더 나은 성능을 발휘합니다.


Collection, Collections, Collectors, Collector

"Collection," "Collections," "Collectors," 그리고 "Collector"는 Java 프로그래밍 언어에서 다루는 데이터 구조 및 관련된 클래스와 인터페이스를 나타냅니다. 각각의 용어에 대한 설명은 다음과 같습니다:

  1. Collection:

    • 설명: Collection은 자바 컬렉션 프레임워크의 가장 상위 인터페이스입니다. 모든 컬렉션 클래스들은 이 인터페이스를 구현합니다.
    • 특징: 요소들을 담는 일반적인 컨테이너로, 리스트(List), 세트(Set), 큐(Queue) 등 다양한 종류의 자료 구조를 표현합니다.
  2. Collections:

    • 설명: Collections는 자바에서 제공하는 유틸리티 클래스로, 컬렉션 객체에 대한 메서드들을 제공합니다. 이 클래스는 주로 정적 메서드로 이루어져 있습니다.
    • 특징: 정렬, 검색, 수정 등 다양한 작업을 수행하는 메서드들을 포함하고 있습니다. 주로 컬렉션 객체를 조작하고 관리하는데 사용됩니다.
  3. Collectors:

    • 설명: Collectors는 자바 8부터 추가된 클래스로, 스트림의 요소들을 수집하고 그룹화하는데 사용됩니다. 주로 스트림 파이프라인에서 최종 결과를 생성하는데 활용됩니다.
    • 특징: 주로 스트림의 결과를 리스트, 세트, 맵 등의 형태로 수집(collect)할 때 사용되며, 많은 유용한 팩토리 메서드를 제공합니다.
  4. Collector:

    • 설명: Collector는 자바 8에서 도입된 인터페이스로, Collectors 클래스에서 제공하는 유틸리티 메서드들의 기반이 되는 인터페이스입니다.
    • 특징: 사용자 정의 컬렉션 작업을 수행하기 위해 구현할 수 있는 메서드를 정의하고 있습니다. 주로 스트림의 요소를 수집하고 그룹화하는 데 사용됩니다.

요약하면, Collection은 모든 컬렉션 클래스의 상위 인터페이스이며, Collections는 컬렉션을 조작하기 위한 유틸리티 클래스입니다. Collectors는 자바 8에서 추가된 스트림 API에서 사용되는 유틸리티 클래스이며, Collector는 이를 지원하기 위한 인터페이스입니다.


Comparable, Comparator, compareTo, compare

"Comparable," "Comparator," "compareTo," 그리고 "compare"는 Java에서 객체 비교와 정렬에 관련된 요소들을 나타냅니다. 이들 간의 차이를 간결하게 설명하겠습니다:

  1. Comparable:

    • 설명: Comparable은 자바에서 제공하는 인터페이스로, 객체가 자연스럽게 비교될 수 있도록 하는데 사용됩니다.
    • 특징: Comparable 인터페이스를 구현한 클래스의 객체들은 compareTo 메서드를 오버라이딩하여 비교 로직을 정의할 수 있습니다.
    • 예시: 문자열의 경우, 문자열 길이 등을 기준으로 비교할 수 있습니다.
  2. Comparator:

    • 설명: Comparator는 객체의 비교를 정의하는 데 사용되는 독립적인 클래스입니다. 주로 외부에서 비교 로직을 제공하고 싶을 때 활용됩니다.
    • 특징: Comparator 인터페이스를 구현한 클래스는 compare 메서드를 오버라이딩하여 비교 로직을 정의합니다.
    • 예시: 정렬 기준이나 순서를 동적으로 변경하고자 할 때 사용됩니다.
  3. compareTo:

    • 설명: compareToComparable 인터페이스에서 파생된 메서드로, 객체 간의 자연스러운 순서를 정의합니다.
    • 특징: 객체가 Comparable을 구현했을 때, 이 메서드를 오버라이딩하여 비교 로직을 정의합니다. 반환값은 음수, 0, 양수로서 비교 결과를 나타냅니다.
    • 예시: String 클래스에서 문자열 비교에 사용됩니다.
  4. compare:

    • 설명: compareComparator 인터페이스에서 파생된 메서드로, 두 객체 간의 비교 로직을 정의합니다.
    • 특징: Comparator를 구현한 클래스에서 이 메서드를 오버라이딩하여 비교 로직을 정의합니다. 반환값은 음수, 0, 양수로서 비교 결과를 나타냅니다.
    • 예시: 나이에 따라 객체를 비교하거나 다양한 비교 기준을 사용하고자 할 때 활용됩니다.

간단히 말하면, Comparable은 객체 자체가 비교 로직을 갖고 있을 때 사용되며, Comparator는 외부에서 비교 로직을 제공하고 싶을 때 사용됩니다. compareTocompare는 각각 인터페이스에서 비교를 정의하는 메서드로, 비교 결과에 따라 정렬이 이루어집니다.


Ordering

파이프라인이 스트림 요소를 처리하는 순서는 스트림이 직렬 또는 병렬로 실행되는지 여부, 스트림 소스 및 중간 작업에 따라 달라집니다. 예를 들어, forEach 작업을 사용하여 ArrayList 인스턴스의 요소를 여러 번 인쇄하는 다음 예제를 고려해 보세요.

Integer[] intArray = {1, 2, 3, 4, 5, 6, 7, 8};
List<Integer> listOfIntegers = new ArrayList<>(Arrays.asList(intArray));

System.out.println("listOfIntegers:");
listOfIntegers
	.stream()
    .forEach(e -> System.out.print(e + " "));
System.out.println("");

System.out.println("listOfIntegers stored in reverse order:");
Comparator<Integer> normal = Integer::comapre;
Comparator<Integer> reversed = normal.reversed();

Collections.sort(listOfIntegers, reversed);
listOfIntegers
	.stream()
    .forEach(e -> System.out.print(e + " "));
System.out.println("");

System.out.println("Parallel stream");
ListOfIntegers
	.parallelStream()
    .forEach(e -> System.out.print(e + " "));
System.out.println("");

System.out.println("Another parallel stream:");
listOfIntegers
    .parallelStream()
    .forEach(e -> System.out.print(e + " "));
System.out.println("");
     
System.out.println("With forEachOrdered:");
listOfIntegers
    .parallelStream()
    .forEachOrdered(e -> System.out.print(e + " "));
System.out.println("");

이 예시는 5개의 파이프라인으로 구성됩니다. 다음과 유사한 출력이 인쇄됩니다.

listOfIntegers:
1 2 3 4 5 6 7 8
listOfIntegers sorted in reverse order:
8 7 6 5 4 3 2 1
Parallel stream:
3 4 1 6 2 5 7 8
Another parallel stream:
6 3 1 5 7 8 4 2
With forEachOrdered:
8 7 6 5 4 3 2 1

이 예제에서는 다음을 수행합니다.

  • 첫 번째 파이프라인은 목록에 추가된 순서대로 listOfIntegers 목록의 요소를 인쇄합니다.
  • 두 번째 파이프라인은 Collections.sort 메서드로 정렬된 후 listOfIntegers의 요소를 인쇄합니다.
  • 세 번째와 네 번째 파이프라인은 목록의 요소를 무작위 순서로 인쇄합니다. 스트림 작업은 스트림 요소를 처리할 때 내부 반복(iteration)을 사용한다는 점을 기억하세요. 결과적으로 스트림을 병렬로 실행할 때 Java 컴파일러와 런타임은 스트림 작업에서 별도로 지정하지 않는 한 병렬 컴퓨팅의 이점을 최대화하기 위해 스트림 요소를 처리할 순서를 결정합니다.
  • 다섯 번째 파이프라인은 스트림을 직렬 또는 병렬로 실행했는지 여부에 관계없이 소스에서 지정한 순서대로 스트림 요소를 처리하는 forEachOrdered 메서드를 사용합니다. 병렬 스트림과 함께 forEachOrdered와 같은 작업을 사용하면 병렬 처리의 이점을 잃을 수 있습니다.

Side Effects

메서드나 식은 값을 반환하거나 생성하는 것 외에도 컴퓨터 상태를 수정하는 부작용이 있습니다. 예제에는 변경 가능한(mutable) reduction(collect 작업을 사용하는 작업, 자세한 내용은 Reduction 섹션 참조) 및 디버깅을 위한 System.out.println 메서드 호출이 포함됩니다. JDK는 파이프라인의 특정 부작용을 잘 처리합니다. 특히, collect 메서드는 병렬 안전(parallel-safe) 방식으로 부작용이 있는 가장 일반적인 스트림 작업을 수행하도록 설계되었습니다. forEachpeek와 같은 작업은 부작용을 위해 설계되었습니다. System.out.println을 호출하는 것과 같이 void를 반환하는 람다 식은 부작용만 있을 뿐입니다. 그렇더라도 forEachpeek 작업은 주의해서 사용해야 합니다. 병렬 스트림과 함께 이러한 작업 중 하나를 사용하면 Java 런타임은 매개 변수로 지정한 람다 식을 여러 스레드에서 동시에 호출할 수 있습니다. 또한 filtermap과 같은 작업에 부작용이 있는 람다 식을 매개 변수로 전달하지 마세요. 다음 섹션에서는 부작용의 원인이 될 수 있고 특히 병렬 스트림에서 일관되지 않거나 예측할 수 없는 결과를 반환할 수 있는 간섭(interference) 및 상태 저장(stateful) 람다 식에 대해 설명합니다. 그러나 게으름(laziness)은 간섭에 직접적인 영향을 미치기 때문에 먼저 게으름의 개념을 논의합니다.

Laziness

모든 중간 작업은 게으르다(lazy). 필요한 경우에만 값이 평가되는 식(expression), 메서드 또는 알고리즘은 게으른 것입니다. (알고리즘은 즉시 평가되거나 처리되는 경우 열성적(eager)입니다.) 중간 작업은 터미널 작업이 시작(commence)될 때까지 스트림 내용 처리를 시작하지 않기 때문에 게으릅니다. 스트림을 게으르게 처리하면 Java 컴파일러와 런타임이 스트림 처리 방법을 최적화할 수 있습니다. 예를 들어 집계 작업 섹션에 설명된 filter-mapToInt-average 예제와 같은 파이프라인에서 average 작업은 filter 작업에서 요소를 가져오는 mapToInt 작업으로 생성된 스트림에서 처음 여러 정수를 가져올 수 있습니다. average 연산은 스트림에서 필요한 모든 요소를 얻을 때까지 이 프로세스를 반복한 다음 평균을 계산합니다.

Interference

스트림 작업의 람다 표현식은 방해를 받아서는 안 됩니다. 파이프라인이 스트림을 처리하는 동안 스트림 소스가 수정되면 간섭이 발생합니다. 예를 들어, 다음 코드는 List listOfStrings에 포함된 문자열을 연결하려고 시도합니다. 그러나 ConcurrentModificationException이 발생합니다.

try {
	List<String> listOfStrings = 
    	new ArrayList<>(Arrays.asList("one", "two"));
 
 // This will fail as the peek operation will attempt to add the
 // string "three" to the source after the terminal operation has
 // commenced.
 
 	String concatenatedString = listOfStrings
    	.stream()
    
    // Don't do this! Interference occurs here. 
    	.peek(s -> listOfStrings.add("three"))

		.reduce((a, b) -> a + " " + b)
    	.get();
    
	System.out.println("Concatenated string: " + concatenatedString);
} catch (Exception e) {
	System.out.println("Exception caught: " + e.toString());
}

이 예제에서는 터미널 작업인 reduce 작업을 통해 listOfStrings에 포함된 문자열을 Optional<String> 값으로 연결합니다. 그러나 여기서 파이프라인은 listOfStrings에 새 요소를 추가하려고 시도하는 중간 작업 peek를 호출합니다. 모든 중간 작업은 게으르다는 점을 기억하세요. 즉, 이 예제의 파이프라인은 get 작업이 호출될 때 실행을 시작하고 get 작업이 완료되면 실행을 종료한다는 의미입니다. peek 작업의 인수는 파이프라인 실행 중에 스트림 소스를 수정하려고 시도하며 이로 인해 Java 런타임에서 ConcurrentModificationException이 발생합니다.


주어진 코드 예제에서 reduce 작업이 Optional<String>을 반환하는 이유는, 스트림이 비어있을 수 있기 때문입니다. 비어있는 스트림의 경우, 연결할 요소가 없어 빈 Optional을 반환합니다.

다음의 코드 부분을 통해 설명할 수 있습니다:

.reduce((a, b) -> a + " " + b)

여기서의 reduce 작업은 스트림의 요소를 연결하기 위해 사용되고 있습니다. 그러나 스트림이 비어있다면, 빈 Optional로 감싸진 결과가 반환됩니다. 스트림이 비어있지 않다면, 연결된 결과는 Optional<String>로 감싸진 상태로 반환됩니다.

만약 스트림이 항상 비어있지 않을 것이며 Optional을 피하고 싶다면, reduce 작업을 collect(Collectors.joining(" "))로 대체할 수 있습니다:

String concatenatedString = listOfStrings.stream()
        .map(s -> s + " three")
        .collect(Collectors.joining(" "));

이렇게 하면 Optional<String> 대신에 직접적으로 String 결과가 반환됩니다.


아니요, peek은 자바 스트림(Stream) API에서 제공하는 중간 연산(Intermediate Operation) 중 하나입니다. peek은 스트림의 각 요소에 주어진 동작(Consumer)을 수행하며, 스트림을 변경하지 않습니다. 다시 말해, 주로 디버깅이나 로깅을 위해 사용되며, 스트림의 각 요소를 확인하고 다음 단계로 넘어갈 때까지 변경하지 않습니다.

peek 메서드는 다음과 같이 정의됩니다:

Stream<T> peek(Consumer<? super T> action)

여기서 action은 각 요소에 적용할 동작을 나타내는 함수형 인터페이스인 Consumer입니다. 이 메서드는 스트림의 각 요소에 대해 action을 수행하고, 그 결과로 같은 스트림을 반환합니다.

간단한 예제를 통해 peek의 동작을 이해해보겠습니다:

List<String> strings = Arrays.asList("one", "two", "three");

List<String> result = strings.stream()
        .peek(s -> System.out.println("Processing: " + s))
        .map(String::toUpperCase)
        .collect(Collectors.toList());

위 코드에서 peek은 각 문자열을 출력하여 처리과정을 확인하고, map 메서드는 각 문자열을 대문자로 변환하여 새로운 리스트를 생성합니다. peek은 중간 연산이므로 최종 결과에는 영향을 주지 않습니다.


Stateful Lambda Expressions 상태 저장 람다 표현식

스트림 작업에서 stateful lambda expressions을 매개 변수로 사용하지 마세요. stateful lambda expression은 파이프라인 실행 중에 변경될 수 있는 상태에 따라 결과가 달라지는 식입니다. 다음 예제에서는 맵 중간 작업을 통해 List listOfIntegers의 요소를 새 List 인스턴스에 추가합니다. 먼저 직렬 스트림으로, 그다음 병렬 스트림으로 이 작업을 두 번 수행합니다.

List<Integer> serialStorage = new ArrayList<>();

System.out.println("Serial stream:");
listOfIntegers
	.stream()
    
    // Don;t do this! It uses a stateful lambda expression.
    .map(e -> { serialStorage.add(e); return e; })
    
    .forEachOrdered(e -> System.out.print(e + " ")); 
System.out.println("");

serialStorage
    .stream()
    .forEachOrdered(e -> System.out.print(e + " "));
System.out.println("");

System.out.println("Parallel stream:");
List<Integer> parallelStorage = Collections.synchronizedList(new ArrayList<>());
listOfIntegers
	.parallelStream()
	
    // Don't do this! It uses a stateful lambda expression.
    .map(e -> { parallelStorage.add(e); return e; })

	.forEachOrdered(e -> System.out(e + " "));
System.out.println("");

람다 표현식 e -> { parallelStorage.add(e); return e; }는 상태 저장 람다 표현식입니다. 코드가 실행될 때마다 결과가 달라질 수 있습니다. 이 예에서는 다음을 인쇄합니다.

Serial stream:
8 7 6 5 4 3 2 1
8 7 6 5 4 3 2 1
Parallel stream:
8 7 6 5 4 3 2 1
1 3 6 2 4 5 8 7

forEachOrdered 작업은 스트림이 직렬 또는 병렬로 실행되는지 여부에 관계없이 스트림에 지정된 순서대로 요소를 처리합니다. 그러나 스트림이 병렬로 실행되는 경우 맵 작업은 Java 런타임 및 컴파일러에서 지정한 스트림의 요소를 처리합니다. 결과적으로 람다 표현식 e -> { parallelStorage.add(e); return e; }List parallelStorage에 요소를 추가하는 순서는 코드가 실행될 때마다 달라질 수 있습니다. 결정적이고(deterministic) 예측 가능한(predictable) 결과를 얻으려면 스트림 작업의 람다 식 매개 변수가 stateful이 아닌지 확인하세요.


람다 표현식은 외부의 상태에 접근할 수 있습니다. 이는 자바의 람다 표현식이 클로저(closure)라는 특성을 가지기 때문입니다. 클로저는 자신을 포함하는 범위(scope)에서 정의된 변수에 접근할 수 있는 함수입니다.


serialStorage.add(e)return e에서 return emap의 결과로 현재 요소를 반환하는 것이지만, serialStorage.add(e)는 외부에 있는 serialStorage 리스트에 현재 요소를 추가하는 동작입니다. map의 결과로 반환된 값은 그냥 새로운 스트림으로 전달되어 사용될 뿐, serialStorage에 추가된 결과는 스트림 파이프라인 외부에서 나타납니다.


참고: 이 예제에서는 synchronizedList 메서드를 호출하여 List parallelStorage가 스레드로부터 안전하도록 합니다. 컬렉션은 스레드로부터 안전하지 않다는 점을 기억하세요. 이는 여러 스레드가 동시에 특정 컬렉션에 액세스해서는 안 된다는 것을 의미합니다. parallelStorage를 생성할 때 synchronizedList 메소드를 호출하지 않는다고 가정해 보겠습니다.

List<Integer> parallelStorage = new ArrayList<>();

특정 스레드가 List 인스턴스에 액세스할 수 있는 시기를 예약하기 위해 동기화와 같은 메커니즘 없이 여러 스레드가 ParallelStorage에 액세스하고 수정하기 때문에 이 예제는 잘못(erratically) 작동합니다. 결과적으로 예제에서는 다음과 유사한 출력을 인쇄할 수 있습니다.

Parallel stream:
8 7 6 5 4 3 2 1
null 3 5 4 7 8 1 2

0개의 댓글