본문 바로가기

Java/Basic

[Java] 멀티스레드(Multi Thread)

1. 멀티 스레드 개념

1-1) 프로세스와 스레드

- 스레드(Thread)는 사전적 의미로 한 가닥의 실이라는 뜻인데, 한 가지 작업을 실행하기 위해 순차적으로 실행할 코드를 실처럼 이어놓았다고해서 유래되었다.

- 하나의 스레드는 하나의 코드 실행 흐름이기 때문에 한 프로세스 내에 스레드가 두 개라면 두 개의 코드 실행 흐름이 생긴다는 의미이다.

- 멀티 프로세스(Multi Process)가 애플리케이션 단위의 멀티 태스킹이라면 멀티 스레드(Multi Thread)는 애플리케이션 내부에서의 멀티 태스킹이라고 볼 수 있다.

 

- 멀티 프로세스들은 운영체제에서 할당받은 자신의 메모리를 가지고 실행하기 때문에 독립적이다, 따라서 하나의 프로세스에서 오류가 발생해도 다른 프로세스에게 영향을 미치 않는다.

- 멀티 스레드는 하나의 프로세스 내부에서 생성되기 떄문에 하나의 스레드가 예외를 발생시키면 프로세스 자체가 종료될 수 있어 다른 스레드에게 영향을 미치게 된다.

1-2) 메인 스레드(Main Thread)

- 모든 자바 애플리케이션은 메인 스레드(main thread)가 main() 메소드를 실행하면서 시작된다.

- 메인 스레드는 필요에 따라 작업 스레드들을 만들어서 병렬로 코드를 실행할 수 있다. == 멀티 태스킹

 

2. 작업 스레드

2-1) Thread 클래스로부터 직접 생성

Thread thread = new Thread(Runnable target);
cs

- java.lang.Thread 클래스로부터 작업 스레드 객체를 직접 생성하려면 다음과 같이 Runnable을 매개값으로 갖는 생성자를 호출해야 한다.

- Runnable은 작업 스레드가 실행할 수 있는 코드를 가지고 있는 객체이다.

- Runnable은 인터페이스 타입이기 때문에 구현 객체를 만들어 대입해야 한다.

- Runnable은 run() 메소드 하나가 정의되어 있고 구현 클래스 run()을 재정의해서 작업 스레드가 실행할 코드를 작성한다.

 

 

Runnable Task = new Task();
Thread thread = new Thread(Task);
cs

- Runnable은 작업 내용을 갖고 있는 객체이지 실제 스레드는 아니다. 따라서 Runnable 구현 객첼ㄹ 생성한 후, 이것을 매가값으로 해서 Thread 생성자를 호출하면 작업 스레드를 생성할 수 있다.

 

 

thread.start();
cs

- 작업 스레드는 생성되는 즉시 실행되는 것이 아니라 start() 메소드를 호출해야한다.

 

2-2) Thread 하위 클래스로부터 생성

public class WorkerThread extends Thread{
    @Override
    public void run() {
        //스레드가 실행할 코드
    }
}
cs

- 작업 스레드가 실행할 작업을 Runnble로 만들지 않고, Thread의 하위 클래스로 작업 스레드를 정의하면서 작업 내용을 포함시킬 수도 있다.

- Thread 클래스를 상속한 후 run 메소드를 overriding해서 스레드가 실행할 코드를 작성한다.

 

 

Thread thread = new Thread() {
    public void run() {
        // 스레드가 실행할 코드
    }
}
cs

- 코드를 좀 더 절약하기 위해 다음과 같이 Thread 익명 개체로 작업 스레드 객체를 생성할 수도 있다.

 

 

thread.start();
cs

- 생성된 작업 스레드 객체에서 start() 메소드를 호출하면 작업 스레드는 자신의 run()메소드를 실행하게 된다.

 

2-3) 스레드의 이름

- 메인 스레드는 "main"이라는 이름을 가지고 있고, 우리가 직접 생성한 스레드는 자동적으로 "Thread-n"이라는 이름으로 생성된다.

 

 

thread.setName("새로운 스레드 이름");
cs

