동기화(Synchronization)

동기화는 특정 객체를 동시에 접근하여 변경하기를 막는 기법입니다. 가장 빈번하게 사용되는 예를 들어볼까요? 은행 입출금과 화장실의 예가 있는데 저는 화장실이 더 좋더군요.

 

우리가 고깃집에서 소주와 맥주를 거나하게 마셔서 화장실을 갈때 열쇠가 있지요? 여기서 화장실은 한개이고 열쇠 또한 한개라고 칩시다. 열쇠를 갖고 화장실에서 볼일을 보려고 문을 잠글겁니다. 문을 잠그고 나서는 각자 취향대로 볼일을 보고 물을 내리고 열쇠를 제자리에 걸어놓습니다.  화장실의 키를 갖고 열고 잠그는 것이 일종의 동기화입니다.

여러분들은 알게 모르게 동기화를 실천하고 있는 것이지요.

 

만약 이 과정에서 동기화가 빠지게 된다면 어떻게 될까요? 제가 화장실을 쓰고 있는데, 다른 사람이 와서 제 무릎에다가 지리는 현상이 발생하겠죠. 

 

이렇듯 멀티쓰레드(Multithread) 환경에서 어떤 쓰레드(사람)가 자원(화장실 열쇠)을 이용하는데 동시에 그 자원을 사용하면 안될때 동기화가 필요합니다.

 

자바에서 이러한 동기화 기법을 적용하는 방법은 두가지가 있습니다.

첫번째로는 특정 객체의 메소드에 synchronized 예약어를 사용하는 방법과 synchronized lock을 사용하는 방법입니다.

 

synchronized 사용하여 동기화

우선 동기화를 걸지 않은 코드를 먼저 봅시다. key라는 객체는 원래 HAS-A 관계가 적절하지만 이해를 돕기위해서 그런건 고려하지 않았습니다. 코드는 그렇게 어렵지 않습니다. 

쓰레드 3개를 생성하고 각각 알기쉽게 사람 이름을 붙여줍니다. 이 쓰레드는 이제 사람이라고 치고 key를 사용하여 볼일을 본다면 어떻게 될까요?

 

 

 

 

class Key {
	
	public void open(String name){
		System.out.println(name+"이(가) 화장실 문을 연다.");
	}
	
	public void close(String name){
		System.out.println(name+"이(가) 화장실 문을 닫는다.");
	}
	public void defecate(String name){
		System.out.println(name+"이(가) 싼다.");
	}
	public synchronized void useToilet(String name){
		
		open(name);
		defecate(name);
		close(name);
	}
	
}

class MyThread extends Thread{
	private String name;
	private Key key;
	public MyThread(String name,Key key){
		this.name=name;
		this.key=key;
	}
	public void run(){
		key.useToilet(name);
	}
}
public class ThreadTest {

	public static void main(String[] ar) throws Exception{
		Key key=new Key(); //이 객체를 쓰레드 3개에서 사용
		MyThread thread1=new MyThread("철수",key);
		MyThread thread2=new MyThread("영희",key);
		MyThread thread3=new MyThread("영철",key);
		
		thread1.start();
		thread2.start();
		thread3.start();
	}
	
}

 

아래 결과처럼 철수가 싸는 중에 영희가 철수 무릎위에 볼일을 보고, 그 다음 영철이가 들어와서 또 한번 철수 무릎에 볼일을 봅니다. 그 다음 철수가 볼일을 보네요.

철수이(가) 화장실 문을 연다.
영희이(가) 화장실 문을 연다.
영희이(가) 싼다.
영희이(가) 화장실 문을 닫는다.
영철이(가) 화장실 문을 연다.
영철이(가) 싼다.
영철이(가) 화장실 문을 닫는다.
철수이(가) 싼다.
철수이(가) 화장실 문을 닫는다.

 

철수를 위해 동기화를 할 필요가 있겠죠? 가장 간단한 방법은 동기화할 메소드에 synchronized 만 추가하면 됩니다. 

useKey메소드를 다음과 같이 변경합시다.

public synchronized void useKey(String name){
	open(name);
	defecate(name);
	close(name);
}

그리고 실행합시다.

철수이(가) 화장실 문을 연다.
철수이(가) 싼다.
철수이(가) 화장실 문을 닫는다.
영철이(가) 화장실 문을 연다.
영철이(가) 싼다.
영철이(가) 화장실 문을 닫는다.
영희이(가) 화장실 문을 연다.
영희이(가) 싼다.
영희이(가) 화장실 문을 닫는다.

저희는 이렇게 철수를 도울 수 있습니다.

 

Synchronized Block으로 원하는 코드부분만 동기화

Synchronized로 동기화를 거는 건 무척 쉽네요. 하지만 그 메소드 전부를 동기화하기 때문에 불필요한 곳까지 동시에 실행할 수가 없습니다.

다음의 예를 보세요. 위의 코드에서 몇줄밖에 추가하지 않았습니다.

class Key {
	
	public void lookIntoAMirror(String name){
		System.out.println(name+"이(가) 거울을 본다.");
	}
	public void open(String name){
		System.out.println(name+"이(가) 화장실 문을 연다.");
	}
	public void close(String name){
		System.out.println(name+"이(가) 화장실 문을 닫는다.");
	}
	public void defecate(String name){
		System.out.println(name+"이(가) 싼다.");
	}
	public synchronized void useToilet(String name){
		lookIntoAMirror(name);
		open(name);
		defecate(name);
		close(name);
	}
	
}

