본문 바로가기

PROGRAMMING/JAVA

[ JAVA 정복 ] Thread(쓰레드) - 2

6. 쓰레드 그룹(Thread group)

: 쓰레드 그룹은 서로 관련된 쓰레드를 그룹으로 다루기 위한 개념. 쓰레드 그룹은 보안상의 이유로 도입된 개념으로, 자신이 속한 쓰레드 그룹이나 하위 쓰레드 그룹은 변경할 수 있지만 다른 쓰레드 그룹의 쓰레드를 변경할 수는 없다.


: 쓰레드를 쓰레드 그룹에 포함시키려면 Thread의 생성자를 이용해야 한다.

모든 쓰레드는 반드시 쓰레드 그룹에 포함되어 있어야 하기 때문에 위와 같이 쓰레드 그룹을 지정하는 생성자를 사용하지 않은 쓰레드는 기본적으로 자신을 생성한 쓰레드와 같은 쓰레드 그룹에 속하게 된다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class ThreadGroupEx {
    public static void main(String args[]) {
        // getThreadGroup() : 쓰레드 자신이 속한 쓰레드 그룹을 반환한다.
        // main 자체도 쓰레드이기 때문에 main 이라는 ThreadGroup은 현재 쓰레드 그룹을 반환하여 정의함.
        ThreadGroup main = Thread.currentThread().getThreadGroup();
        ThreadGroup grp1 = new ThreadGroup("Group1");
        ThreadGroup grp2 = new ThreadGroup("Group2");
        // grp1 하위에 subGrp1 이라는 쓰레드 그룹을 생성
        ThreadGroup subGrp1 = new ThreadGroup(grp1, "SubGroup1");
        
        grp1.setMaxPriority(3); // 쓰레드 그룹 grp1의 최대우선순위를 3으로 설정
        Runnable r = new Runnable() {
            public void run() {
                try {
                    Thread.sleep(1000); // 쓰레드를 1초간 멈추게 한다.
                } catch (InterruptedException e) {}
            }
        };
        
        // Thread 생성자 - Thread(ThreadGroup group, Runnable target, String name)
        new Thread(grp1, r, "th1").start();
        new Thread(subGrp1, r, "th2").start();
        new Thread(grp2, r, "th3").start();
        // getName() : 쓰레드 그룹의 이름을 반환한다.
        // 출력 결과 : >> List of ThreadGroup : main
        System.out.println(">> List of ThreadGroup : " + main.getName());
        // activeGroupCount() : 쓰레드 그룹에 포함된 활성상태에 있는 쓰레드 그룹의 수를 반환한다.
        // 출력 결과 : >> Active ThreadGroup : 3
        System.out.println(">> Active ThreadGroup : " + main.activeGroupCount());
        // activeCount() : 쓰레드 그룹에 포함된 활성상태에 있는 쓰레드의 수를 반환한다.
        // 출력 결과 : >> Active Thread : 4
        System.out.println(">> Active Thread : " + main.activeCount());
        // list() : 쓰레드 그룹에 속한 쓰레드와 하위 쓰레드그룹에 대한 정보를 출력
        main.list();
    }
}
cs


7. 데몬 쓰레드

: 데몬 쓰레드는 다른 일반 쓰레드(데몬 쓰레드가 아닌 쓰레드)의 작업을 돕는 보조적인 역할을 수행하는 쓰레드이다.

: 일반 쓰레드가 모두 종료되면 데몬 쓰레드는 강제적으로 자동 종료되는데 그 이유는 데몬 쓰레드가 일반 쓰레드의 보조 역할을 수행하므로 일반 쓰레드가 모두 종료되고 나면 데몬 쓰레드의 존재의 의미가 없기 때문이다.


: 데몬 쓰레드는 일반 쓰레드의 작성 방법과 실행 방법이 같다. 다만 쓰레드를 생성한 다음 실행하기 전에 setDaemon(true)를 호출하기만 하면 된다. 그리고 데몬 쓰레드가 생성한 쓰레드는 자동적으로 데몬 쓰레드가 된다는 점도 알아두자.


