본문 바로가기

Java

synchronized를 이용한 쓰레드의 동기화

728x90

이번 포스팅은 쓰레드의 동기화를 위한 synchronized에 대하여 작성하였습니다. 


싱글 쓰레드가 아닌 멀티 쓰레드로 프로세스를 구성할 때, 같은 자원을 공유해서 작업을 하게 됩니다. 따라서 서로의 작업에 영향을 미치게 되고 이로 인해 의도했던 것과는 다른 결과를 얻을 수 있게 되는 문제점이 있습니다. 아래 예시를 통해 어떤 문제점이 발생하는지 확인해 보겠습니다.


 

public class Main {
    public static void main(String[] args) {
        Runnable r1 = new Thread_0();
        new Thread(r1).start();
        new Thread(r1).start();
    }
}
class Thread_0 implements Runnable {
    TargetInstance ti = new TargetInstance();

    @Override
    public void run() {
        ti.method();
        System.out.println(ti.getCount());
    }
}
class TargetInstance {

    int count = 0;

    public void method() {
          try {
              Thread.sleep(1000);
          } catch (InterruptedException e) {}
          count++;
    }

    public int getCount() {
        return count;
    }
}

개발자가 의도한 결과는 아래와 같이 1, 2를 차례대로 출력하는 것입니다.

1
2

하지만 해당 코드를 여러 번 실행하다 보면 아래와 같이 2, 2를 출력하는 경우도 있습니다.

2
2

그 이유는 두 쓰레드가 동일한 객체의 자원(count)을 서로 공유하기 때문입니다. 왜 위와 같은 결과가 나왔을지 한 번 고민해 보시고 아래 설명을 확인해 보세요!

 

[결과 이유]

더보기

1. 첫 번째 쓰레드가 먼저 method()에 진입하여 1초 동안 잠들고 count를 1 증가시킵니다. (count: 1)

2. 두 번째 쓰레드는 첫 번째 쓰레드가 잠든 후 뒤따라서 1초 동안 바로 잠이 들게 됩니다. 두 번째 쓰레드가 깨어날 때쯤 첫 번째 쓰레드는 count를 증가시키고 있을 것입니다. (count:1)

3. 첫 번째 쓰레드가 TargetInstance의 count 값을 가지고 오려고 막 getCount() 함수를 호출하기 직전에 두 번째 쓰레드가 count를 증가시키게 될 것입니다. 그러면 count는 2가 되겠죠? (count: 2)

4. 최종적으로 첫 번째 쓰레드는 count가 1인 값을 출력하고 싶었지만, 이미 두 번째 쓰레드에 의해 count 값은 2가 되어 있고, 따라서 2를 출력하게 됩니다.    

 

이러한 일이 발생하지 않도록 하기 위해서 사용하는 방식은 synchronized를 이용하여 공유 데이터를 이용하는 코드 영역을 임계 영역(critical section)으로 정해놓고 잠금(lock)을 걸어, 한 쓰레드만 접근가능하도록 하는 것입니다.  

 


synchronized를 이용한 동기화

synchronized를 이용하는 방법은 메서드 동기화와 참조변수 동기화 이렇게 2가지 방법이 있습니다. 


메서드 동기화

메서드 전체에 lock 걸어 한 쓰레드만 메서드에 진입가능하도록 하는 역할을 합니다. 아래와 같이 메서드 앞에 synchronized를 붙여 정의할 수 있습니다.


public synchronized void method1(){}

참조변수 동기화

메서드 전체에 lock을 걸 필요가 없고 일부 코드만 lock을 걸어야 할 때, 즉 임계영역이 메서드 전체가 아니라 메서드 내의 일부 코드 블록일 때, 위의 방법으로 메서드 전체에 lock을 걸면 시간적인 측면에서 효율적으로 코드가 동작할 수 없습니다. 이때 사용하는 방법이 이 두 번째 방법입니다.

메서드 내의 일부 코드 영역을 블록 { }으로 감싸고 블록 앞에 synchronized(참조변수)를 붙입니다. 이때, 참조변수는 lock을 걸고자 하는 객체를 참조하는 변수여야 합니다. 

 


public void method(){
        ...
        synchronized(this){
            ...
        }
}

 

아래 예시를 통해 어떻게 synchronized가 동작하는지 확인할 수 있습니다.


public class Main {
    public static void main(String[] args) {
        Runnable r1 = new Thread_0();
        new Thread(r1).start();
        new Thread(r1).start();
    }
}
class Thread_0 implements Runnable {
    TargetInstance ti = new TargetInstance();

    @Override
    public void run() {
        ti.method();
        System.out.println(ti.getCount());
    }
}
class TargetInstance {

    int count = 0;

    public void method() {
        synchronized (this) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {}
            count++;
        }
    }

    public int getCount() {
        return count;
    }
}

 

[결과]

1
2

만약 method() 내에 synchronized를 사용하지 않았다면, 결과는 운이 좋으면 위의 결과와 같게 나오고, 운이 나쁘면 아래와 같이 나오게 됩니다.

2
2

이 synchronized를 효율적으로 사용할 수 있도록 도와주는 wait()과 notify() 메서드에 대한 내용도 함께 알면 좋을 것 같습니다! 아래 포스팅을 참고해주세요.

https://silver-programmer.tistory.com/entry/synchronized%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%93%B0%EB%A0%88%EB%93%9C%EC%9D%98-%EB%8F%99%EA%B8%B0%ED%99%94

 

synchronized를 이용한 쓰레드의 동기화

이번 포스팅은 쓰레드의 동기화를 위한 synchronized에 대하여 작성하였습니다. 싱글 쓰레드가 아닌 멀티 쓰레드로 프로세스를 구성할 때, 같은 자원을 공유해서 작업을 하게 됩니다. 따라서 서로

silver-programmer.tistory.com

 

감사합니다!!

 


[참고자료]

남궁 성, [Java의 정석 3rd Edition], 도우출판, 2016

728x90
반응형

'Java' 카테고리의 다른 글

Lock과 Condition (await()과 signal(),signalAll())  (2) 2023.10.29
wait()과 notify(), notifyAll()  (2) 2023.10.29
제네릭(Generic) 타입의 형변환  (0) 2023.10.29
Thread 실행 제어  (0) 2023.10.27
제네릭(Generic)이란?  (0) 2023.10.25