You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
java.util 패키지에 소속되어 있는 컬렉션 프레임워크는 원자적인 연산을 제공할까?
예를 들어서 하나의 ArrayList 인스턴스에 여러 스레드가 동시에 접근해도 괜찮을까?
참고로 여러 스레드가 동시에 접근해도 괜찮은 경우를 **스레드 세이프(Thread Safe)**하다고 한다.
그렇다면 ArrayList는 스레드 세이프 할까?
다음 예제 코드를 보자.
packagethread.collection.simple;
importjava.util.ArrayList;
importjava.util.List;
publicclassSimpleListMainV0 {
publicstaticvoidmain(String[] args) {
List<String> list = newArrayList<>();
// 스레드1, 스레드2가 동시에 실행 가정list.add("A"); // 스레드1 실행 가정list.add("B"); // 스레드2 실행 가정System.out.println(list);
}
}
실행 결과
여기서는 멀티스레드를 사용하지 않지만, 스레드1과 스레드2가 동시에 다음 코드를 실행한다고 가정해보자.
스레드1: list에 A를 추가한다.
스레드2: list에 B를 추가한다.
컬렉션에 데이터를 추가하는 add() 메서드를 생각해보면, 단순히 컬렉션에 데이터를 하나 추가하는 것뿐이다. 따라서 이것은 마치 연산이 하나만 있는 원자적인 연산처럼 느껴진다. 원자적인 연산은 쪼갤 수 없기 때문에 멀티스레드 상황에 문제가 되지 않는다.
물론 멀티스레드는 중간에 스레드의 실행 순서가 변경될 수 있으므로 [A, B] 또는, [B, A] 로 데이터의 저장 순서는 변경될 수 있지만, 결과적으로 데이터는 모두 안전하게 저장될 것 같다.
publicvoidadd(Objecte) {
elementData[size] = e;
sleep(100); // 멀티스레드 문제를 쉽게 확인하는 코드size++;
}
이 메서드는 단순히 데이터 하나를 추가하는 기능을 제공한다. 따라서 밖에서 보면 원자적인 것처럼 보인다.
이 메서드는 단순히 데이터를 추가하는 것으로 끝나지 않는다. 내부에 있는 배열에 데이터를 추가해야 하고, size도 함께 하나 증가시켜야 한다. 심지어 size++ 연산 자체도 원자적이지 않다. size++ 연산은 size = size + 1 연산과 같다.
이렇게 원자적이지 않은 연산을 멀티스레드 상황에 안전하게 사용하려면 synchronized, Lock 등을 사용해서 동기화를 해야한다.
스레드1, 스레드2가 sleep()에서 잠시 대기한다. 여기서 sleep()을 사용한 이유는 동시성 문제를 쉽게 확인하기 위해서다.
이 코드를 제거하면 size++이 너무 빨리 호출되기 때문에, 스레드1이 add()메서드를 완전히 수행하고 나서스레드2가 add() 메서드를 수행할 가능성이 높다.
당연한 이야기지만 sleep()코드를 제거해도 멀티스레드 동시성 문제는 여전히 발생할 수 있다. (확률의 차이이다.) 예를 들어서 sleep()코드를 제거해도 다음과 같은 상황이 발생할 수 있다.
publicvoidadd(Objecte) {
elementData[size] = e;
size++; // 스레드1, 스레드2 size++ 실행 전 대기
}
과정3
publicvoidadd(Objecte) {
elementData[size] = e;
sleep(100);
size++; //스레드1, 스레드2 동시에 실행
}
여기서는 2가지 상황이 발생할 수 있다.
상황1
스레드1, 스레드2가 size++ 코드를 동시에 수행한다. 여기서는 스레드1이 약간 빠르게 수행했다.
스레드1 수행: size++, size의 값은 1이 된다.
스레드2 수행: size++, size의 값은 1 → 2가 된다.
결과적으로 size의 값은 2가 된다.
상황2
스레드1, 스레드2가 size++ 코드를 동시에 수행한다. 여기서는 스레드1, 스레드2가 거의 동시에 실행되었다.
스레드1 수행: size = size + 1 연산이다. size의 값을 읽는다. 0이다.
스레드2 수행: size = size + 1 연산이다. size의 값을 읽는다. 0이다.
스레드1 수행: size = 0 + 1 연산을 수행한다.
스레드2 수행: size = 0 + 1 연산을 수행한다.
스레드1 수행: size = 1 대입을 수행한다.
스레드2 수행: size = 1 대입을 수행한다.
결과적으로 size의 값은 1이 된다.
우리가 본 케이스는 상황1이지만, size++ 연산도 원자적인 연산이 아니므로 때때로 상황2가 될 수도 있다.
(따라서 로그에서 size값이 1로 출력될 가능성도 있다.)
컬렉션 프레임워크 대부분은 스레드 세이프 하지 않다.
우리가 일반적으로 자주 사용하는 ArrayList, LinkedList, HashSet, HashMap 등 수 많은 자료 구조들은 단순한 연산을 제공하는 것 처럼 보인다. 예를 들어서 데이터를 추가하는 add()와 같은 연산은 마치 원자적인 연산처럼 느껴진다. 하지만 그 내부에서는 수 많은 연산들이 함께 사용된다. 배열에 데이터를 추가하고, 사이즈를 변경하고, 배열을 새로 만들어서 배열의 크기도 늘리고, 노드를 만들어서 링크에 연결하는 등 수 많은 복잡한 연산이 함께 사용된다.
따라서 일반적인 컬렉션들은 절대로! 스레드 세이프 하지 않다!
단일 스레드가 컬렉션에 접근하는 경우라면 아무런 문제가 없지만, 멀티스레드 상황에서 여러 스레드가 동시에 컬렉션에 접근하는 경우라면 java.util 패키지가 제공하는 일반적인 컬렉션들은 사용하면 안된다! (물론 일부 예외도 있다. 뒤에서 설명한다.)
최악의 경우 실무에서 두 명의 사용자가 동시에 컬렉션에 데이터를 보관했는데, 코드에 아무런 문제가 없어 보이는데, 한명의 사용자 데이터가 사라질 수 있다.
그럼 어떻게 해야할까?
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
-
0x01. 동시성 컬렉션이 필요한 이유
1.1. 동시성 컬렉션이 필요한 이유1 - 시작
java.util패키지에 소속되어 있는 컬렉션 프레임워크는 원자적인 연산을 제공할까?예를 들어서 하나의
ArrayList인스턴스에 여러 스레드가 동시에 접근해도 괜찮을까?참고로 여러 스레드가 동시에 접근해도 괜찮은 경우를 **스레드 세이프(Thread Safe)**하다고 한다.
그렇다면
ArrayList는 스레드 세이프 할까?다음 예제 코드를 보자.
실행 결과
여기서는 멀티스레드를 사용하지 않지만, 스레드1과 스레드2가 동시에 다음 코드를 실행한다고 가정해보자.
list에A를 추가한다.list에B를 추가한다.컬렉션에 데이터를 추가하는
add()메서드를 생각해보면, 단순히 컬렉션에 데이터를 하나 추가하는 것뿐이다. 따라서 이것은 마치 연산이 하나만 있는 원자적인 연산처럼 느껴진다. 원자적인 연산은 쪼갤 수 없기 때문에 멀티스레드 상황에 문제가 되지 않는다.물론 멀티스레드는 중간에 스레드의 실행 순서가 변경될 수 있으므로
[A, B]또는,[B, A]로 데이터의 저장 순서는 변경될 수 있지만, 결과적으로 데이터는 모두 안전하게 저장될 것 같다.하지만 컬렉션 프레임워크가 제공하는 대부분의 연산은 원자적인 연산이 아니다.
(1) 컬렉션 직접 만들기
이 부분을 이해하기 위해 아주 간단한 컬렉션을 직접 만들어보자.
ArrayList의 최소 구현 버전이라 생각하면 된다.DEFAULT_CAPACITY: 최대 5의 데이터를 저장할 수 있다.size: 저장한 데이터의 크기를 나타낸다.add(): 컬렉션에 데이터를 추가한다.sleep(100): 잠시 대기한다. 이렇게 하면 멀티스레드 상황에 발생하는 문제를 확인하기 쉽다.만든 컬렉션을 실행해보자.
실행 결과
단일 스레드로 실행했기 때문에 아직까지는 아무런 문제 없이 잘 작동한다.
1.2. 동시성 컬렉션이 필요한 이유2 - 동시성 문제
(1) 멀티스레드 문제 확인
add() - 원자적이지 않은 연산
이 메서드는 단순히 데이터 하나를 추가하는 기능을 제공한다. 따라서 밖에서 보면 원자적인 것처럼 보인다.
이 메서드는 단순히 데이터를 추가하는 것으로 끝나지 않는다. 내부에 있는 배열에 데이터를 추가해야 하고,
size도 함께 하나 증가시켜야 한다. 심지어size++연산 자체도 원자적이지 않다.size++연산은size = size + 1연산과 같다.이렇게 원자적이지 않은 연산을 멀티스레드 상황에 안전하게 사용하려면
synchronized,Lock등을 사용해서 동기화를 해야한다.이번에는 멀티스레드를 사용해서 실제 어떤 문제가 발생하는지 확인해보자.
실행 결과
[A, null]이 결과로 나올 수도 있다.size는 2인데, 데이터는 B 하나만 입력되어 있다! 어떻게 된 것일까?과정 1
스레드1, 스레드2가
elementData[size] = e코드를 동시에 수행한다. 여기서는 스레드1이 약간 빠르게 수행했다.elementData[0] = A,elementData[0]의 값은 A가 된다.elementData[0] = B,elementData[0]의 값은 A → B가 된다.결과적으로
elementData[0]의 값은 B가 된다.과정2
스레드1, 스레드2가
sleep()에서 잠시 대기한다. 여기서sleep()을 사용한 이유는 동시성 문제를 쉽게 확인하기 위해서다.이 코드를 제거하면
size++이 너무 빨리 호출되기 때문에, 스레드1이add()메서드를 완전히 수행하고 나서스레드2가add()메서드를 수행할 가능성이 높다.당연한 이야기지만
sleep()코드를 제거해도 멀티스레드 동시성 문제는 여전히 발생할 수 있다. (확률의 차이이다.) 예를 들어서sleep()코드를 제거해도 다음과 같은 상황이 발생할 수 있다.과정3
여기서는 2가지 상황이 발생할 수 있다.
상황1
스레드1, 스레드2가
size++코드를 동시에 수행한다. 여기서는 스레드1이 약간 빠르게 수행했다.size++,size의 값은 1이 된다.size++,size의 값은 1 → 2가 된다.결과적으로
size의 값은 2가 된다.상황2
스레드1, 스레드2가
size++코드를 동시에 수행한다. 여기서는 스레드1, 스레드2가 거의 동시에 실행되었다.스레드1 수행:
size = size + 1연산이다.size의 값을 읽는다. 0이다.스레드2 수행:
size = size + 1연산이다.size의 값을 읽는다. 0이다.스레드1 수행:
size = 0 + 1연산을 수행한다.스레드2 수행:
size = 0 + 1연산을 수행한다.스레드1 수행:
size = 1대입을 수행한다.스레드2 수행:
size = 1대입을 수행한다.결과적으로
size의 값은 1이 된다.우리가 본 케이스는 상황1이지만,
size++연산도 원자적인 연산이 아니므로 때때로 상황2가 될 수도 있다.(따라서 로그에서
size값이 1로 출력될 가능성도 있다.)컬렉션 프레임워크 대부분은 스레드 세이프 하지 않다.
우리가 일반적으로 자주 사용하는
ArrayList,LinkedList,HashSet,HashMap등 수 많은 자료 구조들은 단순한 연산을 제공하는 것 처럼 보인다. 예를 들어서 데이터를 추가하는add()와 같은 연산은 마치 원자적인 연산처럼 느껴진다. 하지만 그 내부에서는 수 많은 연산들이 함께 사용된다. 배열에 데이터를 추가하고, 사이즈를 변경하고, 배열을 새로 만들어서 배열의 크기도 늘리고, 노드를 만들어서 링크에 연결하는 등 수 많은 복잡한 연산이 함께 사용된다.따라서 일반적인 컬렉션들은 절대로! 스레드 세이프 하지 않다!
단일 스레드가 컬렉션에 접근하는 경우라면 아무런 문제가 없지만, 멀티스레드 상황에서 여러 스레드가 동시에 컬렉션에 접근하는 경우라면
java.util패키지가 제공하는 일반적인 컬렉션들은 사용하면 안된다! (물론 일부 예외도 있다. 뒤에서 설명한다.)최악의 경우 실무에서 두 명의 사용자가 동시에 컬렉션에 데이터를 보관했는데, 코드에 아무런 문제가 없어 보이는데, 한명의 사용자 데이터가 사라질 수 있다.
그럼 어떻게 해야할까?
Beta Was this translation helpful? Give feedback.
All reactions