본문 바로가기
개발 공부 기록하기/- Kotlin & Java

스레드 동기화 synchronized에 관해서

by soulduse 2016. 7. 5.
반응형

출처 : http://noesse.tistory.com/27


요즘 Multi Thread를 다뤄야 하는 작업이 많아져서 데이터의 동기화작업을 해줘야 하는 경우가 많은데,

확실하게 개념을 파악하지 못하고 대충 값의 변형이 일어나면 synchronized만 붙여서 사용했던것 같다.


잘못 남용했다가 삽질도 많이 했는데, 이 기회에 확실히 집고 넘어가기 위해 구글링을 하던 중, 

좋은 번역을 해주신분이 있어서 이렇게 자료를 가져왔다.


두고두고 생각 안날때마다 보면 좋을것 같아 이렇게 포스팅한다.





이 글은 원 저자 Jakob Jenkov의 허가로 포스팅된 번역물이다.

자바 컨커런시와 자바 메모리 모델에 관한 자료를 찾던 중 발견한 이 튜토리얼의 깔금한 이미지와 예제, 명료한 설명에 반하여 번역-소개한다. 자바 컨커런시 외에도 유이한 자료가 많으니 관심이 있다면 꼭 들러보길 바란다(특히 자바 아키텍처 기반 대용량 웹서비스와 관련이 있다면).

원문 URL : http://tutorials.jenkov.com/java-concurrency/synchronized.html

!! 쉬운 이해를 위한 의역이 다소 포함되었다.


자바 동기화 블록은 메소드나 블록 코드에 동기화 영역을 표시하며 자바에서 경합 조건을 피하기 위한 방법으로 쓰인다.


자바 synchronized 키워드

 자바 코드에서 동기화 영역은 synchronizred 키워드로 표시된다. 동기화는 객체에 대한 동기화로 이루어지는데(synchronized on some object), 같은 객체에 대한 모든 동기화 블록은 한 시점에 오직 한 쓰레드만이 블록 안으로 접근하도록 - 실행하도록 - 한다. 블록에 접근을 시도하는 다른 쓰레드들은 블록 안의 쓰레드가 실행을 마치고 블록을 벗어날 때까지 블록(blocked) 상태가 된다.

synchronized 키워드는 다음 네 가지 유형의 블록에 쓰인다.

  1. 인스턴스 메소드
  2. 스태틱 메소드
  3. 인스턴스 메소드 코드블록
  4. 스태틱 메소드 코드블록

어떤 동기화 블록이 필요한지는 구체적인 상황에 따라 달라진다.


인스턴스 메소드 동기화

다음은 동기화 처리된 인스턴스 메소드이다.

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

메소드 선언문의 synchronized 키워드를 보자. 이 키워드의 존재가 이 메소드의 동기화를 의미한다.

인스턴스 메소드의 동기화는 이 메소드를 가진 인스턴스(객체)를 기준으로 이루어진다. 그러므로, 한 클래스가 동기화된 인스턴스 메소드를 가진다면, 여기서 동기화는 이 클래스의 한 인스턴스를 기준으로 이루어진다. 그리고 한 시점에 오직 하나의 쓰레드만이 동기화된 인스턴스 메소드를 실행할 수 있다. 결국, 만일 둘 이상의 인스턴스가 있다면, 한 시점에, 한 인스턴스에, 한 쓰레드만 이 메소드를 실행할 수 있다. 

인스턴스 당 한 쓰레드이다. 


스태틱 메소드 동기화

스태틱 메소드의 동기화는 인스턴스 메소드와 같은 방식으로 이루어진다.

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

역시 선언문의 synchronized 키워드가 이 메소드의 동기화를 의미한다.

스태틱 메소드 동기화는 이 메소드를 가진 클래스의 클래스 객체를 기준으로 이루어진다. JVM 안에 클래스 객체는 클래스 당 하나만 존재할 수 있으므로, 같은 클래스에 대해서는 오직 한 쓰레드만 동기화된 스태틱 메소드를 실행할 수 있다.

만일 동기화된 스태틱 메소드가 다른 클래스에 각각 존재한다면, 쓰레드는 각 클래스의 메소드를 실행할 수 있다.

클래스 - 쓰레드가 어떤 스태틱 메소드를 실행했든 상관없이 - 당 한 쓰레드이다.


인스턴스 메소드 안의 동기화 블록

동기화가 반드시 메소드 전체에 대해 이루어져야 하는 것은 아니다. 종종 메소드의 특정 부분에 대해서만 동기화하는 편이 효율적인 경우가 있다. 이럴 때는 메소드 안에 동기화 블록을 만들 수 있다.

  public void add(int value){

    synchronized(this){
       this.count += value;   
    }
  }

이렇게 메소드 안에 동기화 블록을 따로 작성할 수 있다. 메소드 안에서도 이 블록 안의 코드만 동기화하지만, 이 예제에서는 메소드 안의 동기화 블록 밖에 어떤 다른 코드가 존재하지 않으므로, 동기화 블록은 메소드 선언부에 synchronized 를 사용한 것과 같은 기능을 한다.

