Back-end/Java

다시 하자 기초! 쓰레드

이안_ian 2019. 5. 2. 23:19
반응형

프로세스란

간단히 말해서 실행 중인 프로그램이다. 프로그램을 실행하면 OS로부터 실행에 필요한 자원을 할당 받아 프로세스가 됨.

프로세스는 프로그램을 수행하는데 필요한 데이터와 메모리등의 자원 그리고 쓰레드로 구성되어 있으며 프로세스의 자원을 이용해서 실제로 작업을 수행하는 것이 바로 쓰레드이다.

 

그래서 모든 프로세스에는 최소한 하나 이상의 쓰레드가 존재하며, 둘 이상의 쓰레드를 가진 프로세스를 멀티 쓰레드 프로세스라고 한다. 하나의 프로세스가 가질 수 있는 쓰레드 개수는 제한되어 있지않지만 메모리 공간 때문에 적절한 갯수를 사용해야한다.

 

멀티태스킹과 멀티쓰레딩

윈도우나 유닉스같은 대부분의 OS는 멀티태스킹을 지원하기 때문에 여러 개의 프로세스가 동시에 실행될 수 있다. 

이와 마찬가지로 멀티쓰레딩은 하나의 프로세스 내에서 여러 쓰레드가 동시에 작업을 수행하는 것이다. CPU의 코어가 한 번에 단 하나의 작업만 수행할 수 있으므로, 실제로 동시에 처리되는 작업의 개수는 코어의 개수와 일치한다.

 

그러나 처리해야되는 쓰레드의 수는 언제나 코어의 개수보다 훨씬 많기 때문에 각 코어가 아주 짧은 시간 동안 여러 작업을 번갈아 가며 수행함으로써 여러 작업들이 모두 동시에 수행되는 것 처럼 보인다.

그래서 프로세스의 성능이 단순히 쓰레드의 개수에 비례하는 것이 아니며 하나의 쓰레드를 가진 프로세스보다 두 개의 쓰레드를 가진 프로세스가 오히려 더 낮은 성능을 보일 수도 있다.

 

멀티쓰래딩의 장점

-CPU의 사용률을 향상시킨다.
-자원을 보다 효율적으로 사용할 수 있다.
-사용자에 대한 응답성이 향상된다.
-작업이 분리되어 코드가 간결해진다.

멀티쓰래딩의 단점

여러 쓰레드가 같은 프로세스 내에서 자원을 공유하면서 작업을 하기 때문에 발생할 수 있는 동기화, 교착상태와 같은 문제들을 고려해야한다.

 

쓰레드의 구현과 실행

쓰레드를 구현하는 방법은 Thread클래스를 상속받는 방법과 Runnable인터페이스를 구현하는 방법이 있다. 어느 쪽을 선택해도 상관은 없지만 Thread를 상속받으면 다른 클래스를 상속 받지 못하기에 인터페이스쪽을 많이 사용함.

1. Thread 클래스 상속
class MyThread extends Thread {
 public void run() {  /* 작업 내용 */ }		//Thread클래스의 run()을 오버라이딩
}
 
 2. Runnable인터페이스를 구현
 class MyThread implements Runnable {
  public void run() { /* 작업 내용 */ }		//Runnable인터페이스와 run()을 구현
 }

Runnable인터페이스는 오직 run()만 정의되어 있는 간단한 인터페이스다. run()만 구현해주면 된다.

class ThreadEx1 {
 public static void main(String args[]) {
  ThreadEx1_1 t1 = new ThreadEx1_1();
  
  Runnable r = new ThreadEx1_2();
  Thread t2 = new Thread(r);  		//생성자 Thread(Runnable targer)
  
  t1.start();
  t2.start();
 }
}

class ThreadEx1_1 extends Thread {
 public void run() {
  for(int i=0;i<5;i++){
   System.out.println(getName());
   }
  }
}

class ThreadEx1_2 Implements Runnable {
 public void run() {
  for(int i=0;i<5;i++) {
   //Thread.currentThread() 		// 현재 실행중인 Thread를 반환한다.
   System.out.println(getName());
   }
  }
}

