본문

쓰레드(Thread)

반응형

# 쓰레드(Thread)

# 프로그램과 프로세스


# 프로세스와 쓰레드

- 프로세스 : 실행 중인 프로그램, 자원(resources)과 쓰레드로 구성

- 쓰레드 : 프로세스 내에서 실제 작업을 수행. 모든 프로세스는 하나 이상의 쓰레드를 가지고 있다.



프로세스 : 쓰레드 = 공장 : 일꾼


# 멀티 프로세스와 멀티 쓰레드

하나의 새로운 프로세스를 생성하는 것보다 하나의 새로운 쓰레드를 생성하는 것이 더 적은 비용이 든다.


- 2 프로세스 1 쓰레드 vs. 1 프로세스 2 쓰레드



# 멀티 쓰레드의 장점

- CPU의 사용률을 향상시킨다.

- 자원을 보다 효율적으로 사용할 수 있다.

- 사용자에 대한 응답성향상된다.

- 작업이 분리되어 코드가 간결해진다.


여러 모로 좋다.


# 멀티 쓰레드의 단점

- 동기화(synchronization)에 주의해야 한다.

- 교착상태(dead-lock)가 발생하지 않도록 주의해야 한다.

- 각 쓰레드가 효율적으로 고르게 실행될 수 있게 해야 한다.


"프로그래밍할 때 고려해야 할 사항들이 많다."


# 쓰레드의 구현과 실행

쓰레드를 구현하는 방법은 

1. Thread 클래스를 상속받는 방법

2. Runnable인터페이스를 구현하는 방법


총 2가지가 있다. 이 두가지 방법 중 어느 쪽을 사용해도 별 차이는 없지만 Thread클래스를 상속받으면 다른 클래스를 상속받을 수 없기 때문에(JAVA- 다중상속 지원x), Runnable인터페이스를 구현하는 방법이 일반적이다.


Runnable인터페이스를 구현하는 방법은 재사용성(reusabliity)이 높고, 코드의 일관성(consistency)을 유지할 수 있다는 장점이 있기 떄문에 보다 객체지향적인 방법이라 할 수 있겠다.


1. Thread클래스를 상속

1
2
3
class MyThread extends Thread {
public void run() {/* 작업내용 */}   // Thread클래스의 run()을 오버라이딩
}
cs


2. Runnable인터페이스 구현

Runnable인터페이스는 run()메서드만 정의되어 있는 간단한 인터페이스이다. 

1
2
3
class MyThread implements Runnable {
public void run() {/* 작업내용 */}    // Runnable인터페이스의 추상메서드 run()을 구현
}
cs


쓰레드를 구현한다는 것은 위의 2가지 방법 중 어떤 것을 선택하건 간에, 

쓰레드를 통해 작업하고자 하는 내용으로 run()의 몸통을 채우기만 하면 되는 것이다.


source01) ThreadEx01.java

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
37
38
39
40
package threadEx;
 
public class ThreadEx01 {
    public static void main(String[] args) {
 
        // 쓰레드 첫번째 구현 방법 : Thread를 상속받는다.
        InheritancedThread inheritancedThread = new InheritancedThread();
 
        // 쓰레드 두번째 구현 방법 : Runnable 인스턴스를 구현한다.
        RunnableThread r = new RunnableThread();
        Thread runnableThread = new Thread(r);
 
        inheritancedThread.start();
        runnableThread.start();
    }
}
 
// 쓰레드 첫번째 구현 방법 : Thread를 상속받는다.
class InheritancedThread extends Thread {
    final static int LOOP = 5;
 
    public void run() {
        for (int i = 0; i < LOOP; i++) {
            // 조상인Thread의 getName()을 호출
            System.out.println(getName());
        }
    }
}
 
class RunnableThread implements Runnable {
    final static int LOOP = 5;
 
    public void run() {
        for (int i = 0; i < LOOP; i++) {
            // Thread currentThread() - 현재 실행중인 쓰레드의 참조를 반환한다.
            // String getName() - 쓰레드의 이름을 반환한다.
            System.out.println(Thread.currentThread().getName());
        }
    }
}
cs

result)

1
2
3
4
5
6
7
8
9
10
Thread-0
Thread-0
Thread-0
Thread-0
Thread-0
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
cs

Thread클래스를 상속받는 경우와 Runnable인터페이스를 구현한 경우의 인스턴스 생성방법이 다르다는 것을 알 수 있다.
1
2
3
4
5
6
// 쓰레드 첫번째 구현 방법 : Thread를 상속받는다.
InheritancedThread inheritancedThread = new InheritancedThread();
 
// 쓰레드 두번째 구현 방법 : Runnable 인스턴스를 구현한다.
RunnableThread r = new RunnableThread();
Thread runnableThread = new Thread(r);
cs

