[자바의 정석] Ch.13 쓰레드
1. 프로세스와 쓰레드
프로그램을 실행하면 운영체제로부터 필요한 메모리를 할당받아 프로세스가 된다. 프로세스란 실행중인 프로그램을 말한다. 프로세스는 자원과 쓰레드로 구성되어있고 자원을 이용해 작업을 수행하는 것은 쓰레드이다. 모든 프로세스는 하나 이상의 쓰레드가 존재하고 둘 이상의 쓰레드를 가진 프로세스를 멀티쓰레드 프로세스라 한다.
멀티태스킹과 멀티쓰레딩
현재 유투브로 음악을 재생한 상태로 블로그를 작성하고 있다. 이렇게 한 번에 여러가지 작업을 하는 것을 멀티태스킹이라 한다.
멀티쓰레딩은 하나의 프로세스 내에서 여러 개의 쓰레드가 동시에 작업하는 것을 말한다. CPU의 코어는 한 번에 하나의 작업을 수행할 수 있기 때문에 동시에 처리할 수 있는 작업의 개수는 코어의 개수와 일치한다. 하지만 쓰레드는 코어보다 훨씬 많기때문에 짧은 시간동안 여ㄹ 작업을 번갈아 수행하면서 여러 작업이 동시에 수행하는 것처럼 보이게 한다.
멀티쓰레딩의 장단점
멀티쓰레딩의 장점
- CPU의 사용률을 향상시킨다
- 자원을 보다 효율적으로 사용할 수 있다
- 사용자에 대한 응답성이 향상된다.
- 작업이 분리되어 코드가 간결해진다
멀티쓰레딩의 단점
- 교착상태(deadlock)이 발생할 수 있다.
- 동기화에 어려움이 있을 수 있다.
- 예상치 못한 결과가 발생할 수 있다.
2. 쓰레드의 구현과 실행
쓰레드를 구현하는 방법은 Thread클래스를 상속받거나 Runnable인터페이스를 구현하는 방법이 있다. Thread클래스를 상속받는 경우 다른 클래스를 상속받을 수 없으므로 Runnable인터페이스를 구현하는 것이 보다 객체지향적인 방법이라 할 수 있다. Runnbale인터페이스는 run()메소드만 정의되어 있어 이를 구현해주기만 하면 된다.
Thread클래스를 상속받을 때와 Runnable인터페이스를 구현하는 경우 인스턴스 생성 방법이 다르다.
//1. Runnable 인터페이스
//Runnable을 구현한 Threadimplement클래스의 인스턴스 생성
Runnable r = new Threadimplement();
//Thread클래스의 생성자의 매개변수로 인스턴스를 제공
Thread t1 = new Thread(r);
//위 두 줄을 아래 한 줄로 작성 가능
Thread t1 = new Thread(new Threadimplement());
//2. Thread 클래스
Threadimplement2 t2 = new Threadimplement2();
Thread클래스를 상속받으면 자식 클래스에서 Thread클래스의 메소드를 호출할 수 있지만 Runnable인터페이스를 구현하면 Thread 클래스의 static메소드인 currentThread()를 호출하여 쓰레드에 대한 참조를 얻어 와야지만 호출할 수 있다.
class ThreadChild1 extends Therad{
public void run(){
System.out.println(getName());
}
}
class ThreadChild2 implements Runnable{
public void run(){
System.out.println(Thread.currentThread().getName());
}
}
쓰레드의 이름을 지정하지 않으면 'Thread-번호'의 형식으로 이름이 정해진다. Thread의 이름을 지정하거나 변경하기 위해서는 아래와 같은 방법을 이용한다.
Thread(Runnable target, String name)
Thread(String name)
void setName(String name)
쓰레드의 실행 - start()
쓰레드 생성 후 start()를 호출해야 쓰레드가 실행된다. start()를 호출한 다음 실행 대기 상태중인 쓰레드가 없다면 바로 실행상태가 될 수 있지만 대기 상태인 쓰레드가 있다면 기다렸다가 차례가 되었을 때 실행할 수 있다. 또한 종료된 쓰레드는 다시 실행될 수 없다. 만일 다시 쓰레드를 수행해야 한다면 새로운 쓰레드를 생성한 다음 다시 start()를 호출해야 한다. 같은 쓰레드 인스턴스를 두번 이상 호출한다면 IllegalThreadStatedException이 발생한다.
3. start()와 run()
main메소드에서 run()을 호출하는 것은 생성된 쓰레드를 실행시키는 것이 아니라 클래스에 선언된 메서드를 호출하는 것이다.
start()는 새로운 쓰레드가 작업하는데 필요한 호출스택(call stack)을 생성한 다음에 run()을 호출하여 생성된 호출스택에 run()이 첫 번째로 올라가게 한다. start()는 자신의 임무를 마쳤으므로 소멸되고 두 개의 호출스택이 스케쥴러가 정해주는 순서에 따라 번갈아가며 실행된다.
main쓰레드
main메서드에서 작업을 수행하는 것도 쓰레드이다. 프로그램을 실행하면 기본적으로 하나의 쓰레드가 생성되고, 그 쓰레드가 main메서드를 호출하여 작업이 수행되도록 한다. main메서드가 실행을 마치면 아래와 같이 run()메소드가 호출된 호출스택에서의 작업이 종료되지 않았으므로 프로그램이 종료되지 않는다. 실행중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료된다.
public class ThreadEx2 {
public static void main(String[] args) throws Exception{
ThreadEx2_1 t1 = new ThreadEx2_1();
t1.start();
}
}
class ThreadEx2_1 extends Thread{
public void run(){
throwException();
}
public void throwException(){
try{
throw new Exception();
}catch(Exception e){
e.printStackTrace();
}
}
}
start()메소드를 이용하여 쓰레드를 호출하고 쓰레드에서 예외를 발생시켜 당시 호출 스택을 출력하는 예제이다. start()메소드의 경우 호출시 새로운 호출 스택을 만들어 그 스택에서 run()메소드를 호출한다. 그 다음 throwException()이 호출되었다. main메소드는 이미 종료되어 호출스택에 없다.
public class ThreadEx2 {
public static void main(String[] args) throws Exception{
ThreadEx2_1 t1 = new ThreadEx2_1();
t1.run();
}
}
class ThreadEx2_1 extends Thread{
public void run(){
throwException();
}
public void throwException(){
try{
throw new Exception();
}catch(Exception e){
e.printStackTrace();
}
}
}
run()메소드를 호출한 경우 쓰레드가 새로 생성되지 않았다. main쓰레드의 호출스택에 main메서드가 포함되어 있다. 그 다음 run()과 예외가 쌓여있다.
4. 싱글쓰레드와 멀티쓰레드
하나의 쓰레드로 두 개의 작업을 수행한 시간과 두 개의 쓰레드로 두 개의 작업을 수행한 시간은 거의 같다. 그래프를 잘못 그려서 두 개의 쓰레드로 두 개의 작업을 수행한 경우의 시간이 더 짧게 나왔지만 실제로는 두 개의 쓰레드로 작업한 시간이 더 긴데 그 이유는 쓰레드간 작업전환에 시간이 걸리기 때문이다.
public class ThreadEx2 {
public static void main(String[] args) throws Exception{
long startTime = System.currentTimeMillis();
for(int i=0; i<300; i++){
System.out.printf("%s", new String("-"));
}
System.out.println("소요시간1: " + (System.currentTimeMillis() - startTime));
for(int i=0; i<300; i++){
System.out.printf("%s", new String("|"));
}
System.out.println("소요시간2: " + (System.currentTimeMillis() - startTime));
}
}
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------소요시간1: 20
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||소요시간2: 55
public class ThreadEx2 {
static long startTime = 0;
public static void main(String[] args) throws Exception{
ThreadEx2_1 th1 = new ThreadEx2_1();
th1.start();
startTime = System.currentTimeMillis();
for(int i=0; i<300; i++){
System.out.printf("%s", new String("-"));
}
System.out.println("소요시간1: " + (System.currentTimeMillis() - ThreadEx2.startTime));
}
}
class ThreadEx2_1 extends Thread{
public void run(){
for(int i=0; i<300; i++){
System.out.printf("%s", new String("|"));
}
System.out.println("소요시간2: " + (System.currentTimeMillis() - ThreadEx2.startTime));
}
}
--|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||--------------------||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||---|||||||||||||||||------------------------------------------------------------------------------------------------------------------------소요시간2: 20
-----------------------------------------------------------------------------------------------------------------------------------------------------------소요시간1: 37
실행해볼 때마다 소요시간에 차이가 있겠지만 두 개의 쓰레드로 작업할 때 시간이 더 오래 걸린다. 그 이유는 쓰레드 간의 작업 전환 시간과 다른 쓰레드가 작업하는 동안 남은 쓰레드가 대기하는 시간때문이다. 하나의 작업만을 하는 경우라면 싱글쓰레드가 좋지만 서로 다른 자원으로 작업하는 경우 멀티쓰레드가 더 효율적이다.
import javax.swing.*;
public class ThreadEx2 {
public static void main(String[] args) throws Exception{
long startTime = System.currentTimeMillis();
String input = JOptionPane.showInputDialog("아무 값이나 입력하세요.");
System.out.println("입력하신 값은 " + input + "입니다.");
for(int i=10; i>0; i--){
System.out.println(i);
try{
Thread.sleep(1000);
}catch (Exception e) { }
}
System.out.println(System.currentTimeMillis() - startTime);
}
}
//결과
입력하신 값은 abcd입니다.
10
9
8
7
6
5
4
3
2
1
14688
import javax.swing.*;
public class ThreadEx2 {
public static void main(String[] args) throws Exception{
ThreadEx2_1 th1 = new ThreadEx2_1();
th1.start();
long startTime = System.currentTimeMillis();
String input = JOptionPane.showInputDialog("아무 값이나 입력하세요.");
System.out.println("입력하신 값은 " + input + "입니다.");
System.out.println(System.currentTimeMillis() - startTime);
}
}
class ThreadEx2_1 extends Thread{
@Override
public void run() {
for(int i=10; i>0; i--){
System.out.println(i);
try {
Thread.sleep(1000);
}catch (Exception e) { }
}
}
}
//결과
10
9
8
7
6
입력하신 값은 abcd입니다.
4057
5
4
3
2
1
메인쓰레드 내에서 처리할 땐 input을 입력받고 출력한 다음 10초를 세도록 만들었다. input값을 처리한 다음 10초를 세야하니 싱글쓰레드인 셈이다. 멀티쓰레드 예제에서는 input을 받고 출력하는 것은 메인쓰레드, 10초를 세는 것은 start()로 호출한 새로운 호출스택에서 일하도록 만들었다. 이 때는 실행하자마자 10초를 세고, 그 중간에 입력값을 받았다면 입력값이 나오도록 되어있다. 이 때도 시간을 비교해보려고 넣었는데 제대로 넣지 못한 듯하다. 또 멀티스레드에서는 메인 쓰레드가 아니라 새로운 쓰레드에 넣어야 할 것 같다.
5. 쓰레드의 우선순위
쓰레드는 우선순위라는 멤버변수를 갖고있다. 이 우선순위의 값에 따라 쓰레드가 얻는 실행시간이 달라진다. 우선순위에 따라 특정 쓰레드가 더 많은 작업시간을 갖도록 할 수 있다.
쓰레드가 가질 수 있는 우선순위의 범위는 1~10이며 숫자가 높을수록 우선순위가 높다. main메서드를 수행하는 쓰레드는 우선순위가 5이므로 main메서드 내에서 생성하는 쓰레드의 우선순위는 자동으로 5가 된다.
하지만 우선순위는 우선순위일뿐 우선순위에 차이를 두고 쓰레드를 실행하도 결과에는 차이가 없다. 높은 우선순위를 부여한다고 해서 쓰레드가 더 많은 실행시간과 실행기회를 갖게 될 거라 기대하지 않는게 좋다. 대신 작업에 우선순위를 두어 PriorityQueue에 저장해두고 우선순위가 높은 작업부터 처리하도록 하는 것이 나을 수 있다.