본문 바로가기
Java

자바 멀티 스레드

by 승환파크 2023. 6. 28.

운영체제에서는 실행 중인 하나의 애플리케이션을 프로세스 라고 부른다. 사용자가 애플리케이션을 실행하면 운영체제로부터 실행에 필요한 메모리를 할당받아 애플리케이션의 코드를 실행하는데 이것이 프로세스이다.

 

하나의 애플리케이션은 멀티 프로세스를 만들기도 한다. 예를 들어 메모장 애플리케이션을 2개 실행했다면 2개의 메모장 프로세스가 생성된 것이다.

 

 

스레드

운영체제는 두 가지 이상의 작업을 동시에 처리하는 멀티 태스킹을 할 수 있도록 CPU 및 메모리 자원을 프로세스마다 적절히 할당해주고, 병렬로 실행시킨다. 예를 들어, 워드로 문서 작업을 하면서 동시에 윈도우 미디어 플레이어로 음악을 들을 수 있다.

 

멀티 태스킹은 꼭 멀티 프로세스를 뜻하는 것은 아니다. 한 프로세스 내에서 멀티 태스킹을 할 수 있도록 만들어진 애플리케이션도 있다. 대표적인 것이 미디어 플레이어와 메신저 이다. 미디어 플레이어는 동영상 재생과 음악 재생이라는 두 가지 작업을 동시에 처리하고, 메신저는 채팅 기능을 제공하면서 동시에 파일 전송 기능을 수행하기도 한다. 프로세스가 두 가지 이상 작업을 처리하는 방식은 멀티 스레드를 사용하는 것이다.

 

스레드는 사전적 의미로 한 가닥의 실이라는 뜻인데, 한 가지 작업을 실행하기 위해 순차적으로 실행될 코드를 실처럼 이어놓았다고 해서 유래된 이름이다. 하나의 스레드는 하나의 코드 실행 흐름이기 때문에 한 프로세스 내에 스레드가 2개라면 2개의 코드 실행 흐름이 생긴다는 의미이다.

 

멀티 프로세스는 운영체제에서 할당받은 자신의 메모리를 가지고 실행하기 때문에 각 프로세스는 서로 독립적이다. 따라서 하나의 프로세스에서 오류가 발생해도 다른 프로세스에 영향을 미치지 않는다. 하지만 멀티스레드는 하나의 프로세스 내부에 생성되기 때문에 하나의 스레드가 예외를 발생시키면 프로세스 자체가 종료될 수 있어 다른 스레드에 영향을 미치게 된다.

 

예를 들어 멀티 프로세스인 위드와 엑셀을 동시에 사용하던 도중, 위드에 오류가 생겨 먹통이 되더라도 엑셀은 여전히 사용 가능하다. 그러나 멀티 스레드로 동작하는 메신저의 경우 파일을 전송하는 스레드에서 예외가 발생하면 메신저 프로세스 자체가 종료되므로 채팅 스레드도 같이 종료된다. 그렇기 때문에 멜티 스레드에서는 예외 처리에 만전을 기해야 한다.

 

멀티 스레드는 다양한 곳에서 사용된다. 대용량 데이터의 처리 시간을 줄이기 위해 데이터를 분할 해서 병렬로 처리하기도 하고, UI를 가지고 있는 애플리케이션에서 네트워크 통신을 하기 위해 사용되기도 한다. 또한 다수 클라이언트의 요청을 처리하는 서버를 개발할 때에도 사용한다.

 

멀티 스레드는 애플리케이션을 개발하는 데 꼭 필요한 기능이기 때문에 반드시 이해하고 활용할 수 있도록 해야 한다.

 

 

메인 스레드

자바의 모든 애플리케이션은 메인 스레드가 main() 메소드를 실행하면서 시작한다. 메인 스레드는 main() 메소드의 첫 코드부터 아래로 순차적으로 실행하고, main() 메소드의 마지막 코드를 실행하거나 return문을 만나면 실행이 종료된다.

 

메인 스레드는 필요에 따라 작업 스레드들을 만들어서 병렬로 코드를 실행할 수 있다. 즉, 멀티 스레드를 생성해서 멀티 태스킹을 구현 한다.

 

싱글 스레드 애플리케이션에서는 메인 스레드가 종료하면 프로세스도 종료된다. 하지만 멀티 스레드 애플리케이션에서는 실행 중인 스레드가 하나라도 있다면, 프로세스는 종료되지 않는다. 메인 스레드가 작업 스레드보다 먼저 종료되더라도 작업 스레드가 계속 실행 중이라면 프로세스는 종료되지 않는다.

 

 

작업 스레드 생성과 실행

멀티 스레드로 실행하는 애플리케이션을 개발하려면 먼저 몇 개의 작업을 병렬로 실행할지 결정하고 각 작업별로 스레드를 생성해야 한다.

 

어떤 자바 애플리케이션이건 메인 스레드는 반드시 존재하기 때문에 메인 작업 이외의 추가적인 병렬 작업의 수만큼 스레드를 생성하면 된다. 자바에서는 작업 스레드도 객체로 생성되기 때문에 클래스가 필요하다. java.lang.Thread 클래스를 직접 객체화해서 생성해도 되지만, Thread 클래스를 상속해서 하위 클래스를 만들어 생성할 수도 있다.

 

