본문 바로가기

프로그래밍 공부/Java

Java - 스레드(Thread)

프로세스(Process)

프로세스는 단순히 실행 중인 프로그램(Program)이라고 할 수 있다. 사용자가 작성한 프로그램이 운영체제에 의해 메모리 공간을 할당받아 실행 중인 것을 말한다. 이런 프로세스는 프로그램에 사용되는 데이터와 메모리 등의 자원 그리고 스레드로 구성이 된다.

스레드(Thread)

스레드는 프로세스 내에서 실제로 작업을 수행하는 주체를 의미한다. 모든 프로세스에는 한 개 이상의 스레드가 존재해 작업을 수행한다. 또한 두 개 이상의 스레드를 가지는 프로세스를 멀티스레드 프로세스(Multi-threaded process)라고 한다. Java와는 다르게 JavaScript에 경우는 기본적으로 스레드가 하나인 싱글 스레드 프로세스(Single-threaded process)로 작업을 수행한다.

스레드의 생성과 실행

Java에서는 두 가지 방법으로 스레드를 생성할 수 있다.

1. Runnable 인터페이스를 구현하는 방법
2. Thread 클래스를 상속받는 방법

위의 두 방법 모두 스레드를 통해 작업하고 싶은 내용을 run() 메서드에 작성한다.

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
41
// 스레드(Thread)의 생성과 실행
class ThreadWithClass extends Thread {
    public void run(){
        for(int i = 0; i < 5; i++){
            // 현재 실행중인 스레드의 이름을 반환
            System.out.println(getName());    
            try{
                Thread.sleep(10);
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}
 
class ThreadWithRunnable implements Runnable{
    public void run(){
        for(int i = 0; i < 5; i++){
            // 현재 실행중인 스레드의 이름을 반환
            System.out.println(Thread.currentThread().getName());    
            try{
                Thread.sleep(10);
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}
 
public class Main{
    public static void main(String[] args){
        // Thread 클래스를 상속받는 방법
        ThreadWithClass thread01 = new ThreadWithClass();    
        // Runnable 인터페이스를 구현하는 방법
        Thread thread02 = new Thread(new ThreadWithRunnable());
 
        // 스레드의 실행
        thread01.start();
        thread02.start();
    }
}
cs

위 코드의 실행 결과는 아래와 같다.

생성된 스레드가 서로 번갈아가며 실행되고 있는 것을 확인할 수 있다. Thread 클래스를 상속받으면 다른 클래스를 상속받을 수 없으므로, 일반적으로 Runnable 인터페이스를 구현하는 방법으로 스레드를 생성한다.

Runnable 인터페이스는 몸체가 없는 메서드인 run() 메서드 단 하나만을 가지는 간단한 인터페이스 이다.

스레드의 우선순위

Java에서 각 스레드는 우선순위(Priority)에 관한 자신만의 필드를 가지고 있다. 이런 우선순위에 따라 특정 스레드가 더 많은 시간 동안 작업을 할 수 있도록 설정할 수 있다.

필드 설명
static int MAX_PRIORITY 스레드가 가질 수 있는 최대 우선순위를 명시
static int MIN_PRIORITY 스레드가 가질 수 있는 최소 우선순위를 명시
static int NORM_PRIORITY 스레드가 생성될 때 가지는 기본 우선순위를 명시

getPriority()와 setPriority() 메서드를 통해 스레드의 우선순위를 리턴하거나 변경할 수 있다. 스레드의 우선순위가 가질 수 있는 범위는 1부터 10까지이고, 숫자가 높을수록 우선순위가 높아진다.

하지만 스레드의 우선순위는 비례적인 절대값이 아니고 상대적인 값이다. 우선순위가 10인 스레드가 우선순위가 1인 스레드보다 10배 빠르게 처리가 되는 것은 아니다. 단지 우선순위 10의 스레드는 우선순위 1인 스레드보다 좀 더 많이 실행 큐에 포함되어 좀 더 많은 작업 시간을 할당 받을 뿐이다.

또한 스레드의 우선순위는 해당 스레드를 생성한 스레드의 우선순위를 상속받는다.

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
class ThreadWithRunnable implements Runnable {
    public void run() {
        for (int i = 0; i < 5; i++) {
            // 현재 실행 중인 스레드의 이름을 반환
            System.out.println(Thread.currentThread().getName()); 
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
 
public class Main {
    public static void main(String[] args){
        Thread thread1 = new Thread(new ThreadWithRunnable());
        Thread thread2 = new Thread(new ThreadWithRunnable());
 
        // Thread-1의 우선순위를 10으로 변경함.
①      thread2.setPriority(10); 
 
②      thread1.start(); // Thread-0 실행
③      thread2.start(); // Thread-1 실행
 
        System.out.println(thread1.getPriority());
        System.out.println(thread2.getPriority());
    }
}
cs

main() 메서드를 실행하는 스레드의 우선순위는 언제나 5이다. 따라서 main() 메서드 내에서 생성된 스레드 Thread-0의 우선순위는 5로 설정되는 것을 확인할 수 있다.

위의 코드는 번(23행) 라인에서 Thread-0이 먼저 실행되고, 번(24행) 라인에서 Thread-1이 나중에 실행이 된다. 따라서 만약 번(21행) 라인이 존재하지 않으면 Thread-0이 먼저 실행되고 Thread-1이 나중에 실행이 될 것이다.

하지만 번(21행) 라인에서 Thread-1의 우선순위를 10으로 변경을 했기 때문에, Thread-1이 나중에 실행되었더라도 우선순위가 Thread-9보다 높아 먼저 실행이 된다.

멀티 스레드(Multi thread)

일반적으로 하나의 프로세스는 하나의 스레드를 가지고 작업을 한다. 하지만 멀티 스레드는 하나의 프로세스 내에서 둘 이상의 스레드가 동시에 작업을 수행하는 것을 의미한다. 또한 멀티 프로세스는 여러 개의 CPU를 사용하여 여러 프로세스를 동시에 수행하는 것을 의미한다.

멀티 스레드와 멀티 프로레스 모두 여러 흐름을 동시에 수행한다는 공통점을 가지고 있다. 멀티 프로세스는 각 프로세스가 독립적인 메모리를 가지고 별도로 실행되지만, 멀티 스레드는 각 스레드가 자신이 속한 프로세스의 메모리를 공유한다는 점이 다르다.

멀티 스레드는 각 스레드가 자신이 속한 프로세스의 메모리를 공유하므로 시스템 자원의 낭비가 적다. 또한 하나의 스레드가 작업을 할 때 다른 스레드가 별도의 작업을 할 수 있어 사용자와의 응답성도 좋아진다.

문맥 교환(Context switching)

컴퓨터가 동시에 처리할 수 있는 최대 작업 수는 CPU 코어의 수와 같다. 만약 CPU 코어의 수보다 더 많은 스레드가 실행이 되면 각 코어가 정해진 시간동안 여러 작업을 번갈아가며 수행하게 된다.

이때 각 스레드가 서로 교체될 때 스레드 간의 문맥 교환이 발생하는데, 문맥 교환은 현재까지의 작업 상태나 다음 작업에 필요한 각종 데이터를 저장하고 읽어오는 작업을 의미한다. 이런 문맥 교환에 걸리는 시간이 커질수록 멀티 스레딩의 효율은 감소한다. 오히려 많은 양의 단순한 계산은 싱글 스레드로 동작하는 것이 훨씬 효율적일 수 있다. 

따라서 많은 수의 스레드를 실행하는 것이 언제나 좋은 대안이 될 수 없다.

스레드 그룹(Thread group)

스레드 그룹은 서로 관련이 있는 스레드를 하나의 그룹으로 묶어 다루기 위한 장치이다. Java에서는 스레드 그룹을 다루기 위해 ThreadGroup이라는 클래스를 제공한다.

이런 스레드 그룹은 다른 스레드 그룹을 포함할 수도 있고, 이렇게 포함된 스레드 그룹은 트리 형태로 연결된다.

이때 스레드는 자신이 포함된 스레드 그룹이나 그 하위 그룹에는 접근할 수 있지만, 다른 그룹에는 접근할 수 없다. 이렇게 스레드 그룹은 스레드가 접근할 수 있는 범위를 제한하는 보안상으로도 중요한 역할을 하고 있다.
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
class ThreadWithRunnable implements Runnable {
    public void run() {
        try {
            Thread.sleep(10); // 0.01초간 스레드를 멈춤.
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
 
public class Main {
    public static void main(String[] args){
        Thread thread0 = new Thread(new ThreadWithRunnable());
        thread0.start(); // Thread-0 실행
        System.out.println(thread0.getThreadGroup()); 
 
        ThreadGroup group = new ThreadGroup("myThread"); // myThread라는 스레드 그룹 생성함.
        group.setMaxPriority(7); // 해당 스레드 그룹의 최대 우선순위를 7로 설정함. 
 
        // 스레드를 생성할 때 포함될 스레드 그룹을 전달할 수 있음.
        Thread thread1 = new Thread(group, new ThreadWithRunnable());
        thread1.start(); // Thread-1 실행
        System.out.println(thread1.getThreadGroup());
    }
}
cs

위의 코드처럼 main() 메서드에서 생성된 스레드의 기본 스레드 그룹의 이름은 main이 되며, 최대 우선 순위는 10으로 자동 설정된다.

데몬 스레드(Demon thread)

데몬 스레드한 다른 일반 스레드의 작업을 돕는 보조적인 역할을 하는 스레드를 말한다. 따라서 데몬 스레드는 일반 스레드가 모두 종료되면 더는 할 일이 없어지게 되고, 데몬 스레드 역시 자동으로 종료된다.

데몬 스레드의 생성 방법과 실행 방법은 모두 일반 스레드와 같다. 단, 실행하기 전에 setDemon() 메서드를 호출해 데몬 스레드로 설정하기만 하면 된다.

이러한 데몬 스레드는 일정 시간마다 자동으로 수행되는 저장 및 화면 갱신등에 이용된다.

가비지 컬렉터(Gabage collector)

데몬 스레드를 이용하는 가장 대표적인 예로 가비지 컬렉터가 있다. 가비지 컬렉터는 프로그래머가 동적으로 할당한 메모리 중 더 이상 사용하지 않는 영역을 자동으로 찾아내 해제해 주는 데몬 스레드이다.

Java에서는 프로그래머가 메모리에 직접 접근하지 못하게 하는 대신 가비지 컬렉터가 자동으로 메모리를 관리한다. 이런 가비지 컬렉터를 이용해 프로그래밍을 하기가 수월해지고, 메모리에 관련된 버그가 발생활 확률이 줄어든다.

보통 가비지 컬렉터가 동작하는 동안에는 프로세서가 일시적으로 중지되므로 필연적으로 성능의 저하가 따라온다. 하지만 요즘에는 가비지 컬렉터의 성능이 향상되어서 새롭게 만들어지는 대부분의 프로그래밍 언어에서 가비지 컬렉터를 제공한다.

'프로그래밍 공부 > Java' 카테고리의 다른 글

Java - Java 8 : Lambda  (0) 2020.02.26
Java - 컬렉션 프레임워크(Collection framework)  (0) 2020.02.26
Java - 예외 처리  (0) 2020.02.24
Java - 제네릭(Generic)  (0) 2020.02.24
Java - Java API 클래스 : Arrays 클래스  (0) 2020.02.24