synchronized

4 분 소요

자바 Thread에 대한 내용은 자바의 Thread 포스팅을 참고할 것.

자바에서는 synchronized 키워드를 이용해 Multi Thread 환경에서 동기화를 제공한다.

synchronized 키워드는 메서드 앞에 쓰여 메서드에 대한 동기화가 되거나 synchronized 블럭을 만들면서 객체를 전달하여 객체에 대한 동기화를 할 수 있다.

1. instance method 동기화

public synchronized void add(int value){
    this.count += value;
}

위와 같은 instance 메서드 동기화는 자기 instance(this)를 기준으로 동기화 한다.
클래스가 기준이 아니라 객체가 기준이라는 것에 주의하자. 해당 객체에서 synchronized 가 걸린 모든 메서드는 동기화 된다.
즉, 하나의 인스턴스에 대해 여러 Thread에서 동시에 접근할 수 있는 메서드는 단 하나이다.

햇갈리지 말자. 동기화는 객체를 기준으로 되는것이고, synchronized가 걸린 해당 메서드만 동기화 되는게 아니라 해당 클래스에 있는 모든 synchronized 메서드는 동기화된다.

예를 들어 아래와 같은 코드가 있다.


public class BlackOrWhite {
    private String str;
    private final String BLACK = "black";
    private final String WHITE = "white";
    
