synchronized와 volatile은 자바에서 멀티스레드 환경에서 데이터 일관성과 동시성을 관리하기 위한 키워드이다.
쓰레드 간의 데이터 공유 방식과 동기화를 관리하는 데 주로 사용된다.
1. synchronized
synchronized는 자바에서 메서드나 블록에 사용하여 동시 접근을 제한하는 역할을 한다.
이를 통해 한 번에 하나의 쓰레드만 특정 코드에 접근하도록 보장하므로 동시성 문제를 해결하는 데 유용하다.
특징
- 공기화된 메서드 또는 블록은 한 번에 하나의 쓰레드만 접근할 수 있다.
- 특정 코드가
임계 구역(critical section)
이 되어 여러 쓰레드가 동시에 접근하면 안 되는 경우에 사용한다.
- 메서드나 블록 앞에 synchronized 키워드를 붙이면, JVM이 내부적으로
모니터 락
을 걸어 동기화를 관리한다.
public class Counter { private int count = 0; public synchronized void increment() { // synchronized 메서드 count++; } public int getCount() { return count; } }
increment 메서는 동기화되어 여러 쓰레드가 동시에 호출해도 한 번에 한 쓰레드만 접근할 수 있게 된다.
블록으로 사용
public void increment() { synchronized (this) { // synchronized 블록 count++; } }
2. volatile
변수의 값을 모든 쓰레드에서 즉시 읽고 쓰게 보장하는 역할을 한다.
자바에서는 쓰레드가 변수를 캐시하여 사용하기 때문에, 변수의 최신 값이 모든 쓰레드에 즉시 반영되지 않을 수 있다.
이 때 volatile을 사용하면 각 쓰레드가 메인 메모리에서 항상 최신 값을 읽어오도록 보장한다.
특징
- 변수 앞에만 사용 가능하다. 메서드나 블록에는 사용할 수 없다.
- 동기화 없이 최신 값을 보장하지만, 원자성을 보장하지 않는다. 따라서 i++같은 연산에서는 여전히 동기화를 추가해야 한다.
- i++ 같은 증감 연산은 원자적이지 않기 때문에 동기화가 필요한 경우가 있다.
- 이 연산은 읽기, 연산, 쓰기의 세 단계로 이루어진다.
- 읽기: 현재 메모리에서 i 값을 읽어온다.
- 연산: i의 값에 1을 더한다.
- 쓰기: 연산 결과를 다시 메모리에 저장한다.
- 이 세 단계는 별개로 수행되기 때문에 여러 쓰레드가 동시에 i++를 실행하면 동시성 문제가 발생할 수 있게 된다.
- 이럴 때는 i++ 대신 AtomicInteger 같은 AtomicType을 사용하는 것이 좋다.
- 간단한 읽기와 쓰기 연산에서 쓰레드 간 최신 상태를 보장하는 데 사용한다.
public class Flag { private volatile boolean flag = false; public void setFlagTrue() { flag = true; } public boolean isFlagTrue() { return flag; } }
위처럼 flag를 volatile로 선언하면 flag 값을 변경할 때 모든 쓰레드에 즉시 최신 값이 반영된다.
Synchronized, volatile 비교
ㅤ | synchronized | volatile |
용도 | 임계 구역 관리로 동기화 | 변수의 최신 값을 보장 |
보장 성질 | 원자성, 가시성 | 가시성 |
사용 대상 | 메서드, 코드 블록 | 변수 |
성능 | 상대적으로 느림(락 때문) | 상대적으로 빠름 |
동시성 문제 해결 | 주로 복잡한 동기화 문제 해결에 유용 | 단순한 플래그 변경 등의 문제 해결에 적합 |
사용 예시
- synchronized는 공유 리소스(리스트, 맵 등)나 복잡한 연산이 동반되는 메서드에서 동기화가 필요할 때 주로 사용된다.
- volatile은 플래그나 간단한 카운터와 같이 읽기와 쓰기 연산만 필요한 상황에서 최신 상태를 보장할 때 유용하다.