Java

Thread 관련 - Object, ThreadGroup, ThreadLocal, volatile

jamie. 2020. 1. 16. 20:18
반응형

Object 클래스에 선언된 Thread와 관련된 메서드들

/* 다른 Thread가 Object 객체에 대한 notify() 메서드나 notifyAll() 메서드를
 * 호출할 때까지 현재 Thread가 대기하고 있도록 함
 * WAITING 상태로 변경됨
 */
void wait()

/* wait() 메서드와 동일한 기능을 제공
 * 매개 변수에 지정한 시간만큼만 대기
 * 매개 변수 시간을 넘어섰을 때, 현재 Thread는 다시 깨어남
 * 시간은 밀리초로 1/1,000초 단위 - 1초일 경우 1000
 */
void wait(long timeout)

/* wait() 메서드와 동일한 기능을 제공
 * 보다 자세한 밀리초(1/1,000) + 나노초(1/1,000,000,000)만큼 대기
 * 나노초는 0 ~ 999,999까지 지정 가능
 */
void wait(long timeout, int nanos)

/* 모니터
 * = lock 객체처럼 Thread Safe하게 수행하도록 도와주는 객체
 * = synchronized(monitor) { } 이런식으로 사용
 */

// Object 객체의 모니터에 대기하고 있는 단일 Thread를 깨움 - 먼저 대기하는 Thread부터
void notify()

// Object 객체의 모니터에 대기하고 있는 모든 Thread를 깨움
void notifyAll()

사용 예

package jamie.thread;

public class StateThread extends Thread {
    private Object monitor;
    // monitor라는 이름의 객체를 매개변수로 받아 인스턴스 변수로 선언
    public StateThread(Object monitor) {
        this.monitor = monitor;
    }
    public void run() {
        try {
            // Thread를 실행중인 상태로 만들기 위해 간단하게 loop를 돌며 String 객체 생성
            for(int loop = 0; loop < 10000; loop++) {
                String a = "A";
            }
            // synchronized 블럭 안에서 monitor 객체의 wait() 메서드를 호출
            synchronized(monitor) {
                monitor.wait();
            }
            System.out.println(getName) + " is notified");
            // wait() 상황이 끝나면 1초간 대기했다가 해당 쓰레드 종료
            Thread.sleep(1000);
        } catch (InterruptedException ie) {
            ie.printStackTrace();
        }
    }
}
package jamie.thread;

public class RunThreads {
    public static void main(String[] args) {
        RunTreads threads = new RunThreads();
        threads.checkThreadState();
    }
    public void checkThreadState() {
        // StateThread의 매개 변수로 넘겨줄 monitor라는 Object 클래스 작성
        Object monitor = new Object();
        StateThread thread = new StateThread(monitor);
        try {
            System.out.println("thread state = " + thread.getState());
            // Thread 객체를 생성하고 시작
            thread.start();
            System.out.println("thread state(after start) = " + thread.getState());
            
            Thread.sleep(100);
            System.out.println("thread state(after 0.1 sec) = " + thread.getState());
            
            // monitor 객체를 통해 notify() 메서드 호출
            synchronized(monitor) {
                monitor.notify();
            }
            Thread.sleep(100);
            System.out.println("thread state(after notify) = " + thread.getState());
            
            // Thread가 종료될 때까지 기다린 후 상태 출력
            thread.join();
            System.out.println("thread state(after join) = " + thread.getState());
        } catch (InterruptedException ie) {
            ie.printStackTrace();
        }
    }
}

/* 출력 결과
 * thread state = NEW
 * thread state(after start) = RUNNABLE
 * thread state(after 0.1 sec) = WAITING
 * Thread-0 is notified.
 * thread state(after notify) = TIMED_WAITING
 * thread state(after join) = TERMINATED
 */

ThreadGroup에서 제공하는 주요 메서드들

ThreadGroup - Thread의 관리를 용이하게 하기 위한 Class

기본적으로 tree 구조를 가짐 - 즉 다른 그룹에 속할 수도 있고, 다른 그룹을 포함할 수도 있음

// 실행중인 Thread의 개수를 리턴
int activeCount()

// 실행중인 ThreadGroup의 개수를 리턴
int activeGroupCount()

