티스토리 뷰
728x90
반응형
공유중인 가변 데이터는 동기화해 사용하라
- 싱글 스레드 기반의 프로그램에서는 하나의 스레드가 하나의 객체에 접근할 수 있으므로 동기화에 대한 걱정을 크게 하지 않아도 됩니다. 하지만 멀티 스레드 기반의 프로그램에서는 여러 스레드가 하나의 객체에 접근할 수 있으므로 상당한 주의가 필요합니다. 어떤 하나의 스레드가 객체의 상태를 변경하는 도중 다른 스레드가 접근해 읽는다면 기대한 결과와 다른 결과를 초래할 수 있습니다.
💡 동기화란?
- 동기화(Synchronized)란 멀티 스레드 환경에서 하나의 메서드나 블럭에 하나의 스레드만 접근하여 작업을 수행하도록 보장하는 것을 의미합니다.
💡 동기화의 특징
- 한 객체가 일관된 상태를 가지고 생성되었을 때, 이 객체에 접근하는 메서드는 그 객체에 락(lock)을 겁니다. 락을 건 메서드는 객체의 상태를 확인하고 필요하면 수정합니다.
- 동기화를 제대로 사용하면 어떤 메서드도 이 객체의 상태가 일관되지 않은 순간을 볼 수 없을것입니다.
- 동기화는 일관성이 깨진 상태를 볼 수 없게 하는 것은 물론, 동기화된 메서드나 블럭에 들어간 스레드가 같은 락의 보호하에 수행된 모든 이전 수정의 최종 결과를 보게해줍니다.
💡 원자성(Atomic)
- 언어 명세상 long과 double 외의 변수를 읽고 쓰는 동작이 원자적이라 합니다. 멀티 스레드 환경에서 변수를 동기화하지 않고 수정하더라도 스레드가 정상적으로 저장한 값을 온전히 읽어옴을 보장한다는 의미입니다. 이말을 듣고 동기화하지 않아도 될거 같지만 이는 위험한 생각입니다. 자바 언어 명세는 스레드가 필드를 읽을 때 항상 수정이 완전히 반영된 값을 얻는다고 보장하지만, 한 스레드가 저장한 값이 다른 스레드에게 보이는가는 보장하지 않습니다. 그렇기 때문에 동기화는 배타적 실행뿐 아니라 스레드 사이의 안정적인 통신에 꼭 필수입니다.
🧨 문제가 발생한 코드
- 아래의 예제코드를 보면 스레드가 시작되고 1초뒤에 stopRequested변수가 false로 바뀌면서 while문이 종료될거 같지만 무한 루프에 빠지고 맙니다. 해당 원인은 동기화에 있는데 동기화하지 않으면 메인 스레드에서 수정한 stopRequested 변수가 thread에서 언제쯤 보게될지 모르므로 무한 루프에 빠지게 됩니다.
public class Example {
private static boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
int i = 0;
while (!stopRequested) {
i++;
}
});
thread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
💡 Synchronized 사용
- 읽기(stopRequested), 쓰기(requestStop) 메서드를 동기화해서 만들어 제공함으로써 문제를 해결할 수 있습니다.
- 여기서 읽기/쓰기 메서드 모두 동기화를 처리했는데 만약 둘중 하나만하게 된다면 정확한 동작을 보장받을 수 없습니다.
public class Example {
private static boolean stopRequested;
private static synchronized void requestStop() {
stopRequested = true;
}
private static synchronized boolean stopRequested() {
return stopRequested;
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
int i = 0;
while (stopRequested()) {
i++;
}
});
thread.start();
TimeUnit.SECONDS.sleep(1);
requestStop();
}
}
💡 Volatile 사용
- volatile 키워드의 의미는 volatile 변수를 읽어 들일때 CPU 캐시가 아닌 컴퓨터의 메인 메모리로부터 읽어들입니다. 즉 read할 때도 CPU 캐시가 아닌 메모리에서 read하고, write할 때도 메인 메모리에 write작업을 수행합니다.
- volatile 키워드를 사용할 경우 배타적 수행(한 메서드나 블럭을 한 스레드가 실행)과는 상관이 없지만 항상 가장 최근에 기록된 값을 읽게 된다는 점을 보장할 수 있게 됩니다.
public class Example {
private static volatile boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
int i = 0;
while (stopRequested) {
i++;
}
});
thread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
💡 Volatile 사용시 주의 사항
- 아래 메서드는 호출할때마다 1씩 증가하여 스레드에서 고유한 값을 반환할 목적으로 만들어졌습니다. Volatile 키워드가 쓰여졌기 때문에 항상 최신의 값을 가져올 수 있을거 같지만 제대로된 고유한 값이 반환되지 않을 수 있습니다. 원인은 메서드의 nextSerialNumber++에 있습니다. 수를 증가시킬때 nextSerialNumber 변수 값을 한번 읽어와 +1한 다음 다시 nextSerialNumber 변수에 저장하는 형태입니다. 여기서 만약 다른 스레드가 nextSerialNumber + 1을 하는 시점에 들어온다면 두 쓰레드는 중복된 수를 반환하는 상황이 펼쳐질 수 있습니다. 이런 오류를 안전 실패하고 합니다.
private static volatile int nextSerialNumber = 0;
public static int getSerialNumber() {
return nextSerialNumber++;
}
💡 문제 해결 방법은?
- 메서드에 synchronized 키워드를 붙여주면 문제를 해결할 수 있습니다. 동시에 호출해도 배타적으로 수행되기 때문입니다. 만약 메서드에 synchronized 키워드를 붙였다면 변수에는 volatile 키워드를 제거해줘야 합니다.
private static int nextSerialNumber = 0; // volatile 키워드 제거
public static synchronized int getSerialNumber() { // synchronized 키워드 사용
return nextSerialNumber++;
}
💡 AtomicLong
- 그 밖에 java.util.concurrent 패키지에는 동시성 프로그래밍을 위한 여러 자바 라이브러리를 제공하고 있는데 이 중 AtomicLong이라는 클래스가 있는데, 배타적 실행과 스레드간 통신 모드 제공하면서 성능도 동기화 버전보다 좋습니다.
private static final AtomicLong nextSerialNumber = new AtomicLong();
public static long getSerialNumber() {
return nextSerialNumber.getAndIncrement();
}
💡 가변 데이터는 공유하지 말자
- 사실 이러한 문제가 발생하지 않도록 애초에 가변 데이터는 스레드 간 공유하지 않는게 좋습니다. 불변 데이터 정도만 공유를 하는게 제일 안전하고, 가변 데이터는 단일 스레드 환경에서만 사용하는 것이 좋습니다.
- 혹은 한 스레드에서 데이터를 수정한 뒤 다른 스레드에 공유할 때 해당 객체에서 공유하는 부분만 동기화하는 수도 있습니다. 이러면 다시 해당 객체를 수정하기 전까지 동기화 하지 않고 자유롭게 값을 동시에 읽어가도 됩니다. 이러한 객체를 사실상 불변이라 하고, 이런 객체를 다른 스레드에 건네는 행위를 안전 발행이라 합니다.
✔️ 정리
- 가변 데이터는 멀티 스레드 환경에서 사용하지 맙시다.
- 그럼에도 불구하고 멀티 스레드 환경에서 가변 데이터를 사용할 일이 생기면 읽기/쓰기 동작을 반드시 같이 동기화해야 합니다. 둘 중 하나만 동기화를 한다면 완벽한 보장이 이루어지지 않습니다.
- 배타적 수행의 동작은 필요없고, 스레드 간 최신 데이터만 읽는 것으로 충분하다면 가변 변수에 volatile 키워드만으로도 동기화가 충분합니다.
728x90
반응형
'스터디 > 이펙티브 자바' 카테고리의 다른 글
이펙티브 자바 - Item81. wait와 notify보다는 동시성 유틸리티를 애용하라 (0) | 2022.08.23 |
---|---|
이펙티브 자바 - Item79. 과도한 동기화는 피하라 (0) | 2022.08.22 |
이펙티브 자바 - Item73. 메서드가 던지는 모든 예외를 문서화하라 (0) | 2022.08.20 |
이펙티브 자바 - Item72. 표준 예외를 사용하라 (1) | 2022.08.20 |
이펙티브 자바 - Item71. 필요 없는 검사 예외 사용은 피하라 (0) | 2022.08.20 |
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
TAG
- 트랜잭셔널 아웃박스 패턴 스프링 부트 예제
- 레이어드 아키텍처란
- JDK Dynamic Proxy와 CGLIB의 차이
- microkernel architecture
- java userThread와 DaemonThread
- pipeline architecture
- transactional outbox pattern spring boot
- spring boot poi excel download
- spring boot 엑셀 다운로드
- 람다 표현식
- 공간 기반 아키텍처
- 자바 백엔드 개발자 추천 도서
- 서비스 기반 아키텍처
- redis 대기열 구현
- redis sorted set으로 대기열 구현
- pipe and filter architecture
- spring boot excel download paging
- redis sorted set
- space based architecture
- spring boot redis 대기열 구현
- spring boot redisson 분산락 구현
- 트랜잭셔널 아웃박스 패턴 스프링부트
- polling publisher spring boot
- service based architecture
- spring boot excel download oom
- @ControllerAdvice
- transactional outbox pattern
- java ThreadLocal
- spring boot redisson destributed lock
- spring boot redisson sorted set
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
글 보관함