- 스레드의 이름을 다른 이름으로 설정하고 싶다면 Thread 클래스의 setName() 메소드로 변경할 수 있다.

- 반대로 스레드의 이름을 얻고 싶으면 thread.getName() 메소드를 호출하면 된다.

 

 

Thread thread = Thread.currentThread();
cs

- setName()과 getName()은 Thread의 인스턴스 메소드이므로 스레드 객체의 참조가 필요하다.

- 만약 스레드 객체의 참조를 가지고 있지 않다면, Thread의 정식 메소드인 currentThread()로 코드를 실행하는 현재 스레드의 참조를 얻을 수 있다.

 

 

3. 스레드의 우선순위

- 멀티 스레드는 동시성(Concurrency) 또는 병렬성(Parallelism)으로 실행된다.

 동시성(Concurrency) : 멀티 작업을 우해 하나의 코어에서 멀티 스레드가 번갈아가며 실행하는 성질.

 병렬성(Parellelism) : 멀티 작업을 위해 멀티 코어에서 개별 스레드를 동시에 실행하는 성질.

 

✓ 스레드 스케줄링(Thread Scheduling) : 스레드의 개수가 코어의 수보다 많을 경우, 스레드를 어떤 순서에 의해 동시성으로 실행할 것인가를 결정한다.

 - 자바의 스레드 스케줄링은 우선순위(Priority) 방식과 순환 할당(Round-Robin) 방식을 사용한다.

 우선순위(Priority) 방식 : 우선순위가 높은 스레드가 실행 상태를 더 많이 가지도록 스케줄링하는 방식.

 순환 할당(Round-Robin) 방식 : 시간 할당량(Time Slice)을 정해서 하나의 스레드를 정해진 시간만큼 실행하고 다시 다른   스레드를 실행하는 방식.

 

- 우선순위 방식에서는 우선순위는 1 ~ 10까지 부여되는데, 1이 가장 우선순위가 낮고, 10이 가장 높다.

- 우선순위를 부여받지 않은 스레드들은 기본적으로 5의 우선순위를 할당받는다.

thread.SetPriority(우선순위);
 
// 우선순위의 매개값으로 값을 직접 주어도 되지만,
// 코드의 가독성을 높이기 위해서 Thread의 클래스의 상수를 사용할 수도 있다.
 
thread.setPriority(Thread.MAX_PRIORITY);    //    10
thread.setPriority(Thread.NORM_PRIORITY);    //    5
thread.setPriority(Thread.MIN_PRIORITY);    //    1
 
cs

 

4. 동기화 메소드, 동기화 블록

- 싱글 스레드 프로그램에서는 한 개의 스레드가 객체를 독차지하지만, 멀티스레드 프로그램에서는 스레드들이 객체를 공유해서 작업해야 하는 경우가 있다. 이런 경우 서로 다른 스레드에 의해 공유 중인 객체의 상태가 변경될 수 있기 때문에 의도와 다른 결과를 산출할 수 있다.

- 스레드가 사용 중인 객체를 다른 스레드가 변경할 수 없도록 하려면 스레드 작업이 끝날 때까지 객체에 잠금을 걸어서 다른 스레드가 사용할 수 없도록 해야한다. === 동기화

 

✓ 임계 영역(critical section) : 멀티 스레드 프로그램에서 단 하나의 스레드만 실행할 수 있는 코드 영역.

 자바는 임계 영역을 지정하기 위해 동기화(Synchronized) 메소드와 동기화 블록을 제공한다.

 

public void method() {
    // 여러 스레드가 실행 가능 영역
    ...
    synchronized(공유객체){
        임계 영역 // 단 하나의 스레드만 실행
    }
    // 여러 스레드가 실행 가능 영역
}
cs

- 동기화 메소드는 메소드 전체 내용이 임계 영역이므로 스렏가 동기화 메소드를 실행하는 즉시 객체에는 잠금이 일어나고, 스레드가 동기화 메소드를 실행 종료하면 잠금이 해제된다.

 

5. 스레드의 상태

- 스레드를 생성하고 start() 메소드를 호출하면 스레드가 실행되는 것처럼 보이지만 실행 대기 상태(RUNNABLE)가 된다.

 

