경진의 블로그

스레드 - 동기화(lock, monitor, synchronized, wait, notify, notifyAll, ThreadLocal) 본문

개인참고자료/자바(네트워크)

스레드 - 동기화(lock, monitor, synchronized, wait, notify, notifyAll, ThreadLocal)

aith 2008. 7. 12. 16:16
동기화

lock, monitor, synchronized

하나의 자원을 여러 스레드가 사용하려고 할때, 한 시점에서 하나의 스레드만 사용하게 할 수 있도록 한다.

lock - 락(제어권)
monitor - 인스턴스의 상태를 검사한다
synchronized - 동기화

공유되는 데이터 영역

객체(힙 영역) > 메소드(메소드 영역) > 변수(런타임 컨스턴트 풀 영역)

힙에 저장된 어떤 객체의 변수 값(런타임 컨스턴트 풀에 저장된 참조 변수, 레퍼런스 변수)이 여러 스레드에 의해 동시에 직간접적으로, 즉 해당 객체의 메소드에 의해 또는 객체의 변수 값이 직접 변경됨으로 인해 공유 문제가 발생한다. 여기서 볼 수 있듯이 동기화 문제가 발생하는 최소 단위는 객체며, 문제 발생지검은 객체가 소유한 내부 변수가 된다.

이런 점 때문에 자바는 동기화 문제를 해결하기 위해 모든 객체에 락(lock, 또는 세마포어(semaphore)라고 부른다)이라는 것을 포함 시켰다. 락은 어떤 객체에 여러 스레드가 동시에 접근하지 못하도록 하기 위한 것으로 모든 객체가 인스턴스화 될 때, 즉 힙 영역에 객체가 저장될 때 자동으로 생성된다. 이렇게 생성된 락은 보통의 경우에는 사용되지 않는다. 다만 동기화가 필요한 부분에서 락을 사용하기 위해 아래와 같이 synchronizeed 키워드를 사용한다.

은행의 입출금을 다루는 애플리케이션을 만들었는대 동기화를 고려하지 않았다고 가정한다.
Java 라는 통장 계좌에 1,000만원이 예금되어 있다. 이때 A 스레드가 이 통장에서 500만원을 인출한다면 당연히 잔고가 더 많기 때문에 출금이 가능하다. 하지만 동시에 B 스레드가 1,000만원을 인출한다면 동시에 진행된 요청이기 때문에 A 스레드가 인출한 금액을 아직 Java 계좌에서 차감하지 않은 상태기 때문에 역시 1,000만원이 인출된다.

Java 계좌에 있던 금액은 1,000만원이였지만 인출된 금액은 1,500만원이 되어 버렸다. 이런 식으로 시스템을 구축해놓은 은행이라면 얼마 버티지 못하고 파산한다.
이처럼 공유자원에 대해 여러 스레드들이 동시에 접근하게 되면 이러한 동기화 문제가 발생할 가능성이 존재한다.

동기화 메소드

접근제어자 synchronized 반환형 메소드_이름(인자 선언) [throws 예외 클래스] {
}

public class SynchornizedTest {
    // 메소드에 syschronized 키워드를 사용한다.
    // 모니터는 현재 클래스 인스턴스의 락을 검사한다.

    public syschronized String drawingOut(String money) throws AccountException {
        // 기타 고객 데이터 초기화 작업
        initSomething();
        // 1. 잔액을 계산한다.
        // 2. 통장의 잔액이 찾을 금액보다 크다면 정상처리하고 반대일 경우 AccountException 을 던진다.
        // 마무리 작을 하고 남은 금액을 리턴한다.

        finish();
        return something;
    }
}

동기화 코드 블록 형태

synchonized (클래스 레퍼런스) {
    코드
}

public class SynchornizedTest {
    public String drawingOut(String money) throws AccountException {
        // 기타 고객 데이터 초기화 작업
        initSomething();
        // 공유 데이터의 대상인 고객의 통장을 락의 대상으로 설정한다.
        // 모니터는 userAccount 인스턴스의 락을 검사한다.

        syschronized (userAccount) {
            // 1. 잔액을 계산한다.
            // 2. 통장의 잔액이 찾을 금액보다 크다면 정상처리하고 반대일 경우 AccountException 을 던진다.
            // 이처럼 필요한 부부문 블록 형태로 동기화 하는것이 더 효율적이다.

        }
        finish();
        return something;
    }
}

