서버 개발/Thread

Java Thread - (3) 동기화

Johnny Yoon 2019. 8. 5. 19:07
728x90
반응형

 

 

 

 

 

동기화

Thread는 독자적입니다. 이 말은 즉 보통은 다른 Thread를 신경쓰지 않고 자신의 작업을 수행한다는 의미입니다. 멀티프로세서 기반의 시스템에서는 보통 여러개의 CPU에서 각각 많은 양의 Thread들이 동시다발적으로 작업을 수행합니다. 이번 포스팅에서는 이 많은 양의 Thread들이 어떻게 협업하고, 동기화 되는지, 그리고 공유 자원을 접근할 때에는 어떤 방식으로 할 수 있는지에 대해 알아보겠습니다.

 

Lock

Thread들을 동기화 할때는 락이라는 개념을 사용합니다. 이 Lock은 보통 자원을 하나의 Thread만 접근 가능하게 잠궈놓는다는 의미로 쓰입니다. 하나의 Thread가 자원을 접근하고 있을 때, 다른 Thread가 같은 자원에 접근한다면, 이 자원은 의도하지 않은대로 변경이 될 수가 있습니다. 이때 사용하는것이 Lock입니다. Thread는 공유자원에 접근할 때 이 Lock을 걸어놓고 다른 Thread가 접근하지 못하게 합니다. 그리고 본인의 작업이 끝나면, 이 Lock을 풀고, 다른 Thread가 접근할 수 있게 만드는 것입니다.

 

Synchronized

Synchronized가 바로 자바에서 Lock의 구현체 입니다. 내부적으로 Synchronized는 공유 자원에 접근하는 Thread를 하나로 제한하는 로직을 가지고 있습니다.

아래는 Synchronized를 사용한 예제입니다.

wait이나 notify메소드를 사용하려면, synchronized block 안에서 사용해야 합니다.

따라서 아래와 같이 그냥 메소드 안에서 사용하면 에러가 납니다.

(이 예제 외에도, 공유 자원을 접근해야하고 순서를 보장해야 하는데 synchronized를 걸어주지 않으면 문제가 생기는 경우에 사용합니다.)

 

Producer

보통 메시지큐 (MessageQueue)를 사용할 때, 메시지를 만드는 쪽을 Producer, 사용하는 (확인하는)쪽을 Consumer라고 부릅니다. 아래의 예제에서 Queue의 사이즈는 5개이고, 사이즈가 꽉 차거나 0이 되면 wait을 사용해 기다리게 하고, 반대의 상황이면 notify를 해 재개하도록 합니다.

class Producer implements Runnable{
    static final int MAXQUEUE = 5;
    private List messages = new ArrayList();

    @Override
    public void run() {
        while (true){
            putMessage();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void putMessage(){
        while(messages.size() >= MAXQUEUE){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        messages.add(new Date().toString());
        notify();
    }

    public String getMessage(){
        while(messages.size() == 0){
            notify();
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        String message = (String) messages.remove(0);
        notify();
        return message;
    }
}

 

Consumer

아래는 Consumer클래스를 구현한 것입니다.

class Consumer implements Runnable{
    Producer producer;

    Consumer(Producer producer){
        this.producer = producer;
    }

    @Override
    public void run() {
        while (true){
            String message = producer.getMessage();
            System.out.println("Got Message: "+message);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

 

Main

Synch라는 클래스를 이름으로 main메소드를 작성합니다.

public class Synch {
    public static void main(String[] args) {
        Producer producer = new Producer();
        Consumer consumer = new Consumer(producer);

        Thread producerThread = new Thread(producer);
        Thread consumerThread = new Thread(consumer);

        producerThread.start();
        consumerThread.start();
    }
}

 

실행 및 코드 수정

위의 코드를 실행하면 에러가 납니다.

wait과 notify를 사용하는데 synchronized키워드를 넣어주지 않았기 떄문입니다.

이를 위해 아래 두개의 메소드에 synchronized키워드를 추가해주겠습니다.

    private synchronized void putMessage(){

    ...

    public synchronized String getMessage(){
    
    ...

위를 수정하고 실행하면, 연속적으로 아래와 같이 시간 메시지를 출력하는 것을 볼 수 있습니다.

Got Message: Mon Aug 05 16:04:03 KST 2019
Got Message: Mon Aug 05 16:04:05 KST 2019
Got Message: Mon Aug 05 16:04:06 KST 2019
Got Message: Mon Aug 05 16:04:07 KST 2019
Got Message: Mon Aug 05 16:04:08 KST 2019
Got Message: Mon Aug 05 16:04:09 KST 2019
...

 

Synchronized Block

메소드 시그니처에 synchronized를 사용하는 것 외에도, synchronized block을 사용하는 방법이 있습니다.

아래와 같이 synchronized와 중괄호 사이에 필요한 코드를 넣으면 됩니다.

    private void putMessage(){
        while(messages.size() >= MAXQUEUE){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        synchronized (this) {
            messages.add(new Date().toString());
            notify();
        }
    }

 

이 방법은 메소드에서 수행하는 로직이 많을 때 주로 사용됩니다.

로직의 수행 시간이 긴데, synchronized가 필요없는 코드들까지 동기화를 시켜주기보다는,

동기화가 꼭 필요한 부분만 시켜주면 될 때 사용하면 됩니다.

 

 

참고자료: Learning Java, 4th Edition, Orielly 

 

728x90
반응형

'서버 개발 > Thread' 카테고리의 다른 글

Java Thread - (2) 제어  (0) 2019.07.19
Java Thread - (1) 기본 개념  (0) 2019.07.17