thread.getState();
cs

- getState() 메소드로 스레드의 상태를 코드에서 확인할 수 있고, 밑의 표처럼 스레드 상태에 따라서 열거 상수를 리턴한다.

상태 열거 상수 설명
객체생성 NEW 스레드 객체가 생성, 아직 start() 메소드가 호출되지 않은 상태
실행대기 RUNNABLE 실행 상태로 언제든지 갈 수 있는 상태
일시정지 WAITING 다른 스레드가 통지할 때까지 기다리는 상태
TIMED_WATING 주어진 시간동안 기다리는 상태
BLOCKED 사용하고자 하는 객체의 잠금이 풀릴 때까지 기다리는 상태
종료 TERMINATED 실행을 마친 상태

 

6. 스레드의 상태 제어

 

7. 데몬 스레드

✓ 데몬 스레드(daemon thread) : 주 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드

 - 주 스레드가 종ㄹ되면 데몬 ㅅ레드는 강제적으로 자동 종료된다.

example) 워드프로세스의 자동 저장, 미디어플레엉의 동영상 및 음악재생, 가비지컬렉터

 

isDaemon() 메소드의 리턴값(boolean)으로 현재 실행 중인 스레드가 데몬 스레드인지 아닌지를 구별할 수 있다.

 

8. 스레드 그룹

- ThreadGroup은 관련된 스레드를 묶어서 관리할 목적으로 이용된다.

- 스레드는 반드시 하나의 스레드 그룹에 포함되는데, 명시적으로 스레드 그룹에 포함시키지 않으면 기본적으로 자신을 생성 스레드와 같은 그룹에 속하게 된다.

- 작업 스레드는 대부분 main스레드가 생성하므로 기본적으로 main스레드 그룹에 속하게 된다.

8-1) 스레드 그룹 이름 얻기

ThreadGroup group = Thread.currentThread().getThreadGroup();
String group name = group.getName();
cs

- 현재 스레드가 속한 스레드의 이름을 얻는다.

 

Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
cs

- Thread의 정적 메소드인 getAllStackTraces()를 이용해 프로세스 내에서 실행하는 모든 스레드에 대한 정보를 얻을 수 있다.

- getAllStackTrace() 메소드는 Map타입의 객체를 리턴하는데, key는 스레드 객체 value는 스레드의 상태 기로들을 갖고 있는 StackTraceElemenct[] 배열이다.

 

8-2) 스레드 그룹 생성하기

ThreadGroup tg = new ThreadGroup(String name);
ThreadGroup tg = new ThreadGroup(ThreadGroup parent, String name);
cs

- 스레드 그룹 생성시 parent 스레드 그룹을 지정하지 않으면 현제 스레드가 속한 그룹의 하위 그룹으로 생성된다.

 

 

Thread t = new Thread(ThreadGroup group, Runnable target);
Thread t = new Thread(ThreadGroup group, Runnable targer, Sring name);
Thread t = new Thread(ThreadGroup group, Runnable targer, Sring name, long stackSize);
Thread t = new Thread(ThreadGroup group, String name);
cs

- 그룹을 생성한 후, 해당 그룹에 스레드를 포함시키려면 Thread객체를 생성할 때 생성자 매개값으로 스레드 그룹을 지정할 수 있다. 

- Runnable 타입의 target은 Runnable 구현 객체를 말하며, String 타입의 name은 스레드의 이름이고, long타입의 stackSize는 JVM(Java Virtual Machine)이 스레드에 할당할 stack의 크기이다.

 

* ThreadGroup이 가지고 있는 주요 메소드

 

9. 스레드풀

- 병렬 작업 처리가 많아지면 스레드 개수가 증가되고 그에 따른 스레드 생성과 스케줄링으로 인해 CPU가 바빠져 메모리 사용량이 늘어나며 애플리케이션의 성능이 저하된다. 이런 작업의 폭증으로 인한 스레드의 폭증을 막으려면 스레드풀(ThreadPool)을 사용해야한다.

- 자바는 스레드풀을 생성하고 사용할 수 있도록 java.util.concurrent 패키지에서 ExecutorService 인터페이스와 Executor 클래스를 제공하고 있다.

 

