Java synchronized 이해하기

이세민·2025년 2월 4일
0

동시성 문제

Java는 Thread를 이용한 멀티스레딩 프로그래밍을 지원하는데, 이로인해 동시성 문제가 생길 수 있다.

public class SynchronizedTest {
    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            while (counter.getCount() > 0) {
                counter.decrement();
                System.out.println(Thread.currentThread().getName() + " : " + counter.getCount());
            }
        });
        Thread t2 = new Thread(() -> {
            while (counter.getCount() > 0) {
                counter.decrement();
                System.out.println(Thread.currentThread().getName() + " : " + counter.getCount());
            }
        });
        t1.start();
        t2.start();
    }
}

class Counter {
    private int count = 10;

    public void decrement() {
        if(count > 0){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {}
            count-=1;
        }
    }

    public int getCount() {
        return count;
    }
}

위 코드를 실행하게 되면 아래와 같은 결과를 얻을 수 있다.

Thread-0 : 9
Thread-1 : 9
Thread-0 : 7
Thread-1 : 7
Thread-0 : 5
Thread-1 : 5
Thread-1 : 4
Thread-0 : 4
Thread-0 : 3
Thread-1 : 3
Thread-0 : 2
Thread-1 : 2
Thread-0 : 1
Thread-1 : 1
Thread-0 : -1

count가 0보다 클 때만 decrement를 실행하도록 하였는데도, -1이 출력되는 모습을 보여준다. 또한 count의 초기값은 10으로, decrement가 10번만 실행되어야 하지만 위 결과의 9,9 / 7,7 / 5,5 처럼 같은 수에서의 decrement 작업이 여러번 일어났음을 알 수 있다.

Synchronized

Java의 synchronized는 이러한 동시성 문제를 lock을 이용해 해결한다. 사용방법은 method 선언부에 synchronized를 붙이거나 method 내부에 synchronized(Object){}블럭을 추가하면 된다. 사용시 synchronized로 묶인 부분은 한번에 한 스레드만 수행할 수 있다.

작동방식

Java에서는 Object의 instance 마다 monitor lock이라는것을 가진다.sychronized(object){} 구문을 사용하면, 스레드는object의 monitor lock을 요청하고, 이미 다른 스레드에서 사용중이라면 monitor lock이 다시 반환되어 사용가능 할 때 까지 기다린다. method 부분에 synchronized를 추가한 경우엔 method 내부 전체를 syncrhonized(this){}로 감싼것과 같이 작동한다.

예제

class SynchronizedCounter {
    private int count = 10;
    private final Object lock = new Object();

    public void decrement() {
        synchronized (lock) {
            if(count > 0){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {}
                count-=1;
            }else{
                System.out.println("Decrement Cancelled");
            }
        }
    }

    public int getCount() {
        return count;
    }
}

처음 코드에서 Counter를 synchronzied를 사용하도록 바꾸면 이렇게 된다.

Thread-0 : 9
Thread-1 : 8
Thread-0 : 7
Thread-1 : 6
Thread-0 : 5
Thread-1 : 4
Thread-0 : 3
Thread-1 : 2
Thread-0 : 1
Thread-1 : 0
Decrement Cancelled
Thread-0 : 0

실행하게되면 이렇게 9~0까지의 숫자가 차례대로 출력되고, 마지막에선 -1대신 0이 출력되는 모습을 보여준다.

헷갈린 부분

'락을 얻는다', '락을 건다'라는 표현 때문에 synchronized가 해당 객체에 대해 다른 스레드의 접근을 막는다. 라고 생각했는데 실제로는 그렇게 작동하지 않았다. synchronized는 객체를 잠근다기 보다는, 그 객체의 monitor lock을 얻어, 같은 객체의 monitor lock을 요청하는 다른 스레드를 기다리게 만든다. 작동방식을 보면 락이라는 단어보다는 오히려 monitor lock이 명령을 수행하기 위한 잠금을 푸는 열쇠처럼 생각된다.
이러한 작동방식 때문에 위 예제 코드에서도 lock이라는 아무런 관련없는 새 객체를 락 객체로 활용할 수 있었다.

profile
gsm 8기 고등학생

0개의 댓글