위와 같이 Runnable인터페이스를 구현한 경우, Runnable인터페이스를 구현한 클래스의 인스턴스를 생성한 다음, 이 인스턴스를 Thread클래스의 생성자의 매개변수로 제공해야한다. 인스턴스변수로 Runnable타입의 변수 r을 선언해 놓고 생성자를 통해서 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()와 같이 해야한다.

 

주의할점!!

-쓰레드를 생성했다고 해서 자동으로 실행되는 것이 아니라 start()메서드를 호출해야만 실행된다.

-한 번 실행이 종료된 쓰레드는 다시 실행할 수 없다. 그래서 다시 실행하려면 새로운 인스턴스를 생성하고 start()호출!!

 

start()와 run()

main메서드에서 run()을 호출하는 것은 생성된 쓰레드를 실행시키는 것이 아니라 단순히 클래스에 선언된 메서드를 호출 하는 것일 뿐이다. 반면에 start()는 새로운 쓰레드가 작업을 실행하는데 필요한 호출스택을 생성한 다음에 run()을 호출해서, 생성된 호출 스택에 run()이 첫 번째로 올라가게 한다.

모든 쓰레드는 독립적인 작업을 수행하기 위해 자신만의 호출스택을 필요로 하기 때문에, 새로운 쓰레드를 생성하고 실행시킬 때마다 새로운 호출스택이 생성되고 쓰레드가 종료되면 작업에 사용된 호출스택은 소멸된다.

1. main메서드에서 쓰레드의 start()를 호출한다.
2. start()는 새로운 쓰레드를 생성하고, 쓰레드가 작업하는데 사용될 호출스택을 생성한다.
3. 새로 생성된 호출스택에 run()이 호출되어, 쓰레드가 독립된 공간에서 작업을 수행한다.
4. 이제는 호출스택이 2개가 되므로 스케쥴러가 정한 순서에 의해서 번갈아 가면서 실행된다.

그리고 호출스택에서 가장 위에 있는 메서드가 현재 실행중이고 나머지들은 대기상태인데 쓰레드에서는 스케쥴러로 관리하는데 맨 위에 있어도 대기가 될 수 있다. 즉 스케쥴러의 우선순위에 따라 작업 수행. 새로 생성된 호출스택은 run이 모두 비워지면 사라진다.

 

main쓰레드

main메서드의 작업을 수행하는 것도 쓰레드이며, 이를 main쓰레드라고 한다. 지금까지는 main메서드가 수행을 마치면 프로그램이 종료되었으나 다른 쓰레드가 아직 작업을 마치지않으면 프로그램은 종료되지 않는다.

 

싱글쓰레드와 멀티쓰레드

두 개의 작업을 하나으 쓰레드로 처리하는 경우와 두 개의 쓰레드로 처리하는 경우를 가정해보자. 하나의 쓰레드로 두 작업을 처리하는 경우는 한 작업이 마친 후에 다른 작업을 시작하지만 두 개의 쓰레드로 작업 하는 경우에는 짧은 시간동안 2개의 쓰레드가 번갈아 가면서 작업을 수행해서 동시에 두 작업이 처리되는 것과 같이 느끼게 한다.

 

위의 그래프에서 알 수 있듯이 하나의 쓰레드로 두개의 작업을 수행한 시간과 두개의 쓰레드로 두개의 작업을 수행한 시간은 거의 같다. 오히려 두 개의 쓰레드로 작업한 시간이 싱글쓰레드로 작업한 시간보다 더 걸리게 되는데 그 이유는 쓰레드간의 작업전환(context switching)에 시간이 걸리기 때문이다.

 

그래서 싱글 코어에서 단순히 CPU만을 사용하는 계산하는 작업이라면 오히려 멀티쓰레드보다 싱글쓰레드로 프로그래밍 하는게 더 효율적이다.

 

하지만 두 쓰레드가 서로 다른 자원을 사용하는 작업의 경우에는 멀티쓰레드 프로세스가 더 효율적이다. 예를 들면 사용자로부터 데이터를 입력받는 작업, 네트워크로 파일을 주고받는 작업, 프린터로 파일을 출력하는 작업과같은 경우.