9-1) 스레드풀 생성 및 종료

﹅스레드풀 생성

- ExecutorService 구현객체는 Executors 클래스의 다음 두 가지 메소드 중 하나르 이용해서 간편하게 생성할 수 있다.
- 초기 스레드 수는 ExecutorService 객체가 생성될 때 기본적으로 생성되는 스레드수를 말한다.

- 코어 스레드 수는 스레드 수가 증가된 후 사용되지 않는 스레드를 스레드풀에서 제거할 때 최소한 유지해야 할 스레드 수를 말한다.

- 최대 스레드 수는 스레드풀에서 관리하는 최대 스레드 수이다.

 

 

ExecutorService executorService = Executors.newCachedThreadPool();
cs

- newCachedThreadPool() 메소드로 생성된 스레드풀의 특징 

 : 1개 이상의 스레드가 추가되었을 경우 60초 동안 추가된 스레드가 아무 작업을 하지 않으면 추가된 스레드를 종료하고 풀에서 제거한다.

 

 
ExcutorService executorService = Executors.newFixedThreadPool(
    Runtime.getRunTime().availableProcessors()
);
cs

- newFixedThreadPool() 메소드로 생성된 스레드풀의 특징

 : 스레드가 작업을 처리하지 않고 놀고 있더라도 스레드 개수가 줄지 않는다.

 

 

﹅스레드풀 종료

- 스레드풀의 스레드는 기본적으로 데몬 스레드가 아니기 때문에 main스레드가 종료되더라도 작업을 처리하기 위해 계속 실행 상태로 남아있기 때문에, 애플리케이션을 종료하려면 스레드풀을 종료상태가 되도록 처리해주어야 한다.

- ExecutorSErvice는 종료와 관련하여 다음 위 3개의 메소드를 제공하고 있다.

 

executorService.shutdown();
또는
executorService.shutdownNow(); // 남아있는 작업과는 상관없이 강제로 종료
cs

 

9-2) 작업 생성과 처리 요청

작업생성

- 하나의 작업은 Runnable 또는 Callable 구현 클래스로 표현한다.

- Runnable과 Callable의 차이점은 작업 처리 완료 후 리턴값의 여부이다. 

- Runnable의 run() 메소드는 리턴값이 없고, Callable의 call() 메소드는 리턴값이 존재한다.

- call()의 리턴 타입은 implements Callable<T>에서 지정한 T 타입이다.

- 스레드풀의 스레드는 작업 Queue에서 Runnable 객체를 가져와 run()과 call() 메소드를 실행한다.

 

﹅작업 처리 요청

- 작업 처리 요청이란 ExecutorService의 작업 Queue에 Runnable 또는 Callable 객체를 넣는 행위이다.

- ExecutorService는 작업 처리 요청을 위해 다음 두 가지 메소드를 제공한다.

* execute()와 submit()의 차이점

 1) execute()는 작업 처리 결과를 받지 못하고, submit()은 작업 처리 결과를 받을 수 있도록 Future를 리턴한다.

 2) execute()는 작업 처리 도중 예외가 발생하면 스레드가 종료되고, 해당 스레드는 스레드풀에서 제거된다. 따라서 스레드풀은 다른 작업 처리를 위해 새로운 스레드를 생성한다.

 반면 submit()은 작업 처리 도중 예외가 발생하더라도 스레드는 종료되지 않고 다음 작업을 위해 재사용된다.

 

- 가급적이면 스레드의 생성 오버헤더를 주루이기 위해서 submit()을 사용하는 것이 좋다.

 

9-3) 블로킹 방식의 작업 완료 통보

- ExecutorService의 submit() 메소드는 매개값으로 준 Runnable 또는 Callable 작업을 스레드풀의 작업 Queue에 저장하고 즉시 Future 객체를 리턴한다.

- Future 객체는 작업 결과가 아니라 작업이 완료될 때까지 기다렸다가(지연했다가=blocking) 최종 결과를 얻는데 사용된다.

 