동기화 블록이 괄호 안에 한 객체를 전달받고 있음에 주목하자. 예제에서는 'this' 가 사용되었다. 이는 이 add() 메소드가 호출된 객체를 의미한다. 이 동기화 블록 안에 전달된 객체를 모니터 객체(a monitor object) 라 한다. 이 코드는 이 모니터 객체를 기준으로 동기화가 이루어짐을 나타내고 있다. 동기화된 인스턴스 메소드는 자신(메소드)을 내부에 가지고 있는 객체를 모니터 객체로 사용한다.

같은 모니터 객체를 기준으로 동기화된 블록 안의 코드를 오직 한 쓰레드만이 실행할 수 있다.

다음 예제의 동기화는 동일한 기능을 수행한다.

  public class MyClass {
  
    public synchronized void log1(String msg1, String msg2){
       log.writeln(msg1);
       log.writeln(msg2);
    }

  
    public void log2(String msg1, String msg2){
       synchronized(this){
          log.writeln(msg1);
          log.writeln(msg2);
       }
    }
  }

한 쓰레드는 한 시점에 두 동기화된 코드 중 하나만을 실행할 수 있다.

여기서 두 번째 동기화 블록의 괄호에 this 대신 다른 객체를 전달한다면, 쓰레드는 한 시점에 각 메소드를 실행할 수 있다. - 동기화 기준이 달라지므로.


스태틱 메소드 안의 동기화 블록

다음 예제는 스태틱 메소드에 대한 것이다. 두 메소드는 각 메소드를 가지고 있는 클래스 객체를 동기화 기준으로 잡는다.

  public class MyClass {

    public static synchronized void log1(String msg1, String msg2){
       log.writeln(msg1);
       log.writeln(msg2);
    }

  
    public static void log2(String msg1, String msg2){
       synchronized(MyClass.class){
          log.writeln(msg1);
          log.writeln(msg2);  
       }
    }
  }

같은 시점에 오직 한 쓰레드만 이 두 메소드 중 어느 쪽이든 실행 가능하다.

두 번째 동기화 블록의 괄호에 MyClass.class 가 아닌 다른 객체를 전달한다면, 쓰레드는 동시에 각 메소드를 실행할 수 있다.


자바 동기화 예제

두 쓰레드가 실행되고 이 쓰레드들은 같은 Counter 인스턴스의 add() 메소드를 호출한다. 한 시점에 오직 한 쓰레드만 add() 메소드를 호출할 수 있을 것이다 - 같은 인스턴스를 동기화 기준으로 잡았기 때문에. 왜냐하면 add() 메소드는 자신을 가진 클래스의 인스턴스를 기준으로 동기화되고 있다.

  public class Counter{
     
     long count = 0;
    
     public synchronized void add(long value){
       this.count += value;
     }
  }
  public class CounterThread extends Thread{

     protected Counter counter = null;

     public CounterThread(Counter counter){
        this.counter = counter;
     }

     public void run() {
	for(int i=0; i<10; i++){
           counter.add(i);
        }
     }
  }
  public class Example {

    public static void main(String[] args){
      Counter counter = new Counter();
      Thread  threadA = new CounterThread(counter);
      Thread  threadB = new CounterThread(counter);

      threadA.start();
      threadB.start(); 
    }
  }

두 쓰레드가 생성되었고, 같은 Counter 인스턴스가 각 쓰레드의 생성자로 전달되었다. Counter.add() 메소드는 인스턴스 메소드이기 때문에, Counter.add() 메소드는 생성자로 전달된 Counter 인스턴스를 기준으로 동기화된다. 이로써 한 시점에 두 쓰레드 중 한 쓰레드만이 add() 메소드를 호출할 수 있게 되었다. 한 쓰레드가 add() 메소드를 실행하는 동안 다른 쓰레드는 이 실행이 끝나고 실행 쓰레드가 동기화 블록을 빠져나갈 때까지 기다리게 된다.

여기서 만일 두 쓰레드가 서로 다른 Counter 인스턴스를 전달받았다면, add() 메소드는 동시에 호출될 수 있을 것이다. add() 메소드의 호출은 서로 다른 객체에 의해 이루어지고, 당연히 동기화의 기준이 달라진다. 여기에 쓰레드가 블록 상태에 놓이는 일은 없다.

  public class Example {

    public static void main(String[] args){
      Counter counterA = new Counter();
      Counter counterB = new Counter();
      Thread  threadA = new CounterThread(counterA);
      Thread  threadB = new CounterThread(counterB);

      threadA.start();
      threadB.start(); 
    }
  }

threadA, threadB 는 더이상 같은 인스턴스를 참조하지 않는다. add() 메소드는 각자의 인스턴스를 기준으로 동기화된다. counterA 에 대한 add() 메소드 호출은 counterB 의 add() 를 블록시키지 않는다.


자바 컨커런시 유틸리티

 synchronized 매카니즘은 다수의 쓰레드에게 공유되는 객체로의 접근에 대한 자바의 첫 번째 동기화 매카니즘이었다. 이 매카니즘이 아주 훌륭하지는 못했기 때문에, 이보다 한층 나은 동시성 컨트롤을 위해 자바 5 에서는 컨커런시 유틸리티 클래스들이 출현하게 된다.

반응형