/* enumerate의 리턴값 = 배열에 저장된 값의 개수
 * 위의 개수를 파악하는 메서드로 갯수 파악 후 배열을 맞춰 생성하면 됨
 */
// 현재 ThreadGroup에 있는 모든 Thread를 매개 변수로 넘어온 Thread 배열에 담음
int enumerate(Thread[] list)

/* 현재 ThreadGroup에 있는 모든 Thread를 매개 변수로 넘어온 Thread 배열에 담음
 * 두 번째 매개 변수가 true이면 하위에 있는 ThreadGroup의 Thread 목록도 포함됨
 */
int enumerate(Thread[] list, boolean recurse)

// 현재 ThreadGroup에 있는 모든 ThreadGroup을 매개 변수로 넘어온 ThreadGroup 배열에 담음
int enumerate(ThreadGroup[] list)

/* 현재 ThreadGroup에 있는 모든 ThreadGroup을 매개 변수로 넘어온 ThreadGroup 배열에 담음
 * 두 번째 매개 변수가 true라면 하위의 ThreadGroup 목록도 포함됨
 */
int enumerate(ThreadGroup[] list, boolean recurse)

// ThreadGroup의 이름을 리턴
String getName()

// 부모 ThreadGroup을 리턴
ThreadGroup getParent()

// ThreadGroup 상세정보 출력
void list()

// 지금 ThradGroup에 속한 Thread들을 Daemon으로 지정
void setDaemon(boolean daemon)

ThreadLocal - 각 Thread에서 혼자 쓸 수 있는 값

synchronized - 여러 Thread에서 데이터를 공유할 때 발생하는 문제를 해결

ThreadLocal - Thread 별로 서로 다른 값을 처리해야 할 필요가 있을 때 사용

ThreadLocal

  • ThreadLocal에 저장된 값은 해당 Thread에서 고유하게 사용할 수 있음

  • ThreadLocal 클래스의 변수는 private static final로 선언

  • ThreadLocal 클래스에 선언되어 있는 메서드 : set(), get(), remove(), initialValue()

  • 사용이 끝난 후에는 remove() 메서드를 호출해주는 습관을 들일 것

remove() 메서드를 호출하는 이유
web 기반의 application에서는 Thread를 재사용하기 위해 ThreadPool이라는 것을 사용
ThreadPool을 사용하면, Thread가 시작된 후 그냥 끝나는 것이 아니기 때문에 remove()를 사용해서 값을 지워줘야만 해당 Thread를 다음에 사용할 때 쓰레기 값이 들어있지 않게 됨

예제

ThreadLocal을 사용하지 않고 OtherLogic Class에서 임의로 만든 값을 사용하려면?

  • printMyNumber() 메서드 호출시 매개 변수로 넘기기, 단) OtherLogic에서 호출하는 다른 메서드에서 해당 값을 사용하려면 또 매개 변수로 넘겨줘야 함
  • LocalUserThread에서 인스턴스 변수를 선언하여 사용, 단) 구현이 매우 복잡해질 수 있음
package jamie.thread;

import java.util.Random;

public class ThreadLocalSample {
    /* local이라는 ThreadLocal 객체 생성
     * ThreadLocal은 제네릭한 클래스
     * - 객체를 생성하기 위해서는 각 Thread에서 고유하게 사용할 데이터 타입을 지정해줘야 함
     * 예제는 Integer
     */
    private final static ThreadLocal<Integer> local
      = new ThreadLocal<Integer>();
    private static Random random;
    static {
        random = new Random();
    }
    public static Integer generateNumber() {
        int value = random.nextInt(45);
        // ThreadLocal 클래스의 set() 메서드를 이용해 저장하고자 하는 값 할당
        local.set(value);
        return value;
    }
    public static Integer get() {
        // ThreadLocal 클래스의 get() 메서드를 이용하여 값을 꺼냄
        return local.get();
    }
    public static void remove() {
        // local 객체에 저장되어 있는 값을 제거하기 위해 remove() 메서드 사용
        local.remove();
    }
}

// ThreadLocal 클래스 확장 사용시, initalValue() 메서드를 Override하여 초기 값 지정도 가능함
// 현재 Thread의 이름과 ThreadLocalSample의 get() 메서드를 사용하여 값을 읽음
package jamie.thread;

public class OtherLogic {
    public void printMyNumber() {
        System.out.println(Thread.currentThread().getName()
          + " OtherLogic value = " + ThreadLocalSample.get());
    }
}
package jamie.thread;

