멀티 스레드, 멀티 프로세스 개념
- 운영체제는 실행중인 프로그램을 프로세스로 관리한다.
- 운영체제는 멀티 프로세스를 생성해서 멀티 태스킹처리한다.
- 하나의 프로세스 내에서 멀티 태스킹(두가지 일을 동시에 처리)할 수 있도록 하기 위해서 만들어진 프로그램: 메신저 채팅작업 + 파일 전송 동시 작업
- 하나의 프로세스가 두가 이상의 작업을 처리할 수 있는 이유는 멀티 스레드가 있기 때문이다.
스레드: 코드의 처리 흐름을 말한다. 프로세스 내에 스레드가 두 개라면, 두 개의 코드 실행 흐름이 생긴다는 의미이다.
멀티 프로세스가 프로그램 단위의 멀티태스킹이라면 멀티 스레드는 프로그램 내부에서의 멀티태스킹이라고 볼 수 있다.
멀티 프로세스
서로 독립적으로, 하나의 프로세스에서 오류가 발생해도 다른 프로레스에게 영향을 미치지 않는다.
-> 워드와 엑셀을 동시에 사용하다가 워드가 오류가 생겨도 엑셀은 여전히 사용 가능한 이유임.
멀티 스레드
프로세스 내부에서 생성되기 때문에 하나의 스레드가 예외를 발생시키면 프로세스가 종료되므로 다른 스레드에게 영향을 미친다.
-> 메신저의 경우 파일 전송 스레드에서 exception이 발생하면 메신서 프로세스 자체가 종료되기 때문에 채팅 스레드도 같이 종료된다. 그렇기 때문에 멀티 스레드를 사용할 경우에는 예외 처리에 만전을 기해야 한다.
main 스레드
- 모든 자바 프로그램은 메인 스레드가 main()메소드를 실행하면서 시작된다. main메소드의 마지막 코드를 실행하거나 return문을 만나면 실행을 종료한다.
- 메인 스레드는 필요에 따라 추가 작업 스레드들을 만들어서 실행시킬 수 있다. 싱글 스레드에서는 메인 스레드가 종료되면 프로세스도 종료된다.
- 하지만 멀티 스레드에서는 실행 중인 스레드가 하나라도 있다면 프로세스는 종료되지 않는다. -> 메인 스레드가 작업 스레드보다 먼저 종료되더라도 작업 스레드가 계속 실행중이라면 프로세스는 종료되지 않는 것.
- 멀티 스레드로 실행하는 프로그램을 개발하려면, 몇개의 작업을 병렬로 실행할 지 결정하고 각 작업별로 스레드를 생성한다.
- 자바는 작업 스레드도 객체로 관리하기 때문에 클래스가 필요하다.
- thread 클래스로 직접 객체를 생성해도 되지만, 하위 클래스를 만들어 생성할 수도 있다.
Thread 클래스로 직접 생성
java.lang 패키지에 Thread클래스로부터 작업 스레드 객체를 직접 생성하려면, Runnable 구형 객체를 매개값으로 갖는 생성자를 호출한다
Thread thread = new Thread(Runnable target);
Runnable은 스레드가 작업을 실행할 때 사용하는 인터페이스이다. Runnable에는 run() 추상메서드가 정의되어 있고, 구현클래스가 이를 오버라이딩하여 스레드가 실행할 코드를 가지고 있어야 한다.
class Task implements Runnable {
@Override
public void run(){
//스레드가 실행할 코드
}
}
Runnable구현 클래스에 스레드의 작업 내용을 정의했으므로 이를 스레드에 전달해야 한다. 따라서 Runnable구현 객체를 생성한 후, Thread생성자 매개값으로 runnable 객체를 전달하는 것이다.
Runnbale task = new Task();
Thread thread = new Thread(task);
명시적으로 Runnable 구현 클래스를 작성하지 않고 thread생성자를 호출할 때, Runnable 익명 구현 객체를 매개값으로 사용할 수 있다. 오히려 이 방법이 더 많이 사용된다.
Thread thread = new Thread(new Runnable() {
@Override
public void run(){
//스레드가 실행할 코드
}
});
작업 스레드 객체를 생성한 후, 작업 스레드를 실행하려면 스레드 객체의 start()메소드를 호출해야 한다.
thread.start();
start메서드가 호출되면, 작업 스레드는 매개값으로 받은 Runnable의 run() 메소드를 실행하면서 작업을 처리한다.
Thread 하위 클래스로 객체 생성
- 작업 스레드 객체를 생성하는 또 다른 방법이다. Thread의 하위 객체로 만드는 것이다.
- Thread 클래스를 상속한 다음, run메소드를 오버라이딩하여 스레드가 실행될 코드를 작성하고 객체를 생성한다.
- 아니면 명시적인 하위 클래스를 정의하지 않고 Thread 익명 하위 객체를 사용할 수도 있다.
Thread thread = new Thread(){
@Override
public void run(){
// 스레드가 실행할 코드
}
};
thread.start();
스레드 이름 설정
메인 스레드는 'main'이라는 이름을 가지고 있고, 작업 스레드는 자동적으로 ' Thread-n'이라는 이름을 가진다. 작업 스레드의 이름을 다른 이름으로 설정하고 싶은 경우 setName() 메소드를 사용한다.
thread.setName("스레드 이름");
스레드 이름은 디버깅 시, 어떤 스레드가 작업을 하는지 조사할 목적으로 사용된다. 현재 코드를 어떤 스레드가 사용하는지 확인하려면 static메서드 currentThread()로 객체의 참조를 얻은 다음 getName()메소드를 출력한다.
스레드 상태
- 스레드 객체를 생성하고, start()메소드를 호출하면, 바로 스레드가 실행되는 것이 아니라 실행대기상태(runnable)가 된다. 실행 대기 상태란 실행을 기다리고 있는 상태를 말한다.
- 실행 대기 스레드는 cpu스케쥴링에 따라 cpu를 점유하고 run()메소드를 실행한다. 이때를 바로 실행(running)상태라고 한다. 실행 스레드는 run메소드를 모두 실행하기 전에 스케쥴링에 의해 다시 실행 대기 상태로 돌아갈 수 있다.
- 스레드는 실행 대기 상태와 실행 상태를 번갈아가면서 자신의 run() 메소드를 조금씩 실행 한다. 실행 상태에서 run() 메서드가 종료되면 실행할 코드가 없기 때문에, 스레드의 실행은 멈추게 된다. 이 상태를 종료상태(terminatd)라고 함
- 실행 상태 -> 일시정지 상태로 가기도 하는데, 일시 정지 상태는 스레드가 실행할 수 없는 상태를 말한다. 스레드가 다시 실행 상태로 가기 위해서는 일시 정지 상태 -> 실행 대기 상태로 가야만 한다.
일시정지로 가기 위한 메소드, 벗어나기 위한 메소드들
- 일시정지로 보냄: sleep(ling millis), join(), wait()
- 일시정지에서 벗어남: interrupt(), notify(), notifyAll()
- 실행 대시로 보냄: yield()
주어진 시간 동안 일시정지
1. sleep()
- 실행 중인 스레드를 일정 시간 멈추게 한다. 매개값에는 얼마동안 일시정지 상태로 만들건지, 밀리세컨드 단위로 시간을 준다. 1000 = 1초
- 일시정지 상태에는 interruptedException이 발생할 수 있기 때문에 sleep()은 예외 처리가 필요한 메소드이다.
다른 스레드의 종료를 기다림
- 다른 스레드와 독립적으로 실행하지만, 다른 스레드가 종료될 때 까지 기다렸다가 실행을 해야하는 경우도 있다. 예를 들어 계산 스레드의 작업이 종료된 후, 그 결과값을 받아 처리하는 경우.
- join() 메서드로 처리한다. threadA가 thread B의 join()메서드를 호출하면, A는 B가 종료될때까지 일시정지 상태가 된다. B의 run()메서드가 종료되고 나서, A는 일시정지에서 풀리고 다음 코드를 실행한다.
다른 스레드에게 실행 양보
- 스레드가 처리하는 작업은 반복적인 실행을 위해 for문이나 while문을 포함하는 경우가 많은데, 가끔 반복문이 무의미한 반복을 하는 경우가 있다.
- 이때, 다른 스레드에게 실행을 양보하고 자신은 실행 대기 상태로 가는 것이 프로그램 성능에 도움이 된다.
- yield() 메소드를 사용하여 해당 메소드를 호출한 스레드는 실행 대기 상태로 돌아가고, 다른 스레드가 실행 상태가 된다.
스레드 동기화
- 멀티 스레드는 하나의 객체를 공유해서 작업할 수도 있다. 이 경우, 다른 스레드에 의해 객체 내부 데이터가 쉽게 변경될 수 있기 때문에 의도한 바와 다르게 결과가 나올 수 있다.
- 스레드가 사용중인 객체를 다른 스레드가 변경할 수 없도록 하기 위해 스레드 작업이 끝날 때 까지 객체에 잠금을 건다. 이를 위해 자바는 동기화 메소드와 블록을 제공한다.
- 객체 내부에 동기화 메소드와 동기화 블록이 여러 개 있다면 스레드가 이들 중 하나를 실행할 때 다른 스레드는 해당 메소드는 물론이고 다른 동기화 메소드 및 블록도 실행할 수 없다. 하지만 일반 메소드는 실행이 가능하다.
동기화 메소드 및 블록 선언
동기화 메소드를 선언하려면synchronized 키워드를 붙인다. 인스턴스, static메소드 어디든 가능
public synchronized void method(){
// 단 하나의 스데르만 실행하는 영역
}
스레드가 동기화 메소드를 실행하는 즉시, 객체는 잠금이 일어나고 메소드 실행이 끝나야 잠금이 풀린다.
메소드 전체가 아닌 일부 영역을 실행할 때만 객체 잠금을 걸고 싶은 경우 동기화 블록을 만든다.
public void method(){
// 여러 스레드가 실행할 수 있는 영역
synchronized(공유객체){
// 단 하나의 스레드만 실행하는 영역
}
// 여러 스레드가 실행할 수 있는 영역
}
wait(), notify()
- 두 개의 스레드를 번갈아가며 실행할 경우 사용한다. 정확한 교대 작업이 필요할 경우, 자신의 작업이 끝나면 상대방 스레드를 일시정지 상태에서 풀어주고, 자신은 일시 정지 상태로 만드는 것이다.
- 공유객체가 이 방법의 핵심인데, 공유객체는 두 스레드가 작업할 내용을 각각 동기화 메소드로 정해 놓는다. 한 스레드가 작업을 완료하면 notify() 메소드를 호출해서 일시 정지 상태에 있는 다른 스레드를 실행 대기 상태로 만들고, 자신은 두 번 작업하지 않도록 wait()메소드를 호출하여 일시 정지 상태로 만든다.
- notify()는 wait()에 의해 일시 정지된 스레드 중 한개를 실행 대기 상태로 만듬
- notifyAll()은 wait()에 의해 일시 정지된 모든 스레드를 실행 대기 상태로 만든다.
- 두 메소드는 동기화 메소드 또는 동기화 블록내에서만 사용할 수 있다.
스레드 안전 종료
- 자신의 run()메소드가 모두 실행되면 자동적으로 종료되지만, 경우에 따라서 실행중인 스레드를 즉시 종료해야 할 떄가 있다. (사용자가 동영상을 끝까지 보지 않고 멈춤을 요구할 경우)
- 스레드를 강제 종료 시키기 위해 Thread는 stop()메소드를 제공하였지만 deprecated되었음. >> 왜냐? 스레드를 갑자기 종료하면 사용중이던 리소스들이 불안전한 상태로 남겨지기 때문이다. 여기에서 리소스란 파일, 네트워크 연결 등을 의미한다.
- 조건 이용
- 스레드가 while문을 반복한다면, 조건을 이용하여 run()메소드의 종료를 유도할 수 있다.
- interrupt() 메소드 이용
- 스레드가 일시정지 상태에 있을 때 InterruptedException 예외를 발생시키는 역할을 한다. 예외처리를 통해 run메소드를 정상 종료 시킨다.
- 스레드 생성 -> start메소드 실행 -> sleep메소드, 일시정지 -> interrupt()메소드 실행 -> 예외 발생, 예외 처리 블록으로 이동 -> 스레드가 사용한 리소스 정리하고 스레드 종료
- 단, 스레드가 실행/대기 상태일때는 interrupt메소드가 호출되어도 예외가 발생하지 않는다.
- 스레드가 어떤 이유로 일시정지 되면 그때 예외가 발생한다.
Q. 일시정지를 만들지 않고도 interrupt()메소드 호출 여부를 알 수 있는 방법은?
1. interrupted() >> static
2. isInterrupted() >> 인스턴스
데몬 스레드
- 주 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드이다. 주 스레드가 종료되면 데몬 스레드도 따라서 자동으로 종료된다.
- 데몬 스레드를 적용한 예로는 워드프로세서의 자동 저장, 미디어플레이어의 재상 등..이 있고 주 스레드인 워드 프로세서, 미디어플레이어가 종료되면 데몬 스레드도 종료된다고 보면 된다.
- 스레드를 데몬으로 만들기 위해서는 주 스레드가 데몬이 될 스레드의 setDemon(true)를 호출하면 된다.
public static void main(String[] args){
AutoSaveThread thread = new AutoSaveThread();
thread.setDaemon(true);
thread.start();
....
}
// 여기서 주 스레드는 main, 데몬 스레드는 autoSaveThread가 된다.
스레드 풀
- 병렬 작업 처리가 많아지면 스레드 개수가 폭증하여 cpu가 바빠지고 메모리 사용량이 늘어난다. 이에 따라 어플리케이션 성능이 저하 될 수 있다. 병렬 작업 증가로 인한 스레드의 폭증을 말으려면 스레드풀을 사용하는 것이 좋다.
- 스레드 풀은 작업 처리에 사용되는 스레드의 제한된 개수만큼 정해놓고 작업 큐에 들어오는 작업들을 스레드가 하나씩 맡아 처리하는 방식이다.
- 작업 처리가 끝난 스레드는 다시 작업 큐에서 새로운 작업을 가져와 처리한다. 이렇게 하면 작업량이 증가해도 스레드의 개수가 늘어나지 않아 어플리케이션의 성능이 급격히 저하되지 않는다.
- 애플리케이션 -> 스레드 풀에 작업 처리 요청 -> 작업 큐가 있으면 각 스레드는 큐에서 작업을 가져와 처리함 -> 최대 개수제한을 n개로 놓고, 결과 전달
스레드풀 생성
- 자바에서는 스레드풀을 생성하고 사용할 수 있도록 ExecutorService인터페이스와 Executors 클래스를 제공하고 있다.
- 초기 수: 스레드풀이 생성될 때 기본적으로 생성되는 스레드 수
- 코어 수: 스레드가 증가된 후 사용하지 않는 스레드를 제거할 때 최소한 풀에서 유지하는 스레드 수를 말한다.
- 최대 수: 증가되는 스레드의 한도 수
- 스레드 풀의 스레드는 기본적으로 데몬 스레드가 아니기 때문에 main스레드가 종료되더라도 작업을 처리하기 위해 계속 실행 상태로 남아있다. 스레드풀의 스레드를 종료하려면 shutdown(): 남아있는 작업을 마무리하고 스레드풀 종료 /shutdownNow()메서드: 남아있는 작업이 있든 말든 강제로 종료 중 하나를 사용해야 한다.
작업
하나의 작업은 Runnable 또는 Callable 구현 객체로 표현한다. 둘의 차이점은 작업 처리 완료 후 리턴값이 있느냐 없으냐이다
new Runnable() 익명 구현 객체는 return이 없음, new Callable<T>() 익명 구현 객체는 return T, 리턴값이 있음
작업 처리 요청이란 ExecutorService의 작업 큐에 runnable, Callable 객체를 넣는 행위를 말한다.