- 리턴 타입인 V는 submit(Runnable task, V result)의 두 번째 매개값인 V 타입이거나 submit(Callable<V> task)의 Callable 타입 파라미터 V 타입이다.

 

 

- submit() 메소드별로 Future의 get() 메소드가 리턴하는 값

 

﹅ 리턴값이 없는 작업 완료 통보

- 리턴값이 없는 작업일 경우는 Runnable 객체로 생성하면 된다. 

 

Runnable task = new Runnable() {

    @Override

    public void run() {

        // 스레드가 처리할 작업 내용

    }

}

 
Future future = executorService.submit(task);
cs

- 결과값이 없는 작업처리 요청은 submit(Runnable task)메소드를 이용하면 된다.

- 결과값이 없음에도 불구하고 다음과 같이 Future 객체를 리턴하는데, 이것은 스레드가 작업 처리를 정상적으로 완료했는지, 아니면 작업 처리 도중에 예외가 발생했는지 확인하기 위해서이다.

 

﹅ 리턴값이 있는 작업 완료 통보

- 스레드풀의 스레드가 작업을 완료 후 애플리케이션이 처리 결과를 얻어야한다면 작업 객첼ㄹ Callable로 생성하면 된다.

- 제네릭타입의 파라미터 T는 call() 메소드가 리턴하는 타입이 되도록한다.

Callable<T> task = new Callable<T>() {
    @Override
    public T call() throws Exception {
    //스레드가 처리할 작업 내용
    return T;
    }
};
cs

 

 

Future<T> future = executorService.submit(task);

- Callable의 작업의 처리 요청은 ExecutorService의 submit() 메소드를 호출하면 된다.

 

try {
    T result = future.get();
catch (InterruptedException e) {
    // 작업 처리 도중 스레드가 interrupt될 경우 실행할 코드
catch (ExecutionException e) {
    // 작업 처리 도중 예외가 발생된 경우 실행할 코드
}
 

- 스레드풀의 스레드가 Callable 객체의 call() 메소드를 모두 실행하고 T타입의 값을 리턴하면, Future<T>의 get() 메소느는 블로킹이 해제되고 T타입의 값을 리턴하게 된다.

 

﹅작업 처리 결과를 외부 객체에 저장

- 상황에 따라서 스레드가 작업한 결과를 외부객체에 저장해야할 경우도 있다. 예를 드러 스레드가 작업 처리를 완료하고 외부 Result 객체에 작업 결과를 저장하면, 애플리케이션이 Result 객체를 사용해서 어떤 작업을 진행할 수 있을 것이다. 대개 Result 객체는 공유 객체가 되어, 두 개 이상의 스레드 작업을 취합할 목적으로 이용된다.

 

Result result = ...;
Runnable task = new Task(result);
Future<Result> future = executorService.submit(task, result);
result = future.get();
 

- ExecutorService의 submit(Runnable task, V result) 메소드를 사용할 수 있는데, V가 바로 Result타입이 된다.

- Future의 get()메소드를 호출하면 스레드가 작업을 완료할 때 까지 blocking 상태였다가 작업을 완료하면 V 타입 객체를 return한다.

- 리턴된 객체는 submit()의 두 번째 매개값으로 준 객체와 동일한데, 차이점은 스레드 처리 결과가 내부에 저장되어 있다는 것이다.

 

 

Class Task implements Runnable {
    Result result;
    Task(Result result() { this.result = result; }
    @Override
    public void run() {
        // 작업 코드
        // 처리 결과를 result 저장
    }
}
 
cs

- 작업 객체는 Runnable 구현 클래스로 생성하는데, 주의할 점은 스레드에서 결과를 저장하기 위해 외부 Result객체를 사용해야 하므로 생성자를 통해 Result객체를 주입받도록 해야한다.

'Java > Basic' 카테고리의 다른 글

[Java] 변수(Variable)  (0) 2020.03.14
[Java] 클래스(Class)와 객체(Object)  (0) 2020.03.10
[Java] JVM(Java Virtual Machine)  (0) 2020.03.08
[Java] 람다식(Lambda Expressions)  (0) 2020.02.29
[Java] 제네릭(Generic)  (0) 2020.01.19