Thread 클래스로부터 직접 생성

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

Thread thread = new Thread(Runnable target);

Runnable은 작업 스레드가 실행할 수 있는 코드를 가지고 있는 객체라고 해서 붙여진 이름이다. Runnable은 인터페이스 타입이기 때문에 구현 객체를 만들어 대입해야 한다. Runnable에는 run() 메소드 하나가 정의되어 있는데, 구현 클래스는 run()을 재정의해서 작업 스레드가 실행할 코드를 작성해야 한다.

 

아래는 Runnable 구현 클래스를 작성하는 방법이다.

class Task implemets Runnable{
	public void run(){
    	스레드가 실행할 코드;
    }
}

 

Runnable은 작업 내용을 가지고 있는 객체이지 실제 스레드는 아니다. Runnable 구현 객체를 생성한 후, 이것을 매개값으로 해서 Thread 생성자를 호출해야 비로소 작업 스레드가 생성된다.

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

 

코드를 좀더 절약하기 위해 Thread 생성자를 호출할 때 Runnable 익명 객체를 매개값으로 사용할 수 있다. 오히려 이 방법이 더 많이 사용된다.

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

 

작업 스레드는 생성되는 즉시 실행되는 것이 아니라, start() 메소드를 아래와 같이 호출해야만 비로소 실행된다.

thread.start();

 

start() 메소드가 호출되면, 작업 스레드는 매개값으로 받은 Runnable 의 run() 메소드를 실행하면서 자신의 작업을 처리한다.

 

실행 순서

1. 스레드 작성

class Task implements Runnable{
	publiv void run(){
    	스레드 내용
    }
}

2. 메인 스레드에서 Task 스레드 객체 생성

3. start() 메소드 호출

4. 메인 스레드 실행

 

Thread 하위 클래스로부터 생성

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

아래는 작업 스레드 클래스를 정의하는 방법인데, Thread 클래스를 상속한 후 run() 메소드를 재정의해서 스레드가 실행할 코드를 작성하면 된다. 작업 스레드 클래스로부터 작업 스레드 객체를 생성하는 방법은 일반적인 객체를 생성하는 방법과 동일하다.

public class WorkerThread extends Thread {
	@Override
    public void run(){  <-- run() 메소드 재정의
    	스레드가 실행할 코드;
    }
}
Thread thread = new WorkerThread();

 

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

Thread thread = new Thread() {  <-- 익명 자식 객체
	public void run(){
    	스레드가 실행할 코드;
    }
}

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

thread.start();

 

 

동기화 메소드

싱글 스레드 프로그램에서는 1개의 스레드가 객체를 독차지해서 사용하면 되지만, 멀티 스레드 프로그램에서는 스레드들이 객체를 공유해서 작업해야 하는 경우가 있다. 이 경우 주의해야 할 점들이 있다.

 

공유 객체를 사용할 때의 주의점

멀티 스레드 프로그램에서 스레드들이 객첼르 공유해서 작업해야 하는 경우, 스레드 A가 사용하던 객체를 스레드 B가 상태를 변경할 수 있기 때문에 스레드 A가 의도했던 것과는 다른 결과를 산출할 수도 있다.

 

이는 여러 사람이 계산기를 함께 나눠 쓰는 상황을 예로 설명할 수 있다. 사람 A가 계산기로 작업을 하다가 계산 결과를 메모리에 저장한 뒤 잠시 자리를 비웠는데, 이 때 사람 B가 계산기를 만져서 사람 A가 메모리에 저장한 값을 다른 값으로 변경하는 것과 동일하다. 그런 다음 사람 A가 돌아와 계산기에 저장된 값을 이용해서 이후 작업을 진행한다면 결국 사람 A는 엉터리 값을 이용하게 된다.

 

동기화 메소드

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

 

멀티 스레드 프로그램에서 단 하나의 스레드만 실행할 수 있는 코드 영역을 임계 영역 이라고 한다. 자바는 임계 영역을 지정하기 위해 동기화 메소드를 제공한다. 스레드가 객체 내부의 동기화 메소드를 실행하면 즉시 객체에 잠금을 걸어 다른 스레드가 동기화 메소드를 실행하지 못하도록 한다.

 

동기화 메소드를 만들려면 아래와 같이 메소드 선언에 synchronized 키워드를 붙이면 되는데, 인스턴스와 정적 메소드 어디든 붙일 수 있다.

public synchronized void method() {
	임계 영역; // 단 하나의 스레드만 실행
}

 

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

 

만약 동기화 메소드가 여러 개 있을 경우, 스레드가 이들 중 하나를 실행할 때 다른 스레드는 해당 메소드는 물론이고, 다른 동기화 메소드도 실행할 수 없다. 하지만 이 때 다른 스레드에서 일반 메소드는 실행이 가능하다.

'Java' 카테고리의 다른 글

자바 컬렉션 프레임워크  (0) 2023.06.29
자바 스레드 제어  (0) 2023.06.29
자바 java.util 패키지  (1) 2023.06.28
자바 java.lang 패키지  (0) 2023.06.28
자바 예외 처리  (0) 2023.06.28