쓰레드와 동기화

이펙티브자바 규칙66: 변경 가능 공유 데이터에 대한 접근은 동기화하라

아래 코드는 실행한지 1초가 지나면 stopRequested 변수를 true로 바꿔주고 프로그램이 종료되도록 기대하면서 작성된 코드입니다.

하지만 실제로 아래 코드를 돌려보면 절대 종료 되지 않습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.concurrent.TimeUnit;

public class StopThread {

private static boolean stopRequested;

public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(new Runnable() {

@Override
public void run() {
int i = 0;
while (!stopRequested) {
i++;
}
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}

그 이유는 위 코드는 동기화 메커니즘을 적용하지 않고 멀티 쓰레드로 개발을 했기 때문에 **main 쓰레드**에서 변경한 stopRequest의 새로운 값을 backgroundThread 에서 언제 확인하게 될지 알 수가 없기 때문입니다.

책에서는 이 문제를 해결하기 위한 솔루션으로 두가지를 제시 합니다.

  1. synchronized 키워드를 사용하여 stopRequested 변수를 동기화 하는 방법.
  2. volatile 키워드를 사용하여 lock없이 모든 쓰레드가 최근에 기록된 값을 읽어가도록 보장하는 방법

synchronized 키워드를 이용한 방법의 코드

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
import java.util.concurrent.TimeUnit;

public class StopThread {

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 backgroundThread = new Thread(new Runnable() {

@Override
public void run() {
int i = 0;
while (!stopRequested()) {
i++;
}
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
requestStop();
}
}

쓰기 메서드 (requestStop)와 읽기 메서드(stopRequested) 모두에 동기화 메커니즘이 적용되어 있습니다.

** 읽기와 쓰기 모두에 적용하지 않으면 동기화는 아무런 효과도 없다** 고 책에서 강조하고 있습니다.

위 코드에서 동기화 메서드가 하는 일은 실제 그 메서드 들의 상호 배제성을 위해서라기보다는 (Mutual Exclusion) stopRequested변수의 가시성(Visibility)을 보장하기 위해서였습니다.
이 부분은 이 블로그를 한번 읽어보시면 도움이 됩니다.

사실 단순히 그 목적이라면 성능 향상을 위해 해당 객체를 lock 잡지 않고 volatile로 선언하기만 해도 해결할 수 있습니다.

volatile 키워드를 이용한 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.concurrent.TimeUnit;

public class StopThread {

private static volatile boolean stopRequested;

public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(new Runnable() {

@Override
public void run() {
int i = 0;
while (!stopRequested) {
i++;
}
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}

맨 처음의 잘못된 코드와 다른점은 stopRequested 변수를 volatile로 선언한 것 밖에 없지만 1초후에 정상적으로 종료 되는것을 확인 할 수 있습니다.

그럼 long과 double을 제외하고서는 기본적으로 원자성을 보장 해 주고….
위와같이 volatile 키워드를 사용하면 동기화 블럭이나 동기화 함수를 만들지 않고도 Thread safe 하게 변수를 공유하는 프로그램을 짤 수 있겠다!!!

하지만 아래와 같은 코드는 ++연산자가 원자적이지 않기 때문에 쓰레드 쎄이프 하지 않습니다.

1
2
3
4
private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber() {
return nextSerialNumber++;
}

위 코드는 synchronized block을 사용해서 해결 할 수 도 있지만 java.util.concurrent.atomic에 속해있는 AtomicLong을 사용하여 해결하는것이 성능적으로 더 좋습니다.

1
2
3
4
private static final AtomicLong nextSerialNumber = new AtomicLong();
public static long generateSerialNumber() {
return nextSerialNum.getAndIncrement();
}

# 결론

간단하게 쓰레드 간에 변수만 공유하는 경우에 대해서 알아봤는데요 아래 나열한 내용을 숙지하고 프로그램 한다면 좋을것 같습니다.

  • 변경가능 데이터는 한 스레드에서만 이용하는 것이 가장 좋다.
  • 하지만 그게 불가능 하다면 변경 가능한 데이터를 읽거나 쓰는 모든 쓰레드는 동기화를 수행해야 함.
  • 원자성이 보장되는 경우는 volatile키워드만으로 안전하게 데이터를 교환할 수 있음.
  • java.util.concurrent.atomic에 속해있는 클래스를 사용하는것은 좋은 해법임.
Share