synchronized 키워드를 사용하면 모니터(monitor)라는 것이 해당 객체의 락(제어권)을 검사한다. 모니터는 락(제어권)의 현재 사용 여부를 검사함으로써 각 객체를 보호하는데, 락(제어권)과 마찬가지로 모니터도 각 객체의 레퍼런스와 연결되어 있다. 즉, 어떤 클래스를 new 키워드를 사용해서 인스턴스화 하면 락(제어권)과 함께 자동으로 생성된다.

스레드가 sysnchronized 키워드를 사용한 메소드나 블록에 접근하게 되면 그 synchronized와 연관된 모니터는 해당 객체의 레퍼런스를 검사한다. 이때 락(제어권)이 아직 다른 스레드에게 사용되어지지 않고 있다면 JVM에게 알려준다. 그러면 JVM은 'monitorenter' 라는 JVM 내부 명령으로 해당 객체의 락(제어권)을 요청한 스레드에게 준다. 반대로 락(제어권)이 다른 스레드에 의해 사용되고 있다면 락(제어권)이 반환될 때까지 더 이상 진행되지 않고 그 스레드는 대기하게 된다.

스레드가 락(제어권)을 얻은 후에 synchronized 메소드나 블록을 다 마치고 나면 'monitorexit' 라는 JVM 내부 명령을 자동으로 실행해서 해당 스레드가 얻은 객체의 락(제어권)을 즉시 반환한다.

이처럼 메소드 자체에 synchronized를 사용하는 방법과 synchronized 블록을 사용하는 방법있다.
상황에 따라 적절한 것을 선택해서 사용하면 된다. 단, 동기화를 사용하면 성능이 저하된다. '블록 형태의 사용'과 같이 꼭 필요한 부분에 한정해서 사용하자!

※ 동기화를 하면 DeadLock 상태에 올 수 있다

임계영역(Critical Section)
synchronized 키워드나 블록으로 지정된 영역을 가르키는 용어다.
임계영역은 한 스레드가 공유 데이터 영역을 실행하고 있는 동안 다른 스레드는 그 영역에 접근하지 못한다.

wait, notify, notifyAll

스레드간의 협력 작업이 가능하게 하는 메소드다.

객체의 wait()와 notify() 메소드를 호출하려면 반드시 해당 객체의 락(제어권)을 얻어야 한다. 만약 락(제어권)을 얻지 않고 메소드를 호출하려고 시도하면 IllegalMonitorStateException이 발생한다.

wait - 메소드 대기 한다.
notify - 대기 중인 스레드(wait 메소드)를 호출해 대기 중인 스레드를 우선순위가 높은 하나만을 깨운다.
notifyAll - 대기 중인 모든 스레드를 전부 깨운다.

※ notify 에서 실행한다라고 하지 않는 이유는 메소드를 대기 상태에서 실행 상태로 바로 변경되는게 아니라 준비 상태를 거치게 되기 때문이다. 즉, notify를 이용하면 메소드를 실행 전 단계인 준비 상태로 만들어 준다. (notifyAll을 해서 전부 실행하게 된다면 동기화의 의미가 없어지고 우선순위라는게 필요없기 때문이다. 대기 상태를 거쳐 하나씩 실행하게 된다.)

LinkedList 객체에 락(제어권)을 얻은 A 스레드가 LinkedList 객체의 wait() 메소드를 호출했다고 가정한다.

A 스레드는 다른 스레드가 이 객체에 대한 락(제어권)을 얻을 수 있게 하려고 LinkedList 객체의 락(제어권)을 놓고 대기한다. (A 스레드가 대기 상태이다.)

그 후에 B 스레드가 LinkeList 객체의 락(제어권)을 얻은 후 이 객체의 notify() 메소드를 호출해서 대기 상태에 있는 A 스레드를 깨운다. (B 스레드가 제어권을 가지고 A 스레드를 호출한다.)

이 때 B 스레드는 notify() 메소드를 호출하는 시점에서 대기하고 있던 A 스레드가 락(제어권)을 얻을 수 있도록 LinkedList 객체의 락(제어권)을 놓는다. (B 스레드가 A 스레드에게 제어권을 넘긴다.)

그리고 A 스레드는 대기 상태에서 빠져나오면서 LinkedList 객체의 락을 얻어 나머지 로직을 수행하고 동기화 블럭을 빠져나간다. (A 스레드가 정상 종료되었다 제어권을 JVM에게 반환했다.)

그 후에 B 스레드가 LinkeList 객체의 락(제어권)을 다시 얻는다. (제어권을 JVM이 B 스레드에게 넘겨줬다.)