public class LocalUserThread extends Thread {
    public void run() { 
        /* 간단하게 ThreadLocalSample의 generateNumber() 메서드 호출
         * 임의의 변수를 ThreadLocal에 저장하고 리턴
         */
        int value = ThreadLocalSample.generateNumber();
        System.out.println(this.getName() + " LocalUserThread value = " + value);
        OtherLogic otherLogic = new OtherLogic();
        // OtherLogic이라는 Class의 printMyNumber란 메서드 호출
        otherLogic.printMyNumber();
        // ThreadLocal에서 값을 지움
        ThreadLocalSample.remove();
    }
}
package jamie.thread;

public class RunThreads {
    public static void main(String[] args) {
        RunTreads threads = new RunThreads();
        threads.runLocalUserThread();
    }
    public void runLocalUserThread() {
        LocalUserThread threads[] = new LocalUserThread[3];
        for (LocalUserThread thread : threads) {
            thread = new LocalUserThread();
            thread.start();
        }
    }
}
/* 실행 결과
 * Thread-1 LocalUserThread value=20
 * Thread-1 OhterLogic value=20
 * Thread-2 LocalUserThread value=01
 * Thread-2 OtherLogic value=01
 * Thread-3 LocalUserThread value=15
 * Thread-3 OtherLogic value=15
 *
 * LocalUserThread에서 출력한 값과 OtherLogic에서 출력한 값이 동일함
 * Thread별로 값을 절대 공유하지 않음
*/

Java의 volatile

변수 선언시에만 사용되는 예약어

 

예제

package jamie.thread;

public class VolatileSample extends Thread {
    private double instanceVariable = 0;
    // 1. volatile 사용
    // private volatile double instanceVariable = 0; 
   
    void setDouble(double value) {
        this.instanceVariable = value;
    }
    public void run() {
        // instanceVariable이 0이면 계속 while문을 돌림
        while (instanceVariable == 0);
        System.out.println(instanceVariable);
    }
    
    /* 2. 대기시간을 1밀리초 줌
    public void run() {
        try {
            while (instanceVariable == 0) {
                Thread.sleep(1)
            }
        } catch (Exception e) {}
        System.out.println(instanceVariable);
    }
    */
}
package jamie.thread;

public class RunThreads {
    public static void main(String[] args) {
        RunTreads threads = new RunThreads();
        threads.runLocalUserThread();
    }
    public void runValatileSample() {
        VolatileSample sample = new VolatileSample();
        sample.start();
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("Sleep ended !!!");
        sample.setDouble(-1);
        System.out.println("Set value is completed !!!");
    }
}

/* 결과
 * Sleep ended !!!
 * Set value is completed !!!
 *
 * 그런데 sample Thread가 종료되지 않음
 * 원인 : CPU Cache 때문
 * Thread에서 수행되는 변수의 값을 반복적으로 참조할 때는 "메인 메모리"에 저장되지 않음
 * "CPU 캐시"에 저장되고 참조됨. 값을 바꾸게 되면, CPU 캐시의 다른 곳에 또 저장되는데,
 * 진행중인 쓰레드는 기존 값(0)을 참조함. 즉 효과 없음. 
 *
 * 1. volatile으로 선언된 Thread의 인스턴스 변수의 경우, 해당 변수 값이 바뀌면 추적을 해줌
 * 같은 객체에 있는 변수는 모든 쓰레드가 같은 값을 바라보게 됨
 * 단) 남발시 성능 저하 발생
 *
 * 모든 Thread의 인스턴스 변수에 volatile이라고 적어주지 않았다고 전부 데이터가 꼬이는건 아닌데,
 * 왜 예제는 volatile을 적지 않았을 때 문제가 생겼을까?
 * - JIT 컴파일러가 Optimization(최적화 작업)을 캐시간에 두고 최적화되었기 때문
 * 2. 즉 최적화가 되지 않아 캐시간 데이터가 다른 값을 보지 않으면 volatile을 사용할 필요가 없음
 *
 * volatile은 데이터에 문제가 있을 경우에만 사용
 */
그 외, ThreadPool을 비롯한 Thread를 처리하는 Class는 java.util.concurrent 패키지의 Class를 참고

 

반응형