ex) 가비지 컬렉터, 워드프로세서의 자동저장, 화면자동갱신


8. 쓰레드의 실행제어

쓰레드의 상태에 대한 이미지 검색결과

출처 : http://shin6666.tistory.com/entry/%EC%93%B0%EB%A0%88%EB%93%9CThread


① 쓰레드를 생성하고 start()를 호출하면 바로 실행되는 것이 아니라 실행대기열에 저장되어 자신의 차례가 될 때까지 기다려야 한다. 실행대기열은 큐(queue)와 같은 구조로 먼저 실행대기열에 들어온 쓰레드가 먼저 실행된다.


- 쓰레드의 상태#NEW : 쓰레드가 생성되고 아직 start()가 호출되지 않은 상태


② 실행대기상태에 있다가 자신의 차례가 되면 실행상태가 된다.


- 쓰레드의 상태#RUNNABLE : 실행 중 또는 실행 가능한 상태


③ 주어진 실행 시간이 다 되거나 yield()를 만나면 다시 실행대기상태가 되고 다음 차례의 쓰레드가 실행상태가 된다.


- 쓰레드 스케쥴링#yield() : 쓰레드 자신에게 주어진 실행 시간을 다음 차례의 쓰레드에게 양보하도록 한다.


④ 실행 중에 suspend(), sleep(), wait(), join(), I/O block에 의해 일시정지상태가 될 수 있다. I/O block은 입출력 작업에서 발생하는 지연상태를 말한다. 사용자의 입력을 기다리는 경우를 예로 들 수 있는데, 이런 경우 일시정지 상태에 있다가 사용자가 입력을 마치면 다시 실행대기상태가 된다.


- 쓰레드의 상태#BLOCKED : 동기화 블럭에 의해서 일시 정지된 상태(lock이 풀릴 때까지 기다리는 상태)

- 쓰레드의 상태#WAITING, TIMED_WAITING : 쓰레드의 작업이 종료되지는 않았지만 실행가능하지 않은(unrunnable) 일시정지 상태. TIMED_WAITING은 일시정지기간이 지정된 경우를 의미한다.

- 쓰레드 스케쥴링#suspend() : sleep()처럼 쓰레드를 멈추게 한다. suspend()에 의해 정지된 쓰레드는 resume()을 호출해야 다시 실행대기 상태가 된다.

- 쓰레드 스케쥴링#sleep() : 지정된 시간(1/1000초 단위)동안 쓰레드를 일시정지시킨다. 지정한 시간이 지나면 자동적으로 다시 실행대기상태가 된다.

- 쓰레드 스케쥴링#wait() : wait() 메서드를 호출하면 락을 해제(즉, synchronized 블록 내에서 호출되어야 한다.)하고, 스레드는 잠이 든다. 누군가 깨워줄 때 까지 wait()은 리턴되지 않는다.

- 쓰레드 스케쥴링#join() : 쓰레드 자신이 하던 작업을 잠시 멈추고 다른 쓰레드가 지정된 시간동안 작업을 수행하도록 할 때 사용한다. 시간을 지정하지 않으면 해당(타켓) 쓰레드가 작업을 모두 마칠 때까지 기다리게 된다. 작업중에 다른 쓰레드의 작업이 먼저 수행되어야 할 필요가 있을 때 사용한다.


⑤ 지정된 일시정지시간이 다 되거나(time-out), notify(), resume(), interrupt()가 호출되면 일시정지 상태를 벗어나 다시 실행대기열에 저장되어 자신의 차례를 기다리게 된다.


쓰레드 스케쥴링#notify() : 잠들어 있던 스레드 중 임의로 하나를 골라 깨운다. notify() 메서드는 어느 스레드를 깨울지 선택할 수 없기 때문에 제어가 어렵다.

- 쓰레드 스케쥴링#resume() : suspend()에 의해 일시정지 상태에 있는 쓰레드를 실행대기상태로 만든다.

- 쓰레드 스케쥴링#interrupt() : sleep()이나 join()에 의해 일시정지 상태인 쓰레드를 깨워서 실행대기상태로 만든다.