Runnable인터페이스를 구현한 경우, Runnable인터페이스를 구현한 클래스의 인스턴스를 생성한 다음, 이 인스턴스를 가지고 Thread클래스의 인스턴스를 생성할 때 생성자의 매개변수로 제공해야한다. 이때 사용되는 Thread클래스의 생성자는 Thread(Runnable target)로 호출시에 Runnable인터페이스를 구현한 클래스를 넘겨줘야 한다.


아래의 소스에서 인스턴스 변수로 Runnable타입의 변수 r을 선언해 놓고 생성자를 통해서 Runnable인터페이스를 구현한 인스턴스를 참조하도록 되어있는 것을 확인할 수 있다. 그리고 run()을 호출하면 참조변수 r을 통해서 Runnable인터페이스를 구현한 인스턴스의 run()이 호출된다. 이렇게 함으로써 상속을 통해 run()을 오버라이딩하지 않고도 외부로부터 제공받을 수 있게 된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
public class Thread {
private Runnable r;    // Runnable을 구현한 클래스의 인스턴스를 참조하기 위한 변수를 선언
 
pulbic Thread(Runnable r) {
this.r;
}
 
public void run() {
if(r!=null)
r.run();    // Runnable인터페이스를 구현한 인스턴스 run()을 호출한다.
}
...
}
cs


여기서 한 가지 더 알아 두어야 할 것은 한 번 사용한 쓰레드는 다시 재사용할 수 없다는 것이다. 즉, 하나의 쓰레드에 대해 start()가 한 번만 호출될 수 있다는 뜻이다.  그래서 쓰레드의 작업이 한 번 더 수행되기를 원한다면 sample02와 같이 새로운 쓰레드를 생성한 다음에 start()를 호출해야 한다. 만일 sample01와 같이 하나의 쓰레드에 대해 start()를 두 번 이상 호출하면 실행시에 IllegalThreadStateException이 발생한다.


sample01) IllegalThreadStateException 발생

1
2
3
ThreadEx t1 = new ThreadEx();
t1.start();
t1.start();    // 예외발생
cs

                                                         

sample02)

1
2
3
4
ThreadEx t1 = new ThreadEx();
t1.start();
t1 = new TheadEx();
t1.start();
cs


# start()와 run()

쓰레드를 실행시킬 때 run()이 아닌 start()를 호출한다는 것에 대해서 다소 의문이 들었을 것이다. run()을 호출하는 것은 생성된 쓰레드를 실행시키는 것이 아니라 단순히 클래스에 속한 메서드 하나를 호출하는 것이다. 반면에 start()는 새로운 쓰레드가 작업을 실행하는데 필요한 호출스택(call stack)을 생성한 다음에 run()을 호출해서, 성된 호출스택에 run()이 첫 번 째로 저장되게 한다. 모든 쓰레드는 독립적인 작업을 수행하기 위해 자신만의 호출스택을 필요로 하기 때문에, 새로운 쓰레드를 생성하고 실행시킬 때마다 새로운 호출스택이 생성되고 쓰레드가 종료되먄 작업에 사용된 호출스택은 소멸된다.


start()가 호출된 쓰레드는 바로 실행되는 것이 아니라는 것에 주의해야한다. 일단 대기상태로 있다가 스케줄러가 정한 순서에 의해서 실행된다.


이 때 주어진 시간동안  작업을 마치치 못한 쓰레드는 다시 자신의 차례가 돌아올 때까지 대기상태에 있게 되며, 작업을 마친 쓰레드, 즉 run()의 수행이 종료된 쓰레드는 호출스택이 모두 비워지면서 이 쓰레드가 사용하던 호출스택은 사라진다. 이는 마치 처음에 자바프로그램을 실행하면 호출스택이 생성되고 main메서드가 처음으로 호출되고 main메서드가 종료되면 호출스택이 비워지고 프로그램이 종료되는 것과 정확히 일치한다. 이미 눈치 챘겠지만, main메서드의 작업을 수행하는 것도 쓰레드이다. 우리는 지금까지 우리도 모르는 사이에 이미 쓰레드를 사용하고 있었던 것이다. 앞서 쓰레드일꾼이라고 하였는데, 프로그램이 실행되기 위해서는 작업을 수행하는 일꾼이 최소한 하나는 필요하지 않겠는가. 그래서 프로그램을 실행하면 하나의 쓰레드(일꾼)를 생성하고, 그 쓰레드가 main메서드를 호출해서 작업이 수행되도록 하는 것이다.

(실행 중인 사용자쓰레드가 하나도 없을 때 프로그램은 종료된다.)





출처 : JAVA의정석(남궁성 저)


반응형

공유

댓글