Java에서 멀티스레드 프로그래밍을 하다 보면 동기화 문제가 자주 발생한다.
이때 Synchronized는 공유자원을 생성하여 해당 자원에 대한 동기화 영역을 생성하는데 도움을 준다.
Synchronized
: 자바에서 동기화 영역을 생성하는 키워드이다.
Synchrosized 처리된 객체나 메서드는 두 개 이상의 쓰레드가 동시 접근하는 것을 막는다.
즉 하나의 스레드가 해당 객체나 메서드를 사용하는 동안 다른 스레드가 접근하지 못하도록 Lock을 거는 것.
Java 스레드 스케줄러가 다음과 같은 원칙에 따라 어떤 스레드가 먼저 실행할지를 결정한다.
ReentrantLock
클래스의 공정성 설정을 통해 대기 순서대로 접근 제어 가능.ReentrantLock lock = new ReentrantLock(true); // 공정성 설정
synchronized
키워드를 메소드 선언에 붙이면, 해당 메소드를 호출한 인스턴스(객체)를 기준으로 동기화가 이루어진다.public synchronized void add(int value) {
this.count += value;
}
static synchronized
메소드는 해당 클래스 자체를 기준으로 동기화가 이루어진다.public static synchronized void add(int value) {
count += value;
}
synchronized
키워드로 동기화할 수 있다.this
객체(메소드를 호출한 객체) 또는 다른 객체로 설정할 수 있다.public void add(int value) {
// 동기화되지 않은 코드
synchronized(this) {
this.count += value; // 이 부분만 동기화
}
// 동기화되지 않은 코드
}
ClassName.class
)이다.public static void add(int value) {
synchronized (MyClass.class) {
count += value; // 이 부분만 동기화
}
}
유형 | 동기화 기준 | 동작 범위 | 특징 |
---|---|---|---|
인스턴스 메소드 | 호출한 객체 (this ) | 메소드 전체 | 객체 단위로 동기화되며, 한 객체당 한 스레드만 실행 가능하다. |
스태틱 메소드 | 클래스 객체 | 메소드 전체 | 클래스 단위로 동기화되며, 모든 인스턴스가 공유한다. |
인스턴스 메소드 블록 | 호출한 객체 (this ) | 특정 코드 블록 | 객체 단위로 동기화되며, 블록 내 코드만 한 번에 한 스레드만 실행 가능하다. |
스태틱 메소드 블록 | 클래스 객체 | 특정 코드 블록 | 클래스 단위로 동기화되며, 특정 블록만 한 번에 한 스레드만 실행 가능하다. |
Synchronized는 데이터의 일관성을 보장해 주지만, 코드의 실행 시간이 길어질수록 다른 스레드의 대기 시간이 늘어나면서 시스템의 효율성이 떨어질 수 있다.
따라서 공유 객체를 사용하는 임계 영역(critical section)은 꼭 필요한 부분에만
최대한 작게
유지하는 것이 중요하다.
CleanCode 13.동시성
p236, 동기화하는 부분을 최대한 작게 만들어라.임계 영역을 줄인다고 임계 영역의 크기를 키우지는 마라. 그러면 스레드간의 경쟁도 늘고 성능도 떨어진다.
Java 5부터는 스레드 안전한 java.util.concurrent 패키지가 제공되면서 보다 정교한 동기화 제어가 가능해졌다.
ReentrantLock
, ReadWriteLock
, Semaphore
, Atomic
클래스들이 대표적이다.
CleanCode 13.동시성
p233, 언어가 제공하는 스레드 안전한 클래스를 검토하고 자바에서는 해당 클래스들을 익혀라.
Synchronize
가 스레드의 공유자원을 설정하는 키워드라면 ThreadLocal
은 Java에서 각 스레드가 독립적으로 자원을 보유할 수 있도록 도와주는 클래스이다.
ThreadLocal 객체 생성
private ThreadLocal<String> myThreadLocal = new ThreadLocal<>();
각 스레드는 이 객체를 통해 독립된 값을 설정하고 가져올 수 있다.
값 설정
myThreadLocal.set("Thread-specific value");
현재 스레드에 특정 값을 저장한다.
값 가져오기
String value = myThreadLocal.get();
현재 스레드에 설정된 값을 반환한다.
값 제거
myThreadLocal.remove();
현재 스레드의 값을 초기화한다.
ThreadLocal
의 initialValue()
메소드를 오버라이드하여 기본값을 제공할 수 있다.private ThreadLocal<Integer> threadLocal = new ThreadLocal<>() {
@Override
protected Integer initialValue() {
return 0;
}
};
ThreadLocal
은 부모 스레드의 값을 자식 스레드가 참조하지 못한다.InheritableThreadLocal
을 사용하면 부모 스레드의 값을 자식 스레드에 전달할 수 있다.private InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
initialValue()
와 InheritableThreadLocal
을 활용해 초기값과 상속 관계도 커스텀하여 설정 가능하다.각 스레드에 고유 값 부여
private ThreadLocal<Integer> threadLocal = new ThreadLocal<>() {
@Override
protected Integer initialValue() {
return 0;
}
};
public void increment() {
threadLocal.set(threadLocal.get() + 1);
}
public Integer getValue() {
return threadLocal.get();
}
InheritableThreadLocal 사용
private InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
public void setParentValue(String value) {
threadLocal.set(value);
}
public String getValue() {
return threadLocal.get();
}
ThreadLocal
에 저장된 값을 반드시 remove()
로 제거해야 한다.ThreadLocal
객체는 값이 참조되지 않더라도 GC에 의해 수거되지 않을 수 있다.