⑥ 실행을 모두 마치거나 stop()이 호출되면 쓰레드는 소멸된다.


- 쓰레드의 상태#TERMINATED : 쓰레드의 작업이 종료된 상태


** 설명을 위한 1번부터 6번까지 번호를 붙이기는 했지만 번호의 순서대로 쓰레드가 수행되는 것은 아니다.


9. 쓰레드의 동기화

: 멀티쓰레드 프로세스의 경우 여러 쓰레드가 같은 프로세스 내의 자원을 공유해서 작업하기 때문에 서로의 작업에 영향을 주게 된다. 만일 쓰레드A가 작업하던 도중에 다른 쓰레드 B에게 제어권이 넘어갔을 때, 쓰레드 A가 작업하던 공유 데이터를 쓰레드 B가 임의로 변경하였다면 다시 쓰레드 A가 제어권을 받아서 나머지 작업을 마쳤을 때 원래 의도했던 것과는 다른 결과를 얻을 수 있다.


: 이러한 일이 발생하는 것을 방지하기 위해서 한 쓰레드가 특정 작업을 끝마치기 전까지 다른 쓰레드에 의해 방해받지 않도록 하는 것이 필요하다. 그래서 도입된 개념이 바로 '임계 영역(critical section)'과 '잠금(lock)'이다.

공유 데이터를 사용하는 코드 영역을 임계 영역으로 지정해놓고, 공유 데이터(객체)가 가지고 있는 lock을 획득한 단 하나의 쓰레드만 이 영역 내의 코드를 수행할 수 있도록 한다.

→ 쓰레드 A 가 임계 영역 내의 모든 코드를 수행하고 벗어나서 lock을 반납, 쓰레드 B가 반납된 lock을 획득하여 임계 영역의 코드를 수행할 수 있게 함.

= 쓰레드의 동기화(synchronization), 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하기 못하도록 막는 것.


9.1 synchronized를 이용한 동기화


1
2
3
4
5
6
7
class SynchronizedEx {
    // 1. 메서드 전체를 임계 영역으로 지정
    public synchronized void calcSum() {}
 
    // 2. 특정한 영역을 임계 영역으로 지정
    synchronized(객체의 참조변수) {}
}
cs


: 두 방법 모두 lock의 획득과 반납이 자동적으로 이루어지므로 우리가 해야 할 일은 그저 임계 영역만 설정해주는 것 뿐이다. 임계 영역은 멀티 쓰레드 프로그램의 성능을 좌우하기 때문에 가능하면 메서드 전체에 lock을 거는 것보다는 synchronized 블럭으로 임계 영역을 최소화해서 보다 효율적인 프로그램을 작성해야 함.


ex) 출금(출금을 하는 중에 또 다른 출금이 이중으로 생겨버리면 잔고 대비 출금 금액 확인 로직이 어긋날 수 있음. 그래서 synchronized 설정을 통해 출금이 순서대로 이루어지도록 함)


9.2 wait()과 notify()

: synchronized로 동기화해서 공유 데이터를 보호하는 것까지는 좋은데 특정 쓰레드가 객체의 락을 가진 상태로 오랜 시간을 보내지 않도록 하는 것도 중요하다. 이러한 상황을 개선하기 위해 고안된 것이 wait()과 notify()이다. 동기화된 임계 영역의 코드를 수행하다가 작업을 더 이상 진행할 상황이 아니면, 일단 wait()을 호출하여 쓰레드가 락을 반납하고 기다리게 한다. 그러면 다른 쓰레드가 락을 얻어 해당 객체에 대한 작업을 수행할 수 있게 된다. 


: notify()가 호출 되면 대기하고 있던 쓰레드 중에서 임의의 쓰레드를 깨우는 것이고 notifyAll()은 대기하고 있던 쓰레드 모두를 깨우는 것이다. notifyAll()의 경우 모든 대기 쓰레드를 깨우지만 결국 lock을 획득할 수 있는 것은 하나이기 때문에 lock을 얻지 못한 쓰레드는 다시 대기 상태로 돌아간다.