글 검색

공개된 글의 제목과 본문을 검색합니다.

    닫기 Esc

    1. 개요#

    1.1 Java 그리고 Thread#

    • WAS에서는 많은 수의 동시 사용자를 처리하기 위해 수십 ~ 수백개의 Thread를 사용한다.
    • 두 개 이상의 Thread가 같은 자원을 이용할 때는 필연적으로 Thread간에 경합이 발생한다.
      • 경우에 따라 Dead Lock이 발생할 수도 있다.
      • 대표적으로 로그를 기록하려는 Thread가 Lock을 획득하고 공유 자원에 접근한다.
    • Thread 경합 때문에 다양한 문제가 발생할 수 있다.
    • 이런 문제를 분석하기 위해서 Thead Dump를 이용하기도 한다.

    1.2 Thread 동기화#

    • 여러 Thread가 공유 자원을 사용할 때 정합성을 보장하려면 동기화 장치로 한 번에 하나의 Thread만 공유 자원에 접근할 수 있게 해야 한다.
    • Java에서는 Monitor를 이용해 Thread를 동기화 한다.
    • 모든 Java 객체는 하나의 Monitor을 가지고 있다.
      • Monitor는 하나의 Thread만 소유할 수 있다.
    • 특정 Thread가 소유한 Monitor를 다른 Trhead가 획득하려면 해당 Monitor를 소유하고 있는 스레드가 Monitor를 해재헤라 때까지 Wait Queue에서 대기해야 한다.

    1.3 Mutual Exclusion과 Critical Section#

    • 공유 데이터에 다수의 스레드가 동시에 접근하여 작업하면 메모리 Corruption이 발생할 수 있다.
    • 공유 데이터의 접근은 한번에 한 Thread씩 순차적으로 이루어져야 한다.
    • Heap에는 Object의 멤버 변수가 있는데 JVM은 해당 Object와 Class를 Object Lock(광의적 개념)을 사용해 보호한다.
    • Object Lock은 한번에 한 스레드만 Object를 사용하게끔 내부적으로 Mutex같은 걸 활용 한다.
    • JVM이 Class File을 Load할 때 Heap에는 Java.lang.class의 인스턴스가 하나 생성되며 Object Lock은 Java.lang.class Object의 인스턴스에 동기화 작업하는 것이다.
    • 이 Synchronization은 DBMS의 Lock과는 좀 다르다.
      • 오라클의 경우 SELECT는 배타적 Lock을 안걸지만, DML일 경우 배타적 락을 건다.
      • Java는 Thread가 무슨 작업을 하던 동기화가 필요한 지역에 들어가면 무조건 동기화를 수행한다.
        • 이 지역을 Critical Seciton이라 하는데 스레드가 이 지역에 들어가면 반드시 동기화가 필요하다.
      • 스레드가 Object의 임계 영역에 진입할 때 동기화를 수행해 Lock을 요청하는 방식
        • Lock을 획득하면 임계 영역에서 작업이 가능하며
        • Lock획득에 실해하면 Lock을 소유한 다른 스레드가 Lock을 놓을 때까지 대기한다.
      • Java는 Object에 대해 Lock을 중복해서 획득하는게 가능
        • 스레드가 특정 Object의 임계영역에 진입할 때마다 Lock을 획득하는 작업을 다시 수행한다는 것이다.
        • Object의 Header에는 Lock Counter를 가지고 있다.
          • Lock 획득시엔 +1 Lock 해제시엔 -1
          • Lock을 소유한 스레드만 가능하다.
          • Count가 0일때 다른 스레드가 Lock을 획득할 수 없고 스레드가 반복해 Lock을 획득하면 카운트가 증가한다.
    • Critical Section은 Object Reference와 연계해 동기화를 수행한다.
      • 스레드는 임계영역의 첫 Instruction을 수행할 때 참조하는 Object에 대해 Lock을 획득해야 한다.
      • 임계영역을 떠날 때 Lock은 자동 Release되며 명시적인 작업은 불필요
      • 임계영역을 지정해주면 동기화는 자동으로 이뤄짐

    1.4 Monitor#

    • Java는 기본적으로 멀티 스레드 환경을 전제로 설계되었고 동기화 문제를 해결하기 위한 기본적 매커니즘을 제공한다.
    • 앞서 설명한 Object Lock이 Monitor에 해당한다.
    • 특정 Object의 모니터에는 동시에 하나의 스레드만이 들어갈 수 있다.
    • 다른 스레드에 의해 이미 점위된 모니터에 들어가고자 하는 스레드는 모니터의 Wait Set에서 대기한다.
    • 모니터를 점유하는 유일한 방법은 synchronized 키워드를 사용하는 것
      • synchronized는 Statement와 Method 두 가지 방법이 있다.
      • Statement는 메소드 내 특정 코드에 synchronized 키워드를 사용한 것
        • 락을 수행하는 작업이 바이트코에 명시적으로 나타나는 특징이 있음
      • Method는 메소드 전체에 synchronized키워드를 사용한 것

    2. Java의 동기화 방법#

    2.1 Synchronized Statement#

    java
    ...
    private int[] intArr = new int[10];
    void syncBlock() {
        synchronized (this) {
            for (int i = 0; i < intArr .length; i++) {
                intArr[i] = i;
            }
        }
    }
    ...
    • 스레드는 for 구문이 실행되는 동안 Object의 모니터를 점유한다.
    • 해당 Object에 대해 모니터를 점유하려는 모든 스레드는 for구문이 실행되는 동안 대기 상태에 빠진다.
    • 바이트코드에서 MONITORENTER가 실행되면 Lock Count를 1증가시킨다.
    • MONITOREXIT을 실행하게 되면 Lock Count를 하나 감소시킨다.
    • Synchronized Statement의 사용은 내부적으로 try ~ catch 절을 사용하는 효과가 있다.
      • 획득시에 획득을 못하면 대기상태로, 감소시에 카운트가 0이면 락이 해제되기 때문

    2.2 Synchronized Method#

    java
    ...
    class SyncMtd {
    		private int[] intArr = new int[10];
    		synchronized void syncMethod() {
            for (int i = 0; i < intArr .length; i++) {
                intArr[i] = i;
            }
    		}
    }
    ...		
    • Synchronized Method는 Method를 선언할 때 Synchronized 접근지정자를 사용하는 방식이다.
    • 이 방식은 바이트코드에 모니터 Lock관련 내용이 없다.
    • 왜냐하면 Synchronized Method에 대해 모니터 락의 사용 여부는 메소드 symbolic reference를 resolution하는 과정에서 결정되기 때문이다.
    • 메소드 내용이 임계영역이 아니고 메소드의 호출 자체가 임계영역이란 것을 의미한다.
    • 이 방법은 메소드를 호출하기 위해 Lock을 획득해야 한다.
    • 메소드 동기화가 Instance Method라면 Method를 호출하는 this Object에 대해 Lock을 획득해야 한다.
    • Synchronized Method가 정상 여부 상관없이 종료되기만 하면 JVM은 Lock을 자동으로 해제한다.

    2.3 Wait And Notify#

    • 한 스레드는 특정 데이터를 필요로 하고 다른 스레드는 특정 데이터를 제공하는 경우 Monitor Lock을 사용해 스레드간 Cooperation 작업을 수행할 수 있다.
    • 메신저 프로그램의 경우 클라이언트에서 네트워크를 통해 상대방의 메시지를 받는 Listener 스레드와 받아온 메시지를 보여주는 Reader 스레드가 있다고 가정해보자.
    • Reader 스레드는 버퍼의 메시지를 유저에게 보여주고 버퍼를 다시 비우고 버퍼에 메시지가 들어올 때까지 대기하게 된다.
    • Listener 스레드는 버퍼에 메시지를 기록하고 어느 정도 기록이 끝나면 Reader 스레드에게 메시지가 들어온 사실을 알려줘 Reader 스레드가 메시지를 읽는 작업을 수행할 수 있도록 한다.
    • 이때 스레드간 Wait and Notify 형태의 모니터 락을 사용한다.
      • Wait과 Notify Method를 이요해 동기화를 수행하는 방식은 Synchronized의 응용이라 할 수 있다.
    • Cooperation을 위한 모니터 락을 표현한 것이다.
    • Reader 스레드는 모니터락을 소유하고 있고 버퍼를 비운 다음 wait()을 수행한다.
      • 자신이 소유한 모니터를 잠시 놓고 이 모니터를 대기하는 Wait Set으로 들어간다.
    • Listener 스레드는 메시지를 받고 이를 유저가 읽어야 할 때쯤 notify()를 수행하여 Wait Set에서 나와도 된다는 신호를 알리면 Reader 스레드가 모니터락을 바로 획득하지 못할 수도 있다.
      • Listener 스레드가 자발적으로 모니터 락을 놓지 않으면 누구도 모니터 락을 획득할 수 없다.
      • 스레드간 Cooperation도 Mutual exclusion처럼 Object Lock를 사용한다.
      • 즉 스레드들은 특정 Object Class의 wait(), notify()등의 Method를 통해 모니터 락을 사용하는 것
    • 스레드가 Entry Set으로 진입하면 바로 모니터 락 획득을 시도한다.
      • 다른 스레드가 모디터 락을 획득했다면 후발 스레드는 엔트리 셋에서 대기해야 한다.
      • 모니터락을 소유한 스레드가 작업 수행 중 wait()을 수행하면 획득했던 모니터락을 놓고 Wait Set으로 들어간다.
    • wait()만 수행하고 Wait set으로 들어가면 이 모니터 락을 획득할 수 있는 권한은 Entry set의 스레드들에게만 주어진다.
    • notify()는 Wait set에 있는 스레드 중 임의의 한 스레드만을 모니터 락 경합에 참여시킨다.
    • notifyAll()은 모든 스레드들을 경쟁에 참여시킨다.
    • Wait set에 들어온 스레드가 임계영역을 벗어나는 방법은 모니터를 다시 획득해 락을 놓고 나가는 방법 이외에는 없다.
      • JVM 벤더마다 다르지만 모니터 락은 성늘상의 이유로 자주 사용하지 않는 것이 추세이다.
    • 앞서 말한 모니터락을 Heavy-weight Lock, Light-weight Lock 으로 나눈다.
      • Heavy-weight Lock은 모니터 락과 동일한 개념
        • 뮤텍스 같은 OS 자원을 이용하지 않고 Operation 만으로 동기화 처리해 모니터 락에 비해 가벽다는 장점이 있다.
      • Light-weight Lock은 Atomic operation을 이용한 가벼운 Lock으로 뮤텍스와 같은 OS의 자원을 사용하지 않고 내부의 Operation 만으로 동기화 처리해 모니터 락에비해 가볍다는 장점이 있다.
        • Object의 경우 스레드간의 경합이 발생하지 않는다는 점에 착안하여 만약 스레드간 경합 없이 자신이 락을 소유한 채 다시 Object

    2.4 Synchronized Statement와 synchronized Method 사용#

    • 여러스레드가 동시에 Access 할 수 있는 객체는 무조건 Synchronized Statement/Method로 보호해야 하는걸까?
      • 항상 그렇지는 않다.
      • Synchronized를 수행하는 코드와 그렇지 않은 코드의 성능 차이는 대단히 큰데 동기화를 위해 모니터에 액세스하는 작업에는 오버헤드가 따른다.
    java
    private static Instance instance = null;
    public static synchronized getInstace() {
        if(instance == null)
            instace = new Instance(...);
        return instance;
    }
    • Singleton 방식을 구현하기 위해 getInstance Method을 Synchronized로 잘 보호했지만 불필요한 성능감소가 있다.
    • Instance 변수가 실행 도중에 변경될 가능성이 없다면 위 코드는 비효율적이다.

    3. Thread 상태#

    • Thread dump를 분석하려면 스레드의 상태를 알아야 한다.
    • Thread의 상태는 Java.lang.Thread클래스 내부에 State이름을 가진 열거타입으로 선언되어 있다.
    • NEW
      • Thread가 생성되었지만 아직 실행되지 않은 상태
    • RUNNABLE
      • 현재 CPU를 점유하고 작업을 수행중인 상태
      • 운영체제의 자원 분배로 인해 WAITING상태가 될 수 있다.
    • BLOCKED
      • 모니터를 획득하기 위해 다른 Thread가 Lock을 해제할 때까지 기다리는 상태
    • WAITING
      • wait(), join(), park() 메소드 등등을 이용해 대기하는 상태
    • TIMED_WAITING
      • sleep(), wait(), join(), park() 등등 의 메소드를 이용해 대기하는 상태
      • WAITING과 다른점은 메소드 인수로 최대 대기 시간을 명시할 수 있다.
      • 외부적인 변화뿐 아니라 시간에 의해 WAITING상태가 될 수도 있다.

    4. Thread의 종류#

    • 자바의 Thread는 데몬 Thread와 비데몬 Thread로 나뉜다.
    • 데몬 스레드는 다른 비데몬 스레드가 없다면 동작을 중지한다.
    • 사용자가 직접 스레드를 생서하지 않더라도 자바는 기본적으로 여러 스레드를 생성한다.
    • 대부분이 데몬 스레드로 GC나 JMX 등 작업을 처리하기 위한 것이다.
    • VM Background Thread
      • Compile, Optimization, Garbage Collection 등 JVM내부의 일을 수행하는 백그라운드 스레드
    • Main Thread
      • main 메소드를 실행하는 Thread
      • 사용자가 명시적으로 스레드를 수행하지 않더라도 JVM이 Main Thread를 생성
      • Hotspot JVM에서는 VM Thread라는 이름이 부여
    • User Thread
      • 사용자에 의해 명시적으로 생성된 Thread 들이다.
      • Java.lang.Thread 를 상속받거나, Java.lang.Runnable 인터페이스를 구현함으로써
        스레드를 생성할 수 있다.

    5. JVM에서의 대기 현상 분석#

    5.1 Java에서 제공하는 방법#

    • Thread Dump, GC Dump와 같은 기본적인 툴
    • BCI (Bytecode Instrumentation) + JVMP/TI (C Interface)
    • Java 5 에서 표준으로 채택된 JMX의 Platform MXBean, JVMPI/TI를 통해 얻을수 있던 정보를 쉽게 획득 가능하지만 아직 부족한 면이 많다.
    • 대부분의 WAS는 Thread Pool, Connection Pool, EJB Pool/Cache와 같은 개념들을 구현했는데 이런 Pool/Cache들에서 대기 현상이 파악된다.
    • 대부분의 WAS가 이런 성능정보를 JMX API를 통해 제공하고 맘만 먹으면 자신의 성능 Repository를 만들 수도 있다.

    5.2 비효율 소스 튜닝에 따른 Side Effect#

    • Loop을 돌면서 DML을 수행하는 구조에서 해당 작업을 여러 Thread가 동시에 수행한다고 할 때 오라클 튜너가 이를 파악하고 모든 App을 Batch Execution으로 변환하게 끔 유도를 하였다.
    • DB와 통신이 획기적으로 줄고 DB 작업 자체의 일량도 줄어들었기 때문에 선능이 증가해야하지만 APP에서 극단적인 성능저하가 발생하게 된다.
    • 이유로는 두 가지가 있다.
      • Batch Execution은 APP에서 더 많은 메모리를 요구한다.
        • GC가 더 왕성하게 발생한다.
      • Batch Execution은 한번의 Operation에 Connection을 보유하는 시간이 좀 더 길다.
        • 더 많은 Connection이 필요하기 때문에 Connection Pool이 금방 소진된다.

    6. Thread Dump#

    6.1 Thread Dump 생성 방법#

    • Linux/Unix 계열 = kill -3 [PID]
    • Windows 계열 = 콘솔에서 Ctrl + Break
    • 공통 = jstack [PID]

    6.2 Thread Dump 정보 의미#

    전체 글 둘러보기