ThreadLocal

동기화 문제가 발생하는 근본적인 원인은 공유 데이터(혹은 자원) 때문이다.

공유 자원의 특정 데이터만 접근하는 각각의 스레드가 다른 값을 갖도록 만들어 유지하고 싶을 때도 있다.
동기화 문제를 피해 이 문제를 해결하는 가장 손쉬운 방법은 공유 자원의 특정 데이터에 대해 자신의 내부에 private 형태의 필드로 만들어 사용한다.

하지만 스레드들이 접근하려는 제3의 객체에 대해 동기화 문제가 발생할 필드를 각각의 스레드가 내부 필드로 정의해서 갖고 있어야 한다는 것은 객체 지향적이지도 못하고 또한 코드의 가독성도 떨어진다.

이런 문제를 ThreadLocal과 InheritableThreadLocal 이라는 클래스로  해결할 수 있다.

ThreadLocal 클래스의 메소드

메소드 설명
public Object get() ThreadLocal 클래스에 설정된 Object 값을 리턴한다.
public void set(Object value) 파라미터로 넘겨진 Object를 현재 스레드 값으로 설정한다.
protected Object initialValue() ThreadLocal 클래스의 초기 값을 설정하고 그 값을 리턴한다.

initialValue() 메소드의 접근자가 protected 이다. ThreadLocal 클래스를 사용하는 이유는 객체에 접근하는 스레드 각각에 대해 다른 값을 갖게 하는 것이므로 아무객체나 접근 할 수 있는 public 접근자를 사용하지 않는 것이 당연하다.

첨부파일



import java.util.Random;
public class ThreadLocalTest {
    // 카운터 변수 생성.
    static volatile int counter = 0;
    // 랜덤 클래스 생성 (스레드를 잠시 재우기 위한 시간을 정하기 위해 사용됨)
    static Random random = new Random();
    // ThreadLocal 을 상속한 ThreadLocalObject 클래스 생성
    private static class ThreadLocalObject extends ThreadLocal { // inner 클래스
        Random random = new Random();
        // 초기값으로 0~999 사이의 랜덤값을 설정.
        // initialValue() 메소드의 접근자가 protected 임에 주의.
        // return 값이 Object 임에 주의.

        protected Object initialValue() {
            return new Integer(random.nextInt(1000));
        }
    }
    // ThreadLocal 의 변수 생성.
    static ThreadLocal threadLocal = new ThreadLocalObject();
    // 각 스레드의 value 출력 메소드.
    private static void displayValues() {
        System.out.println("Thread Name:"
                + Thread.currentThread().getName() // 스레드의 이름
                + ", initialValue:"
                + threadLocal.get() // initialValue의 값을 가져옴
                + ", counter:"
                + counter); // for문을 돌면서 스레드의 카운터를 증가한 동기화 값
    }
    public static void main(String args[]) {
        // main 스레드 value 출력.
        displayValues();
        Runnable runner = new Runnable() { // 추상 메소드를 선언 생성 그리고 사용
            public void run() { // 실행 메소드를 구현
                synchronized (ThreadLocalTest.class) { // 블록 형태의 동기화
                    // 카운터를 1 증가시킴.
                    counter++;
                }
                // value 출력.
                displayValues(); // 메소드 호출 (콘솔창에 출력을 행동을 알고 있는 메소드)
                try {
                    // 스레드로컬의 초기값만큼 멈춤.
                    Thread.sleep(((Integer) threadLocal.get()).intValue());
                    // value 출력.
                    displayValues();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }; // 스레드 추상메소드 끝
        // 스레드 3개를 생성해서 각 스레드 value 를 출력.
        for (int i = 0; i < 3; i++) {
            Thread t = new Thread(runner); // runner를 인자로 갖는 t 스레드를 생성
            t.start(); // 스레드를 준비 상태로 만든다.
        }
    }
}

ThreadLocalObject라는 ThreadLocal 하위 클래스를 만들어서 초기 값으로 0에서 999 사이의 임의 값을 갖게 만들었다 그 후 테스트 스레드 세 개를 만들어서 ThreadLocal 초기 값과 ThreadLocalTest 클래스에서 설정한 카운터 변수의 값을 두 번 출력하게 만들었다.

실제로 JDK 1.4 이후에 추가된 java.nio.charset 패키지의 Charset 클래스와 java.util.logging 패킺지의 LogRecord 클래스에서 실제 구현 예를 볼 수 있다.
0 Comments
댓글쓰기 폼