2 minute read

Mutial Exclusion (상호배제)

임계영역(데이터, 메소드…)에는 동시에 하나의 프로세스만 접근할수 있다.

atomic (원자성)

쪼갤수 없는 연산을 의미한다. 자바언어의 경우 long, double 타입 이 아니면 reading, writing 하는 행위는 atomic(더이상 쪼갤수 없음) 하다.

변경가능한 공유 대이터를 접근할때는 Synchronize 시킨다.

synchronize는 동기화라는 뜻으로 여러 이벤트를 잘 조화시키는것을 뜻한다. (coordination of events to operate a system like conductor of orchestra), 프로그램관점에서의 동기화는 작업을 할때 이전의 변경상황들이 전부 반영된 상태를 가지고 작업을 할수 있도록 도와주는 개념이다.

예를 들어 자바언어의 synchronized 키워드는 한번에 오직 하나의 쓰레드만 메소드나 블럭을 실행하도록 막고, (mutual exclusion) 항상 최신의 데이터에 작업을 할수 있도록 도와서 동기화를 달성한다.

/**
 * block에 사용하는 예제
 */
public void add(int value){
	synchronized(this){
		this.count += value;
	}
}

StopThread 예제

public class StopThread {
		private static boolean stopRequested;
		public static void main(String[] args) throws InterruptedException {
		    Thread backgroundThread = new Thread(() -> {
        int i = 0;

        /**
         * 컴파일러에 의해 다음과 같이 해석된다.
         * if (!stopRequested)
				      while (true)
								i++;
         */
        while (!stopRequested)
           i++;
        });
        backgroundThread.start();
        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
}

stopRequested 는 atomic 하긴 하지만, 컴파일러가 의도와 반대로 해석을 할수 있다. 그래서 위코드는 무한루프를 돈다고 한다. 그래서 아래와 같이 synchronized 키워드를 통해 상호배제와 동기화를 하여 항상 최신의 상태를 기반으로 stopRequested 변수를 읽어올수 있도록 아래와 같이 고칠수 있다.

public class StopThread {
    private static boolean stopRequested;

		/**
     * 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(() -> {
								int i = 0;
								while (!stopRequested()) i++;
				    });
         backgroundThread.start();
         TimeUnit.SECONDS.sleep(1);
				 requestStop(); 
		}
}

아래와 같이 voltaile 키워드를 사용해서 상호배제를 제외하고 동기화만 사용하는 방법도 있다.

public class StopThread {
		/**
     * CPU의 캐시가 아닌 항상메모리에서 읽어온다.
     */
		private static volatile boolean stopRequested;

    public static void main(String[] args) throws InterruptedException {
		    Thread backgroundThread = new Thread(() -> {
		        int i = 0;
            while (!stopRequested)
		            i++;
        });
        backgroundThread.start();
        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
		}
}

하지만 voltaile은 상호배제가 없다. 따라서 원자적이지 않은 연산 (++) 가 있다면 문제가 발생할수 있다. synchronized 를 사용해서 달성가능하지만 AtomicLong등의 클래스를 통해 lock-free 하게 달성할수 있다. 자바가 제공하는 아토믹 클래스들은 compare and swap 방식을 통해 연산에 들어가기전에 캐시와 메모리의 값을 비교해서 같은경우에 진행, 다른경우에는 캐시 갱신후 재시도를 통해 동기화를 돕는다.

/**
 * ++연산은 데이터를 가져오고 증가시키는 2가지 연산으로 구성된다. (원자적이지 않다)
 * 두개의 쓰레드가 데이터를 가져오고 증가시키면 nextSerialNumber 가 1만 증가될수도 있다!
 */

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

동기화를 달성하는 가장 좋은 방법은 변경될수 있는 데이터를 쓰레드간에 공유하지 않는것이다. 만약 변경될수 있는 데이터를 쓰레드간에 공유하면 반드시 동기화를 달성하는것이다.

참고자료

Effective java 11장, 78절

Leave a comment