숑숑이의 개발일기

본글은 'Do it 자바 완전 정복' 책을 통한 공부내용을 정리한 글입니다.

 

동기화의 개념

동기화(sychronized)는 개념적으로 가장 쉽게 표현하면 하나의 작업이 완전히 완료된 후 다른 작업을 수행하는 것을 말한다. 반대로 비동기(asynchronous)는 하나의 작업 명령 이후 완료 여부와 상관없이 바로 다른 작업 명령을 수행하는 것을 말한다.

 

동기화의 필요성

멀티 쓰레드를 사용할 때 왜 동기화가 필요할까? 아래의 예시 코드를 보자.

class MyData {
	int data = 3;
    public void plusData() {
    	int mydata = data;	// 데이터 가져오기
        try {Thread.sleep(2000);} catch(InterruptedException e) {}
        data = mydata + 1;
    }
}

// 공유 객체를 사용하는 쓰레드
class PlusThread extends Thread {
	MyData myData;
    public PlusThread(myData) {
    	this.myData = myData;
    }
    @Override
    public void run() {
    	myData.plusData();
        System.out.println(getName() + "실행 결과: " + myData.data)
    }
}

public class NeedSynchronized {
	public static void main(String[] args) {
    	// 공유 객체 생성
        MyData myData = new MyData();
        
        Thread plusThread1 = new PlusThread(myData);
        plusThread1.setName("plusThread1");
        plusThread1.start();
        
        // 1초 기다림
        try {Thread.sleep(2000);} catch(InterruptedException e) {}
        
        Thread plusThread2 = new PlusThread(myData);
        plusThread2.setName("plusThread2");
        plusThread2.start();
        // plusThread1 실행 결과 : 4
        // plusThread2 실행 결과 : 4
    }
}

2개의 쓰레드가 각각 MyData 객체의 data 필드값을 1씩 증가시켰는데도 두 쓰레드 모두 4의 결괏값을 가진다. 이유는 두 번째 스레드가 data 필드를 증가시키는 시점에 아직 첫 번째 쓰레드의 실행이 끝나지 않았기 때문이다. (이 시점에서 데이터 값은 여전히 3이다)

 

쓰레드의 결괏값이 5가 나오게 하려면 바로 하나의 쓰레드가 완전히 종료된 후 다른 쓰레드를 실행해야 할 것이다. 이렇게 한 쓰레드가 객체를 모두 사용해야 다음 쓰레드가 사용할 수 있도록 설정하는 것동기화라고 한다.

 

동기화 방법

동기화 방법은 크게 메서드 동기화와 블록 동기화로 나뉜다. 메서드 동기화는 2개의 쓰레드가 동시에 메서드를 실행할 수 없다는 것을 의미한다. 블록 동기화는 2개의 쓰레드가 동시에 해당 블록을 실행할 수 없다는 것을 의미한다.

하나의 쓰레드가 공유 객체를 사용할 때 다른 쓰레드가 해당 객체를 사용할 수 없도록 하는 것을 객체를 잠근다(lock)라고 표현한다. 사용을 완료한 후 객체의 잠금을 풀면(unlock) 다른 쓰레드가 사용할 수 있게 된다.

 

메서드 동기화

메서드를 동기화 할때는 메서드의 리턴 타입 앞에 synchronized 키워드를 넣는다.

// 예시 객체
class MyData {
	int data = 3;
    public synchronized void plusData() {
    	// data 필드의 값을 +1 수행
    }
}

이렇게 되면 동시에 2개의 쓰레드에서 해당 메서드를 실행할 수 없게 되므로 아까의 예시 코드의 실행 결과는 이제 의도한 바와 같이 첫 번째 쓰레드의 결과로 4, 두 번째 쓰레드의 결과로 5의 값을 가진다.

 

블록 동기화

멀티 쓰레드를 사용하는 프로그램이라 하더라도 동기화 영역에서는 하나의 쓰레드만 실행할 수 있기 때문에 성능 면에서 많은 손해를 본다. 따라서 동기화 영역은 꼭 필요한 부분에 한정해 적용하는 것이 좋다. 메서드 전체 중에 동기화가 필요한 부분이 일부라면 전체 메서드를 동기화할 필요 없이 해당 부분만 동기화한다. 이것이 블록 동기화다.

// 예시객체
class MyData {
	int data = 3;
    public void plusData() {
    	synchronized (this) {
        	// data 필드의 값을 +1 수행
        }
    }
}

문법의 구조는 synchronized(임의의 객체) {}다. 임의의 객체는 어떤 객체도 올 수 있지만, 일반적으로는 this를 넣어 자기 객체를 가리킨다. this는 모든 클래스 내부에서 객체의 생성 과정 없이 바로 사용할 수 있기 때문이다. 기존의 코드를 블록 동기화 했을 경우 연산 과정이 짧기 때문에 성능상의 차이는 없겠지만, 메서드 내의 코드가 길고 연산 과정이 다수 포함되어 있다면 메서드 동기화와 블록 동기화의 성능상 차이가 존재한다.

 

동기화의 원리

모든 객체는 자신만의 Key를 하나씩 갖고 있다. 블록 동기화 코드synchronized(this){...} 일 때에는 블록이 this 객체가 갖고 있는 열쇠로 잠긴다. 첫 번째로 동기화 블록을 실행하는 쓰레드가 그 열쇠를 갖게 되며, 그 쓰레드가 동기화 블록의 실행을 완료하기 전까지 다른 쓰레드는 열쇠를 얻을 수 없으므로 그 블록을 실행할 수 없다.

블록 동기화의 소괄호 안에는 어떠한 객체가 와도 무방하지만, 메서드를 동기화 하는 경우에는 this 객체의 열쇠만을 사용한다. (하나의 객체 내부에 3개의 동기화 메서드가 있다면 이 3개의 메서드 모두 this의 열쇠로 잠겨있고, 이들 중 1개의 메서드를 실행하고 있다면 나머지 2개의 메서드도 잠겨 사용할 수 없다.)

 

class MyData {
	synchronized void abc() {
    	for (int i = 0; i < 3 ; i++) {
       	// this 객체가 갖고 있는 하나의 열쇠를 함께 사용
        	System.out.println(i + "sec");
            try {Thread.sleep(1000);} catch(InterruptedException e) {}
        }
    }
    synchronized void bcd() {
    	for (int i = 0; i < 3 ; i++) {
        // this 객체가 갖고 있는 하나의 열쇠를 함께 사용
        // abc() 메서드가 끝날 때 까지 해당 메서드는 실행안됨.
        	System.out.println(i + "초");
            try {Thread.sleep(1000);} catch(InterruptedException e) {}
        }
    }
    void cde() {
    	synchronized(new Object()) {
        // Object 객체가 갖고 있는 열쇠 사용
            for (int i = 0; i < 3 ; i++) {
                System.out.println(i + "번째");
                try {Thread.sleep(1000);} catch(InterruptedException e) {}
            }
        }
    }
}

public class KeyObject {
	public static void main(String[] args) {
    	MyData myData = new MyData();
        new Thread() {
        	public void run() { myData.abc() };
        }.start();
        new Thread() {
        	public void run() { myData.bcd() };
        }.start();
        new Thread() {
        	public void run() { myData.cde() };
        }.start();
    }
}
profile

숑숑이의 개발일기

@숑숑-

풀스택 개발자 준비중입니다