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 작업이 여러번 일어났음을 알 수 있다.
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이라는 아무런 관련없는 새 객체를 락 객체로 활용할 수 있었다.