    public synchronized void black(){
        str = BLACK;
        try {
            long sleep = (long) (Math.random()*100L);
            Thread.sleep(sleep);
            if (!str.equals(BLACK)) {
                System.out.println("++++++broken!!++++++");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public synchronized void white(){
        str = WHITE;
        try {
            long sleep = (long) (Math.random()*100L);
            Thread.sleep(sleep);
            if (!str.equals(WHITE)) {
                System.out.println("++++++broken!!++++++");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Test {
    
    public static void main(String[] args) {
        final BlackOrWhite bow = new BlackOrWhite();
        Thread white = new Thread() {
            public void run() {
                while (true) {
                    bow.white();
                }
            }
        };
        Thread black = new Thread() {
            public void run() {
                while (true) {
                    bow.black();
                }
            }
        };
        white.start();
        black.start();
    }
}

위 코드에서 whtie Thread와 black Thread는 bow 라는 인스턴스를 공유하면서 각자 white()와 black() 메서드를 호출한다.
위 내용을 실행해보면 동기화가 잘 이루어져 broken 이라는 문자는 절때 찍히지 안는다.

서로 다른 Thread에서 각각 black()과 white()를 호출하고 있지만 두 메서드 모두 bow 인스턴스의 메서드 콜이기 때문에 동일한 객체에 대한 동기화가 이루어 진 것이다.

2. static method 동기화

public static synchronized void add(int value){
    count += value;
}

위와 같이 static 메서드 동기화는 이 메서드를 가진 클래스 객체를 기준으로 이루어진다.
JVM 안에 클래스 객체는 클래스 당 하나만 존재할 수 있다.

즉, 인스턴스에 따라서 동기화되는 instance 메서드 동기화와 달리 static 메서드 동기화는 클래스에 대해서 동기화 되어 어떤 인스턴스가 어떤 Thread에서 실행되던지와 무관하게 동시에 접근이 불가능하다.

3. 동기화 블럭을 활용한 객체 동기화

동기화가 반드시 메서드 전체에 대해 이루어져야 하는 것은 아니다. 동기화를 잘못 할 경우 전체 성능을 크게 떨어트리기 때문에 종종 메서드 특정 부분에 대해서만 동기화를 제공하는 것이 효율적일 수 있다.

public void add(int value){
    synchronized(this){
        this.count += value;   
    }
}

위와 같이 메서드 안에서 synchronized 블럭을 따로 쓸 수 있다.
참고로 위와 같이 메서드 내부 전체가 synchronized 블럭으로 잡힌 경우 메서드 자체에 synchronized를 건 것과 동일한 동작을 하게 된다.

동기화 블럭은 객체를 전달받고 있다.(여기에서는 자기 자신 객체인 this가 전달되었다. 자기 자신이라 함은 현재 add를 호출한 객체를 의미한다.)

이와 같이 동기화 블럭안에 전달되는 객체를 모니터 객체 라고 한다. 동기화 블럭은 모니터 객체를 기준으로 동기화가 이루어진다.

현재 메서드 자체에 synchronized를 건것과 동일한 이유는 synchronized를 건 범위가 메서드 구현부 전체이면서 모니터 객체가 this이기 때문이다.

만약 method가 static 이라면 모니터 객체로 자기 자신 클래스(XXX.class)로 하면 역시 동일한 의미를 가진다.

3.1. 모니터 객체 활용

public class TwoMap {
    private Map<String, String> map1 = new HashMap<String, String>();
    private Map<String, String> map2 = new HashMap<String, String>();
    
    public synchronized void put1(String key, String value){
        map1.put(key, value);
    }
    public synchronized void put2(String key, String value){
        map2.put(key, value);
    }
    
    public synchronized String get1(String key){
        return map1.get(key);
    }
    public synchronized String get2(String key){
        return map2.get(key);
    }
}

위 코드는 비효율적으로 동작한다. 실제로 동기화가 이뤄져야 하는 코드는 put1()과 get1() 그리고 put2()와 get2()이다. 하지만 모든 인스턴스 메서드에 synchronized가 걸려있어 하나의 인스턴스는 동시에 하나의 메서드만 사용이 가능하게 되버린다.
실제로는 pu1이 실행됨과 동시에 pu2가 실행되어도 무관한데 말이다.

이 때 모니터 객체를 활용할 수 있다.

public class TwoMap {
    private Map<String, String> map1 = new HashMap<String, String>();
    private Map<String, String> map2 = new HashMap<String, String>();
    private final Object syncObj1 = new Object();
    private final Object syncObj2 = new Object();
    
    public void put1(String key, String value){
        synchronized (syncObj1) {
            map1.put(key, value);
        }
    }
    public void put2(String key, String value){        
        synchronized (syncObj2) {
            map2.put(key, value);
        }
    }
  
    public String get1(String key){
        synchronized (syncObj1) {
            return map1.get(key);
        }
    }
    public String get2(String key){
        synchronized (syncObj2) {
            return map2.get(key);
        }
    }
}

위는 동기화 되어야 하는 쌍인 put1(), get1()이 syncObj1 객체에 의해 동기화 되고 put2(), get2()가 syncObj2 객체에 의해 동기화되어 이전보다 효율적으로 동작한다.

3.2. 클래스 객체 동기화

private void printLog(String log) {

    synchronized (java.lang.Object.class) {
        System.out.println("디버깅 시작했다~");
        System.out.println(log);
        System.out.println("디버깅 끝났다~");
    }
}

위는 모니터 객첼 모든 class의 부모인 Object 클래스 객체에 대해서 동기화를 했다.
이 코드는 어떤 클래스가 어떤 Thread에서 실행되든지 절때 중복해서 실행되지 않는다.

이렇게 클래스를 기준으로 동기화 할 경우 static 메서드 동기화와 같이 같은 클래스에 대해서 동기화 되어 어떤 인스턴스가 어떤 Thread에서 실행되던지와 무관하게 동시에 접근이 불가능하다.

참고로 아래 둘은 같은 동작을 한다.

class MyClass {
    public static synchronized void add(int value){
        count += value;
    }
}
class MyClass {
    public static void add(int value){
        synchronized (MyClass.class) {
            count += value;
        }
    }
}

4. 자바 Concurrency 유틸리티

자바 Concurrent 패키지 관련은 Concurrent 패키지 포스팅을 참고할 것.

synchronized 키워드는 여러 쓰레드에 공유되는 객체에 대한 동기화 매커니즘이다. 이 방식은 아주 훌륭한 방식은 아니었고 이후 보다 나은 동시성 컨트롤을 위해 Concurrency API가 나왔다. (Java 5)

synchronized가 암시적인 lock이라면 concurrent package에 있는 ReentrantLock은 명시적인 lock 객체이다.


[참고 문서]

카테고리:

업데이트:

댓글남기기