class MyThread extends Thread{
	private String name;
	private Key key;
	public MyThread(String name,Key key){
		this.name=name;
		this.key=key;
	}
	public void run(){
		key.useToilet(name);
	}
}
public class ThreadTest {

	public static void main(String[] ar) throws Exception{
		Key key=new Key();
		MyThread thread1=new MyThread("철수",key);
		MyThread thread2=new MyThread("영희",key);
		MyThread thread3=new MyThread("영철",key);
		
		thread1.start();
		thread2.start();
		thread3.start();
	}
	
}

 

거울을 보는것까지 동기화를 걸었네요. 우리는 그럴 필요없습니다. 거울은 누가 먼저보든 상관없으니까요. 볼일 보는 것만 정확히 동기화를 하려고 한다면 block을 이용해야합니다.

 

 

 

 

아래와 같이 원하는 부분만을 동기화할 수 있습니다. 

class Key {
	
	public void lookIntoAMirror(String name){
		System.out.println(name+"이(가) 거울을 본다.");
	}
	public void open(String name){
		System.out.println(name+"이(가) 화장실 문을 연다.");
	}
	public void close(String name){
		System.out.println(name+"이(가) 화장실 문을 닫는다.");
	}
	public void defecate(String name){
		System.out.println(name+"이(가) 싼다.");
	}
	public void useToilet(String name){
		lookIntoAMirror(name);
		synchronized(this){
			open(name);
			defecate(name);
			close(name);
		}
	}
	
}

class MyThread extends Thread{
	private String name;
	private Key key;
	public MyThread(String name,Key key){
		this.name=name;
		this.key=key;
	}
	public void run(){
		key.useToilet(name);
	}
}
public class ThreadTest {

	public static void main(String[] ar) throws Exception{
		Key key=new Key();
		MyThread thread1=new MyThread("철수",key);
		MyThread thread2=new MyThread("영희",key);
		MyThread thread3=new MyThread("영철",key);
		
		thread1.start();
		thread2.start();
		thread3.start();
	}
	
}

 

거울은 순서없이 먼저 들어온 사람이 봅니다.

철수이(가) 거울을 본다.
영희이(가) 거울을 본다.
철수이(가) 화장실 문을 연다.
철수이(가) 싼다.
영철이(가) 거울을 본다.
철수이(가) 화장실 문을 닫는다.
영철이(가) 화장실 문을 연다.
영철이(가) 싼다.
영철이(가) 화장실 문을 닫는다.
영희이(가) 화장실 문을 연다.
영희이(가) 싼다.
영희이(가) 화장실 문을 닫는다.

 

synchronized block의 인자는 사실 잠글(lock 시킬) 객체를 말합니다. this이니까 바로 key객체 자신이겠죠.

lock 시킬 객체를 한번 바꿔서 실행하도록 하지요.

 

1) 아래는 this(Counter객체)를 통해서 lock을 시켰습니다. 

class Counter{
	public Integer cnt=0;
	
	public void increase(String threadName){
		synchronized(this){ 
			for(int i=0;i<10;i++)
				System.out.println(threadName+", cnt:"+(cnt++));
			System.out.println();
		}
	}
	
}
class MyThread extends Thread{
	private String threadName;
	private Counter counter;
	public MyThread(String threadName,Counter counter){
		this.threadName=threadName;
		this.counter=counter;
	}
	public void run(){
		counter.increase(threadName);
	}
}
public class ThreadTest {

	public static void main(String[] ar) throws Exception{
		Counter counter=new Counter();
		MyThread thread1=new MyThread("thread1",counter);
		MyThread thread2=new MyThread("thread2",counter);
		
		thread1.start();
		thread2.start();
		
		thread1.join();
		thread2.join();
		System.out.println(counter.cnt);
	}
	
}

 

 

2) 다음의 코드는 cnt객체로 lock을 시켰습니다.

public void increase(String threadName){
  synchronized(cnt){
  	for(int i=0;i<10;i++)
  		System.out.println(threadName+", cnt:"+(cnt++));
  		System.out.println();
	}
}

 

3) 마지막으로 동기화를 걸지 않은 코드입니다.

public void increase(String threadName){
	
	for(int i=0;i<10;i++)
		System.out.println(threadName+", cnt:"+(cnt++));
	System.out.println();
	
}

 

1)의 결과는 thread1의 increase, thread2의 increase 둘 중 먼저 실행되는 것이 끝난 이후에 다른 쓰레드의 메소드가 실행되며 마지막 결과도 20으로 항상 동일합니다. this로 lock을 걸었기 때문에 다른 쓰레드의 increase가 끝난 이후에 다른 쓰레드의 increase가 실행됩니다. 

 

2)의 결과는 thread1의 increase, thread2의 increase 둘 중 무작위로 실행되며 실행이 끝난 이후 마지막 결과가 20으로 항상 동일합니다. 이 경우 cnt로 lock을 걸었기 때문에 cnt가 쓰이지 않는 순간(cnt++가 되지 않는 찰나의 순간) 다른 쓰레드가 cnt++을 실행할 수 있습니다.

 

3)의 결과는 동기화를 걸지 않았기 때문에 결과가 매 수행마다 다를 수 있습니다.

 

보통은 1)의 방법으로 동기화를 많이 구현하게 됩니다.

 

각각의 결과를 비교해보시기 바랍니다.

반응형
블로그 이미지

REAKWON

와나진짜

,