(자세한 내용은 자바의 정석 736page참고)

쓰레드의 우선순위

쓰레드는 우선순위라는 속성(멤버변수)을 가지고 있는데, 이 우선순위의 값데 따라 쓰레드가 얻는 실행시간이 달라진다.

void setPriority(int newPriority)	//쓰레드의 우선순위를 지정한 값으로 변경한다.
int getPriority()			//쓰레드의 우선순위를 반환한다.

public static final int MAX_PRIORITY = 10;	//최대우선순위
public static final int MIN_PRIORITY = 1;	//최소우선순위
public static final int NORM_PRIORITY = 5;	//보통우선순위

쓰레드의 우선순위는 쓰레드를 생성한 쓰레드로부터 상속받는다. main메서드를 수행하는 쓰레드는 우선순위가 5이므로 main메서드 내에서 생성하는 쓰레드의 우선순위는 자동적으로 5가 된다. 또 우선순위 셋팅은 실행하기전에만 변경할 수 있다.

쓰레드 그룹

쓰레드 그룹은 서로 관련된 쓰레드를 그룹으로 다루기 위한 것으로, 폴더를 생성해서 관련된 파일들을 함께 넣어서 관리하는 것처럼 쓰레드 그룹을 생성해서 쓰레드를 그룹으로 묶어서 관리할 수 있다. 또한 폴더 안에 폴더를 만드는 것처럼 쓰레드 그룹도 그게 가능하다. 쓰레드를 쓰레드 그룹에 포함시키려면 Thread의 생성자를 이용해야한다.

Thread(ThreadGroup group, String name)
Thread(ThreadGroup group, Runnable target)
Thread(ThreadGroup group, Runnable target, String name)
Thread(ThreadGroup group, Runnable target, String name, long stackSize)

모든 쓰레드는 반드시 쓰레드 그룹에 포함되어 있어야 하기 때문에, 위와 같이 쓰레드 그룹을 지정하는 생성자를 사용하지 않는 쓰레드는 기본적으로 자신을 생성한 쓰레드와 같은 쓰레드 그룹에 속하게 된다. 다음은 쓰레드 그룹관련 메서드.

ThreadGroup getThreadGroup()	//쓰레드 자신이 속한 쓰레드 그룹을 반환한다.
void uncaughtException(Thread t, Throwable e) 	//쓰레드 그룹의 쓰레드가 처리되지 않은 예외에 의해 실행이 
						//종료되었을 때, JVM에 의해 이 메서드가 자동적으로 호출된다.

데몬쓰레드

데몬 쓰레드는 다른 일반 쓰레드(데몬 쓰레드가 아닌)의 작업을 돕는 보조적인 역할을 수행하는 쓰레드이다.

일반 쓰레드가 종료되고나면 데몬 쓰레드는 강제적으로 자동 종료되는데, 그 이유는 데몬 쓰레드는 일반 쓰레드의 보조역할을 수행하므로 일반 쓰레드가 모두 종료되고 나면 데몬 쓰레드의 존재의 의미가 없기 때문이다. 이 점을 제외하고는 데몬 쓰레드와 일반 쓰레드는 다르지 않다.

 

데몬 쓰레드는 무한루프와 조건문을 이용해서 실행 후 대기하고 있다가 특정 조건이 만족되면 작업을 수행하고 다시 대기 하도록 작성한다. 일반 쓰레드의 작성방법과 동일하며 다만 쓰레드를 생성한 다음 실행하기전에 setDaemon(true)를 호출하기만 하면된다. 그리고 데몬 쓰레드가 생성한 쓰레드는 자동적으로 데몬 쓰레드가 된다.

boolean isDaemon()		//데몬쓰레드인지 확인한다.
void setDaemon(boolean on) 	//쓰레드를 데몬 쓰레드로 또는 사용자 쓰레드로 변경한다. true면 데몬 쓰레드

setDaemon은 반드시 start()를 호출하기전에 실행되어야한다.

 

 

 

 

 

 

반응형