규칙 66 - 변경 가능 공유 데이터에 대한 접근은 동기화하라.

JAVA/Effective Java|2018. 6. 10. 12:48

우리는 동시에 사용이 가능한 객체에 대해서 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
import java.util.concurrent.TimeUnit;
 
public class Main {
    private static boolean stopRequested;
    
    public static void main(String args[]) throws InterruptedException {
        
        Thread backgroundThreat = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while (!stopRequested) {
                    System.out.println(i);
                    i++;
                }
            }
        });
        
        backgroundThreat.start();
        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
    
}
 
cs


 예상을 한다면 1초 후에 프로그램의 반복이 종료될 것 같으나, 종료되지 않는다. 왜그럴까? 이는 메인 Thread에서 stopRequest의 값을 변경하였어도 backgroundThreat가 언제 그 변경된 값을 인지 할지는 알 수 없기 때문이다. 그래서 정확하게 1초에 종료가 되지 않는다.

-> main Thread에서 stopRequest에 값을 변경하여 메모리에 값을 변경하려고 하는 순간에 backgroundThreat 스레드에서는 그 전에 상태를 읽어가기 때문이다.

-> 이는 JVM에서 호이스팅을 하는 작업때문이다. (자세히는 나도 모르겠다.)

-> 이를 위해서는 동기화작업이 필요하다.


 그래서 이렇게 변경되는 객체에 대해서 정확하게 공유하고 싶을 때는 변경되는 객체에 대해서도 동기화 처리를 해주어야 한다.


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
32
33
import java.util.concurrent.TimeUnit;
 
public class Main {
    private static boolean stopRequested;
    
    public static synchronized void requestStop() {
        stopRequested = true;
    }
    
    public static synchronized boolean checkRequestStop() {
        return stopRequested;
    }
    
    public static void main(String args[]) throws InterruptedException {
        
        Thread backgroundThreat = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while (!checkRequestStop()) {
                    System.out.println(i);
                    i++;
                }
            }
        });
        
        backgroundThreat.start();
        TimeUnit.SECONDS.sleep(1);
        requestStop();
    }
    
}
 
cs

읽기 그리고 쓰기 메서드인 requestStop(), checkRequestStop() 메서드를 모두 동기화 처리 해주었다. 쓰는 작업과 읽는 작업이 모두 동기화가 되지 않으면 사실 동기화 작업은 의미가 없다.


이렇게 해결하는 방법은 비용이 크지 않지만 그래도 비용을 조금이라도 더 줄일 수 있는 방법이 있다.

바로 stopRequested를 volatile로 선언하는 것이다. 그러면 락없이도 사용할 수 있다. volatile은 상호 배제성은 해소되지 않지만 어떤 스레드에서 값에 접근할 때 가장 최근에 기록된 값을 읽도록 할 수있다.


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
import java.util.concurrent.TimeUnit;
 
public class Main {
    private volatile static boolean stopRequested;
    
    public static void main(String args[]) throws InterruptedException {
        
        Thread backgroundThreat = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while (!stopRequested) {
                    System.out.println(i);
                    i++;
                }
            }
        });
        
        backgroundThreat.start();
        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
    
}
 
cs


volatile에 대한 설명은 이 블로그에 자세히 나와있다.

(각 쓰레드에서 데이터를 각 cpu의 캐시에서 데이터를 읽어가기 때문에 한쪽 쓰레드에서 변경한 값이 메인메모리에 아직 적용되기 전에는 다른 쓰레드에서는 이값을 알지 못하기 때문에 원자성이 깨진다. 그래서 volatile을 사용하여 원자성을 지켜준다.)

http://thswave.github.io/java/2015/03/08/java-volatile.html


결론은 변경 가능한 데이터를 공유할 때는 해당 데이터를 읽거나 쓰는 모든 스레드는 동기화를 수행해야 한다는 것이다.

상호 배제와 원자성을 확보할 때는 synchronize를 사용하고 원자성만 필요할 때는 volatile을 사용하면 되지만 사용이 까다롭다. 잘 사용하시기를..



출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 규칙66

댓글()