📌프로세스와 스레드
프로세스(process)란 간단히 말해서 '실행 중인 프로그램'이다. 프로그램을 실행하면 OS로부터 실행에 필요한 자원(메모리)을 할당받아 프로세스가 된다.
또한 하나의 프로그램(어플리케이션)은 다중 프로세스를 만들기도 하는데, 예를 들어 이클립스 프로그램을 두 개 실행한다거나, Chrome 브라우저를 두 개 실행했다면 두 개의 Chrome 프로세스 혹은 이클립스 프로세스가 생성된 것이다.
멀티 태스킹은 두 가지 이상의 작업을 동시에 처리해주는 것을 말하는데, OS는 멀티 태스킹을 할 수 있도록 CPU 및 메모리 자원을 프로세스마다 적절히 할당해주고, 병렬로 실행시킨다. 즉, 동시에 여러 프로세스를 실행시키는 것이다. 예를 들어 웹 서핑을 하면서 넷플릭스를 감상하는 경우가 바로 멀티 태스킹인 경우이다.
하나의 프로세스 내에서 멀티 태스킹을 할 수 있도록 만들어진 프로그램(어플리케이션)들도 있는데 대표적인 것이 바로 메신저 또는 미디어 플레이어이다. 메신저는 채팅 기능을 제공하면서 동시에 파일 전송 기능을 수행하기도 하고 미디어 플레이어 같은 경우 동영상 재생과 음악 재생이라는 작업을 동시에 처리한다. 그 방법은 바로 멀티 스레드이다.
프로세스는 프로그램을 수행하는 데 필요한 데이터와 메모리 등이 자원 그리고 스레드로 구성되어 있으며 프로세스의 자원을 이용해서 실제로 작업을 수행하는 것이 바로 스레드이다. 그래서 모든 프로세스에는 최소한 하나 이상의 스레드가 존재하며, 둘 이상의 스레드를 가진 프로세스를 '멀티 스레드 프로세스'라고 한다.
멀티 프로세스들은 OS에서 할당받은 자신의 메모리를 가지고 실행하기 때문에 서로 독립적이다. 때문에 하나의 프로세스에서 오류가 발생해도 다른 프로세스에 영향이 미치지 않는다. 하지만 멀티 스레드는 하나의 프로세스 내부에 생성되기 때문에 하나의 스레드가 예외를 발생시키면 프로세스 자체가 종료될 수 있어 다른 스레드에게 영향을 미치게 된다.
📌싱글 스레드 vs 멀티 스레드
위 그림은 싱글 스레드 프로세스와 멀티 스레드 프로세스의 작업을 비교한 그림이다.
위 그림을 보면 하나의 스레드로 두개의 작업을 수행한 시간과 두 개의 스레드로 두 개의 작업을 수행한 시간은 거의 같고 오히려 두 개의 스레드로 작업한 시간이 싱글 스레드로 작업한 시간보다 더 걸리게 되는데 그 이유는 스레드간의 작업 전환(context switching)에 시간이 걸리기 때문이다.
그래서 싱글 코어에서 단순히 CPU만을 사용하는 계산작업이라면 오히려 멀티 스레드보다 싱글 스레드로 프로그래밍하는 것이 더 효율적이다.
📌메인 스레드
모든Java 어플리케이션은 메인 스레드가 main() 메소드를 실행하면서 시작된다.
메인 스레드는 필요에 따라 작업 스레드들을 만들어서 병렬로 코드를 실행할 수 있다. 즉 멀티 스레드를 생성해서 멀티 태스킹을 수행한다.
싱글 스레드 어플리케이션에서는 메인 스레드가 종료하면 프로세스도 종료된다. 하지만 멀티 스레드 어플리케이션에서는 실행 중인 스레드가 하나라도 있다면, 프로세스는 종료되지 않는다. 메인 스레드가 작업 스레드보다 먼저 작업이 종료가 되더라도 작업 스레드가 계속 실행 중이라면 프로세스는 종료되지 않는다.
📌작업 스레드 생성과 실행
멀티 스레드로 실행하는 어플리케이션을 개발하려면 먼저 몇 개의 작업을 병렬로 실행할지 결정하고 각 작업별로 스레드를 생성해야 한다. 즉, 메인 작업 이외의 추가적인 병렬 작업의 수만큼 스레드를 생성하면 된다. 자바에서는 작업 스레드도 객체로 생성되기 때문에 클래스가 필요하다. 스레드를 생성하는 방법은 두 가지가 있다.
1. java.lang.Thread 클래스를 직접 객체화해서 생성
2. Thread를 상속해서 하위 클래스를 만들어 생성
1. java.lang.Thread 클래스를 직접 객체화해서 생성
java.lang.Thread 클래스로부터 작업 스레드 객체를 직접 생성하려면 Runnable을 매개값으로 갖는 생성자를 호출해야 한다.
Thread thread = new Thread(Runnable target);
Runnable은 작업 스레드가 실행할 수 있는 코드를 가지고 있는 객체라고 해서 붙여진 이름이다.
Runnable은 인터페이스 타입이기 때문에 구현 객체를 만들어 대입해야 한다. Runnable인터페이스는 오로지 run( )만 정의되어 있는 간단한 인터페이스이다. Runnable인터페이스를 구현하기 위해서 해야 할 일은 추상메서드인 run( )의 몸통 { }을 만들어 주는 것 뿐이다.
class Task implements Runnable{
public void run(){
스레드가 실행할 코드;
}
}
Runnable task = new Task();
Thread thread = new Thread(task); //작업 스레드가 생성된다.
Runnable은 작업 내용을 가지고 있는 객체이고 실제 스레드는 아니기 때문에 Runnable 구현 객체를 생성한 후, 이것을 매개값으로 해서 Thread 생성자를 호출하면 비로소 작업 스레드가 생성된다. 즉 스레드를 구현한다는 것은, 어떤 방법을 선택하든 그저 쓰레드를 통해 작업하고자 하는 내용으로 run( )의 몸통{ }을 채우는 것일 뿐이다.
보통 코드를 절약하기 위해 Thread 생성자를 호출할 때 Runnable 익명 객체를 매개값으로 사용하여 구현을 많이한다. Runnable 인터페이스는 run() 메소드 하나만 정의되어 있기 때문에 함수적 인터페이스이다. 따라서 람다식을 매개값으로 사용할 수도 있다.
Thread thread = new Thread(new Runnable(){ //익명 구현 객체
public void run(){
스레드가 실행할 코드;
}
});
Thread thread = new Thread( () -> { //람다식
스레드가 실행할 코드;
});
2. Thread를 상속해서 하위 클래스를 만들어 생성
작업 스레드가 실행할 작업을 Runnable로 만들지 않고, Thread의 하위 클래스로 작업 스레드를 정의하면서 작업 내용을 포함시킬 수 있다. Thread 클래스를 상속한 후 run 메소드를 재정의해서 스레드가 실행할 코드를 작성하면 된다. 작업 스레드 클래스로부터 작업 스레드 객체를 생성하는 방법은 일반적인 객체를 생성하는 방법과 동일하다.
class MyThread extends Thread{
public void run() { /*작업내용*/} //Thread클래스의 run()을 오버라이딩
}
Thread클래스를 상속받으면 다른 클래스를 상속받을 수 없기 때문에, Runnable인터페이스를 구현하는 방법이 일반적이다. Runnable인터페이스를 구현하는 방법은 재사용성이 높고 코드의 일관성을 유지할 수 있기 때문에 보다 객체지향적인 방법이라 할 수 있다.
1, 2 방법 비교
class ThreadEx{
public static void main(String[] args){
ThreadEx1_1 t1 = new ThreaEx_1_1(); //Thread의 자손 클래스의 인스턴스를 생성
Runnable r = new ThreadEx1_2(); //Runnable을 구현한 클래스의 인스턴스를 생성
Thread t2 = new Thread(r); //생성자 Thread(Runnable target)
//Thread t2 = new Thread(new ThreadEx1_2()); //위의 두 줄을 한 줄로 간단히
t1.start();
t2.start();
}
}
class ThreadEx1_1 extends Thread{ // Thread클래스의 run()을 오버라이딩
public void run(){
for(int i=0;i<5;i++){
System.out.println(getName()); //조상인 Thread의 getName()을 호출
}
}
}
class ThreadEx1_2 implements Runnable{ //Runnable인터페이스의 추상메서드 run()을 구현
public void run(){
for(int i=0;i<5;i++){
//Thread.currentThread() -현재 실행중인 Thread를 반환한다.
System.out.println(Thread.currentThread().getName());
}
}
}
//실행결과
//Thread-0
//Thread-0
//Thread-0
//Thread-0
//Thread-0
//Thread-1
//Thread-1
//Thread-1
//Thread-1
//Thread-1
Thread클래스를 상속받은 경우와 Runnable인터페이스를 구현한 경우의 인스턴스 생성 방법이 다르다.
Thread클래스를 상속받으면, 자손 클래스에서 조상인 Thread클래스의 메서드를 직접 호출할 수 있지만, Runnable을 구현하면 Thread클래스의 static메서드인 currentThread()를 호출하여 스레드에 대한 참조를 얻어 와야만 호출이 가능하다.
static Thread currentThread() 현재 실행중인 쓰레드의 참조를 반환한다.
String getName() 쓰레드의 이름을 반환한다.
Thread를 상속받은 ThreadEx1_1에서는 간단히 getName()을 호출하면 되지만,
Runnable을 구현한 ThreadEx1_2에는 멤버라고는 run()밖에 없기 때문에 Thread클래스의 GetName()을 호출하려면, 'Thread.currentThread().getName()'와 같이 해야한다.
스레드의 이름
스레드는 자신의 이름을 갖고 있다. 스레드의 이름이 특별한 역할을 하는 것은 아니지만, 디버깅할 때 어떤 스레드가 어떤 작업을 하는지를 알고 싶을때 사용된다. 메인 스레드는 'main'이라는 이름을 갖고 있고, 우리가 생성한 스레드는 자동적으로 'Thread-n'이라는 이름으로 설정된다. 다른 이름으로 설정하고 싶으면 다음과 같은 메소드를 사용하면된다.
thread.setName("스레드 이름"); //스레드 이름 변경
thread.getName(); //스레드 이름 얻기
setName()과 getName()은 Thread의 인스턴스 메소드이므로 스레드 객체의 참조가 필요하다. 만약 스레드 객체의 참조를 가지고 있지 않다면, Thread의 정적 메소드인 currentThread()로 코드를 실행하는 현재 스레드의 참조를 얻을 수 있다.
따라서 위에도 설명했듯이 Runnable을 구현한 스레드 같은 경우 이름을 얻으려면 스레드의 참조를 얻은 후 이름을 얻어야한다.
스레드의 실행 - thread.start();
스레드를 생성했다고 해서 자동으로 실행되는 것은 아니다. start( )를 호출해야만 스레드가 실행된다.
t1.start( ); //쓰레드 t1을 실행시킨다.
t2.start( ); //쓰레드 t2를 실행시킨다.
사실은 start( )가 호출되었다고 해서 바로 실행되는 것이 아니라, 일단 실행대기 상태에 있다가 자신의 차례가 되어야 실행된다. 물론 실행대기중인 쓰레드가 하나도 없으면 곧바로 실행상태가 된다.
추가로 알아야할 것은 한 번 실행이 종료된 쓰레드는 다시 실행할 수 없다는 것이다. 즉 , 하나의 쓰레드에 대하 start( )가 한 번만 호출될 수 있다는 뜻이다.
그래서 만일 쓰레드의 작업을 한 번 더 수행해야 한다면 새로운 쓰레드를 생성한 다음에 start( )를 호출해야 한다.
만일 하나의 쓰레드에 대하 start( )를 두번 이상 호출하면 실행시에 IllegalThreadStateException이 발생한다.
start( )와 run( )
스레드를 실행시킬 때 run( )이 아닌 start( )를 호출한다.
main메서드에서 run( )을 호출하는 것은 생성된 스레드를 실행시키는 것이 아니라 단순히 클래스에 선언된 메서드를 호출하는 것일 뿐이다.
반면에 start( )는 새로운 스레드가 작업을 실행하는데 필요한 호출스택을 생성한 다음에 run( )을 호출해서, 생성된 호출스택에 run( )이 첫 번째로 올라가게 한다. 모든 스레드는 독립적인 작업을 수행하기 위해 자신만의 호출스택을 필요로 하기 때문에, 새로운 스레드를 생성하고 실행시킬 때마다 새로운 호출스택이 생성되고 스레드가 종료되면 작업에 사용된 호출스택은 소멸된다. 호출스택에서는 가장 위에 있는 메서드가 현재 실행중인 메서드이고 나머지 메서드들은 대기상태에 있다. 스케줄러는 실행대기중인 쓰레드들의 우선순위를 고려하여 실행순서와 실행시간을 결정하고, 각 스레드들은 작성된 스케줄에 따라 자신의 순서가 되면 지정된 시간동안 작업을 수행한다.
주어진 시간동안 작업을 마치지 못한 스레드는 다시 자신의 차례가 돌아올 때까지 대기상태로 있게 되며, 작업을 마친 스레드, 즉 run( )의 수행이 종료된 쓰레드는 호출스택이 모두 비워지면서 이 스레드가 사용하던 호출스택은 사라진다.
이는 마치 자바프로그램을 실행하면 호출스택이 생성되고 main메서드가 처음으로 호출되고, main메서드가 종료되면 호출스택이 비워지면서 프로그램도 종료되는 것과 같다.
📌스레드의 우선순위 (priority of thread)
멀티 스레드는 동시성 또는 병렬성으로 실행되기 때문에 이 용어에 대한 개념을 알아둬야 한다.
'동시성'은 멀티 작업을 위해 하나의 코어에서 멀티 스레드가 번갈아가며 실행하는 성질을 말하고,
'병렬성'은 멀티 작업을 위해 멀티 코어에서 개별 스레드를 동시에 실행하는 성질을 말한다.
싱글 코어 CPU를 이용한 멀티 스레드 작업은 병렬적으로 실행되는 것 처럼 보이지만, 사실은 번갈아가며 실행하는 동시성 작업이다. 번갈아 실행하다보니 병렬성으로 보일 뿐이다.
스레드의 개수가 코어의 수보다 많은 경우, 스레드를 어떤 순서에 의해 동시성으로 실행할 것인가를 결정해야 하는데, 이것을 스레드 스케줄링이라 한다. 이 스레드 스케줄링에 의해 스레드들은 아주 짧은 시간에 번갈아가면서 그들의 run() 메소드를 조금씩 실행한다.
자바의 스레드 스케줄링은 우선순위 방식과 순환 할당 방식을 사용한다.
우선순위 방식은 우선순위가 높은 스레드가 실행 상태를 더 많이 가지도록 스케줄링하는 것을 말한다.
순환할당 방식은 시간 할당량을 정해서 하나의 스레드를 정해진 시간만큼 실행하고 다시 다른 스레드를 실행하는 방식을 말한다. 스레드 우선순위 방식은 스레드 객체에 우선 순위 번호를 부여할 수 있기 때문에 개발자가 코드로 제어할 수 있지만, 순환할당 방식은 JVM에 의해서 정해지기 때문에 코드로 제어할 수 없다.
스레드가 가질 수 있는 우선순위의 범위는 1~10이며 숫자가 높을수록 우선순위가 높다. 스레드의 우선순위는 스레드를 생성한 스레드로부터 상속받는 것이다. main메서드를 수행하는 스레드는 우선순위가 5이므로 main메서드 내에서 생성하는 스레드의 우선순위는 자동적으로 5가 된다.
📌동기화 메소드와 동기화 블록
싱글 스레드 프로그램에서는 한 개의 스레드가 객체를 독차지해서 사용하면 되지만, 멀티 스레드 프로그램에서는 여러 스레드들이 객체를 공유해서 작업해야 하는 경우가 있다. 이때 한 스레드가 사용 중인 객체를 다른 스레드가 접근하게 되면 무결성을 해치게 된다. 따라서 이를 방지하기 위해서는 스레드 작업이 끝날 때까지 객체에 잠금을 걸어서 다른 스레드가 사용할 수 없도록 해야 한다. 멀티 스테르 프로그램에서 단 하나의 스레드만 실행할 수 있는 코드 영역을 임계 영역(critical section)이라고 한다.
Java는 임계 영역을 지정하기 위해 동기화(synchronized) 메소드와 동기화 블록을 제공한다. 스레드가 객체 내부의 동기화 메소드 또는 블록에 들어가면 즉시 객체에 잠금을 걸어 다른 스레드가 임계 영역 코드를 실행하지 못하도록 한다.
동기화 메소드를 만드는 방법은 메소드 선언에 synchronized 키워드를 붙이면 된다. synchronized 키워드는 인스턴스와 정적 메소드 어디든 붙일 수 있다.
public synchronized void method() {
임계 영역; //단 하나의 스레드만 실행
}
동기화 메소드는 메소드 전체 내용이 임계 영역이므로 스레드가 동기화 메소드를 실행하는 즉시 객체 잠금이 일어나고, 스레드가 동기화 메소드를 실행 종료하면 잠금이 풀린다. 메소드 전체 내용이 아니라, 일부 내용만 임계 영역으로 만들고 싶다면 동기화 블록을 만들면 된다.
public void method(){
//여러 스레드가 실행 가능 영역
...
//////////////////////////////////////////// 동기화 블록
synchronized(공유 객체){ //공유 객체가 객체 자신이면 this를 넣을 수 있다.
임계 영역 // 단 하나의 스레드만 실행
}
////////////////////////////////////////////
//여러 스레드가 실행 가능 영역
...
}
동기화 블록의 외부 코드들은 여러 스레드가 동시에 실행할 수 있지만, 동기화 블록의 내부 코드는 임계 영역이므로 한 번에 한 스레드만 실행할 수 있고 다른 스레드는 실행할 수 없다.
만약 동기화 메소드와 동기화 블록이 여러 개 있을 경우, 스레드가 이들 중 하나를 실행할 때 다른 스레드는 해당 메소드는 물론이고 다른 동기화 메소드 및 블록도 실행할 수 없다. 만약 동기화 메소드와 동기화 블록이 여러 개 있을 경우, 스레드가 이들 중 하나를 실행할 때 다른 스레드는 해당 메소드는 물론이고 다른 동기화 메소드 및 블록도 실행할 수 없다. 하지만 일반 메소드는 실행이 가능하다.
📌스레드의 상태 제어
스레드의 상태
- 스레드 객체를 생성(NEW)
- start() 메소드 호출하면 스레드는 실행 대기 상태(RUNNABLE)
- 실행 대기 상태에 있는 스레드 중 스레드 스케줄링으로 선택된 스레드가 CPU를 점유하고 run() 메소드를 실행한다.
이때를 실행(Running) 상태라고 한다. 실행 상태의 스레드는 run() 메소드를 모두 실행하기 전에 스레드 스케줄링에 의해 다시 실행 대기 상태로 돌아갈 수 있다. 그리고 실행 대기 상태에 있는 다른 스레드가 선택되어 실행 상태가 된다. 이렇게 스레드는 실행 대기 상태와 실행 상태를 번갈아가면서 자신의 run() 메소드를 조금씩 실행한다. - 실행 상태에서 run() 메소드가 종료되면, 더 이상 실행할 코드가 없기 때문에 스레드의 실행은 멈추게 되고, 이 상태를 종료 상태라고 한다.
경우에 따라 스레드는 실행 상태에서 실행 대기 상태로 가지 않고, 일시 정지 상태로 가기도 하는데, 이는 스레드가 실행할 수 없는 상태일 경우이다. 일시 정지 상태에 있는 스레드가 다시 실행 상태로 가기 위해서는 실행 대시 상태로 먼저 가야 한다.
Thread 클래스에 getState() 메소드를 실행하면 스레드 상태를 리턴한다.
사용자가 동영상을 보다가 일시 정지시킬 수도 있고, 종료시킬 수도 있다. 일시 정지인 경우 동영상 스레드를 일시 정지 상태로 만들어야 하고, 종료는 스레드를 종료 상태로 만들어야 한다. 이와 같이 실행 중인 스레드의 상태를 변경하는 것을 스레드 상태 제어라고 한다.
sleep() - 주어진 시간동안 스레드를 멈추게 한다.
1. 실행 중인 스레드를 일정시간 멈추게 한다. 주어진 시간 동안 일시 정지 상태가 되고, 다시 실행 대기 상태로 돌아간다.
static void sleep(long millis) //천분의 일초 단위
static void sleep(long millis, int nanos) //천분의 일초 + 나노초
2.예외처리를 해야한다.
sleep()에 의해 일시정지 상태가 된 스레드는 지정된 시간이 다 되거나 interrupt()가 호출되면, InterruptedException이 발생되어 잠에서 깨어나 실행대기 상태가 된다. 그래서 sleep( )을 호출할 때는 항상 try-catch문으로 예외를 처리해줘야 한다. 매번 예외처리를 해주는 것이 번거롭기 때문에, 아래와 같이 try-catch문까지 포함하는 새로운 메서드를 만들어서 사용하기도 한다.
void delay(long millis){
try{
Thread.sleep(millis);
}catch(InterruptedException e) { }
}
yield( ) - 다른 스레드에게 실행 양보한다.
yield( )는 스레드 자신에게 주어진 실행시간을 다음 차례의 스레드에게 양보하고, 자신은 실행대기한다.
yield( )와 interrupt( )를 적절히 사용하면, 프로그램의 응답성을 높이고 보다 효율적인 실행이 가능하게 할 수 있다.
join( ) - 다른 쓰레드의 작업을 기다린다.
스레드는 다른 스레드와 독립적으로 실행하는 것이 기본이지만 다른 스레드가 종료될 때까지 기다렸다가 실행해야 하는 경우가 발생할 수도 있다. 즉 스레드 자신이 하던 작업을 잠시 멈추고 다른 스레드가 지정된 시간동안 작업을 수행하도록 할 때 join( )을 사용한다.
스레드의 안전한 종료 - interrupt( )와 interrupted( )
스레드는 자신의 run() 메소드가 모두 실행되면 자동적으로 종료된다. 하지만 경우에 따라서 진행 중인 쓰레드의 작업이 끝나기 전에 즉시 종료할 때가 있다. Thread는 스레드를 즉시 종료시키기 위해서 stop() 메소드를 제공하고 있는데, 이 메소드는 deprecated되었다. 그 이유는 stop() 메소드로 스레드를 갑자기 종료하게 되면 스레드가 사용 중이던 자원(자원: 파일, 네트워크 연결 등)들이 불완전한 상태로 남겨지기 때문이다.
스레드를 종료 시키는 가장 안전한 방법은 바로, run() 메소드가 정상적으로 종료되도록 유도하는 것이다. 위에서 설명했듯이 스레드는 run() 메소드가 끝나면 자동적으로 종료되기 때문에 run() 메소드가 정상적으로 끝나게 유도하면 된다. 이 방법으로는 두 가지 방법이 있다.
- stop 플래그를 이용하는 방법
stop 플래그를 이용하여 스레드를 종료시킬 지점을 사용자가 지정하는 것이다. - interrupt() 메소드를 이용하는 방법
interrupt() 메소드는 스레드가 일시 정지 상태에 있을 때 InterruptedException 예외를 발생시키는 역할을 한다. 이것을 이용하면 run()메소드를 정상 종료시킬 수 있다. 주목할 점은 스레드가 실행 대기 또는 실행 상태에 있을 때 interrupt() 메소드가 실행되면 즉시 InterruptedException 예외가 발생하지 않고, 스레드가 후에 일시 정지 상태가 되면 그제서야 예외가 발생한다. 따라서 스레드가 일시 정지 상태가 되지 않으면 interrupt() 메소드 호출은 아무런 의미가 없다. 그래서 짧은 시간이나마 일시 정지시키기 위해 Thread.sleep() 메소드를 사용한다.
일시 정지를 만들지 않고도 interrupt() 호출 여부를 알 수 있는 방법이 있다. 바로 interrupted() 메소드인데 만약 interrupt() 메소드가 호출되었다면 스레드의 interrupted()와 isInterrupted() 메소드는 true를 리턴한다. interrupted()는 정적 메소드로 현재 스레드가 interrupted 되었는지 확인하는 것이고, isInterrupted()는 인스턴스 메소드로 현재 스레드가 interrupted되었는지 확인할 때 사용한다.
쓰레드가 sleep( ), wait( ), join( )에 의해 '일시정지 상태'에 있을 때, 해당 쓰레드에 대해 interrupt()를 호출하면, Interrupted Exception이 발생하고 쓰레드는 '실행 대기 상태(RUNNABLE)로 바뀐다. 즉 멈춰있던 쓰레드를 깨워서 실행가능한 상태로 만드는 것이다.
📌데몬 쓰레드
데몬 쓰레드는 다른 일반 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드이다. 일반 스레드가 모두 종료되면 데몬 스레드는 강제적으로 자동 종료되는데, 그 이유는 데몬 스레드는 일반 쓰레드의 보조역할을 수행하므로 일반 스레드가 모두 종료되고 나면 데몬 스레드의 존재의 의미가 없어지기 때문이다. 이 점을 제외하면 일반 스레드와 큰 차이가 없다.
📌스레드 그룹
스레드 그룹은 관련된 스레드를 묶어서 관리할 목적으로 이용된다. JVM이 실행되면 system 스레드 그룹을 만들고, JVM 운영에 필요한 스레드들을 생성해서 system 스레드 그룹에 포함시킨다. 그리고 system의 하위 스레드 그룹으로 main을 만들고 메인 스레드를 main 스레드 그룹에 포함시킨다. 스레드는 반드시 하나의 스레드 그룹에 포함되는데, 명시적으로 스레드 그룹에 포함시키지 않으면 기본적으로 자신을 생성한 스레드와 같은 스레드 그룹에 속하게 된다. 우리가 생성하는 작업 스레드는 대부분 main 스레드가 생성하므로 기본적으로 main 스레드 그룹에 속하게 된다.
(즉 스레드는 자신을 생성한 스레드의 그룹과 우선순위를 상속받는다.)
현재 스레드가 속한 스레드 그룹의 이름 얻기
//현재 스레드가 속한 스레드 그룹의 이름 얻기
ThreadGroup group = Thread.currentThread.getThreadGroup();
String groupName = group.getName();
현재 실행하고 있는 스레드의 이름과 데몬 여부 그리고 속한 스레드 그룹 이름이 무엇인지 출력하기
public class ThreadInfoExample{
public static void main(String[] args){
AutoSaveThread autoSaveThread = new AutoSaveThread();
autoSaveThread.setName("AutoSaveThread");
autoSaveThread.setDaemon(true);
autoSaveThread.start();
//Thread의 정적 메소드인 getAllStackTraces()를 이용하여
//프로세스 내에서 실행하는 모든 스레드에 대한 정보를 얻음
Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
Set<Thread> threads = map.keySet();
for(Thread thread : threads){
System.out.println("Name: " + thread.getName() +
((thread.isDaemon())?"(데몬)": "(주)"));
System.out.println("\t" + "소속그룹: " + thread.getThreadGroup().getName());
System.out.println();
}
}
}
📌스레드풀(ThreadPool)
병렬 작업 처리가 많아지면 스레드 개수가 증가되고 그에 따른 스레드 생성과 스케줄링으로 인해 CPU가 바빠져 메모리 사용량이 늘어나고 어플리케이션의 성능이 저하된다. 떄문에 병렬 작업의 폭증으로 인한 스레드의 폭증을 막으려면 스레드풀(ThreadPool)을 사용해야 한다.
스레드풀은 작업 처리에 사용되는 스레드를 제한된 개수만큼 정해 놓고 작업 큐(Queue)에 들어오는 작업들을 하나씩 스레드가 맡아 처리한다. 작업 처리가 끝난 스레드는 다시 작업 큐에서 새로운 작업을 가져와 처리한다. 그렇기 때문에 작업 처리 요청이 많아져도 스레드의 전체 개수가 늘어나지 않으므로 어플리케이션의 성능이 급격히 저하되지 않는다.
Java는 스레드풀을 생성 및 사용할 수 있도록 java.util.concurrent 패키지에서 ExecutorService인터페이스와 Executors 클래스를 제공하고 있다. Executors의 다양한 정적 메소드를 이용해서 ExecutorService 구현 객체를 만들 수 있는데, 이것이 바로 스레드풀이다.
'Java' 카테고리의 다른 글
[Java] 스트림(stream) (0) | 2021.08.07 |
---|---|
[Java] 람다식 (Lambda expression) (0) | 2021.08.07 |
[Java] 날짜와 시간(Date, Calendar 클래스) & 형식(Format 클래스) (0) | 2021.08.04 |
[Java]Java.lang 패키지 (Object, System, Class, String, String, Pattern, Arrays, Math, Wrapper) (0) | 2021.08.04 |
Java 스레드(Thread) (0) | 2021.05.05 |