[Java] 스레드 개요, 멀티태스킹과 멀티스레딩, 스레드 생성과 실행, 중요 클래스를 활용한 기본적인 실습

프로세스(process)와 스레드(thread)
프로세스란 간단히 말해서 '실행 중인 프로그램'이다. 프로그램을 실행하면 OS로부터 실행에 필요한 자원(메모리)을 할당받아 프로세스가 된다. 
프로세스는 프로그램을 수행하는 데 필요한 데이터와 메모리 등의 자원 드리고 스레드로 구성되어 있으며 프로세스의 자원을 이용해서 실제로 작업을 수행하는 것이 바로 쓰레드이다.

 

즉 프로세스는 CPU, 메모리 등의 자원과 스레드로 구성되어있고, 프로세스에 할당된 자원을 가지고 실제로 작업을 수행하는건 스레드이다. 그리고 프로세스에 자원을 할당해주는건 운영체제이다.

 

 그래서 모든 프로세스에는 최소한 하나 이상의 스레드가 존재하며, 둘 이상의 스레드를 가진 프로세스를 '멀티스레드 프로세스(multi-threaded process)라고 한다.

 

 

 

하나의 프로세스가 가질 수 있는 쓰레드의 개수는 제한되어 있지 않으나 스레드가 작업을 수행하는데 개별적인 메모리 공간(호출스택)을 필요로 하기 때문에 프로세스의 메모리 한계에 따라 생성할 수 있는 스레드의 수가 결정된다. 하지만 메모리 걱정은 안해도된다.

 

 

멀티태스킹(multi-tasking)과 멀티스레딩(multi-threading)

현재 우리가 사용하고 있는 윈도우나 유닉스를 포함한 대부분의  OS는 멀틴태스킹을 지원하기 때문에 여러 개의 프로세스가 동시에 실행될 수 있다.

 이와 마찬가지로 멀티스레딩은 하나의 프로세스 내에서 여러 쓰레드가 동시에 작업을 수행하는 것이다. CPU의 코어가 한 번에 단 하나의 작업만 수행할 수 있으므로, 실제로 동시에 처리되는 작업의 개수는 코어(core)의 개수와 일치한다. 그러나 처리해야하는 스레드의 수는 언제나 코어의 개수보다 훨씬 많기 때문에 각 코어가 아주 짧은 시간 동안 여러 작업을 번갈아 가며 수행함으로써 여러 작업들이 모두 동시에 수행되는 것처럼 보이게 한다. 

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

 
 
멀티스레딩의 장단점
메신저로 채팅하면서 파일을 다운로드 받거나 음성대화를 나눌 수 있는 것이 가능한 이유가 바로 멀티스레드로 작성되어 있기 때문이다. 만일 싱글스레드로 작성되어 있다면 파일을 다운로드 받는 동안에는 다른 일(채팅)을 전혀 할 수 없을 것이다.
 여러 사용자에게 서비스를 해주는 서버 프로그램의 경우 멀티스레드로 작성하는 것은 필수적이어서 하나의 서버 프로세스가 여러 개의 스레드를 생성해서 스레드와 사용자의 요청이 일대일로 처리되도록 프로그래밍 해야 한다. 
 만일 싱글스레드로 서버 프로그램을 작성한다면 사용자의 요청 마다 새로운 프로세스를 생성해야 하는데 프로세스를 생성하는 것은 스레드를 생성하는 것에 비해 더 많은 시간과 메모리 공간이 필요하기 때문에 많은 수의 사용자 요청을 서비스하기 어렵다.
 
그러나 멀티스레딩에 장점만 있는 것은 아니어서 멀티스레드 프로세스는 여러 쓰레드가 같은 프로세스 내에서 자원을 공유하면서 작업을 하기 때문에 발생할 수 있는 동기화(synchronization), 교착상태(deadlock)와 같은 문제들을 고려해서 신중히 프로그래밍해야 한다. 
 
 
메인 스레드
모든 자바 애플리케이션은 메인 스레드(main thread)가 main() 메소드를 실행하면서 시작된다. 메인 스레드는 main() 메소드의 첫 코드부터 아래적으로 순차적으로 실행하고, main() 메소드의 마지막 코드를 실행하거나 return문을 만나면 실행이 종료된다.
 
메인 스레드는 필요에 따라 작업 스레드들을 만들어서 병렬로 코드를 실행할 수 있다. 즉 멀티 스레드를 생성해서 멀티 태스킹을 수행한다. 싱글 스레드 애플리케이션에서는 메인 스레드가 종료하면 프로세스도 종료된다. 하지만 멀티 스레드 애플리케이션에서는 실행 중인 스레드가 하나라도 있다면, 프로세스는 종료되지 않는다.  

 


 

스레드의 구현과 실행

스레드를 구현하는 방법은 Thread클래스를 상속받는 방법과 Runnable인터페이스를 구현하는 방법 모두 두 가지가 있다. 어느 쪽을 선택해도 별 차이는 없지만 Thread클래스를 상속받으면 다른 클래스를 상속받을 수 없기 때문에, Runnable인터페이스를 구현하는 방법이 일반적이다.

 그리고 Runnable인터페이스를 구현하는 방법은 재사용성(reusability)이 높고 코드의 일관성(consistency)을 유지할 수 있기 때문에 보다 객체지향적인 방법이라 할 수 있겠다.

 

자바에서는 작업 스레드도 객체로 생성되기 떄문에 클래스가 필요하다. java.lang.Thread 클래스를 직접 객체화해서 생성해도 되지만, Thread를 상속해서 하위 클래스를 만들어 생성할 수도 있다.

 

Thread 클래스로부터 직접 생성

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

 

 Thread thread = new Thread(Runnable target);

 

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

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

 

스레드 생성 및 실행하는 방법

1. Thread 클래스를 상속하는 방법
- Thread 클래스를 상속 받아서 클래스를 작성한다.
- run() 메소드를 재정의한다.
- Thread 객체를 생성한다. 
1
2
3
public class T extends Thread{
    //Thread 클래스를 상속받은 후에 run() 메소드를 재정의함
    //run() 메소드 안에 작업을 기술함.
 
}
cs
1
2
3
4
5
6
7
8
9
public class Test {
    public static void main(String[] args) {
        Thread t = new T();
        t.start();
    }
}
 
cs
 
 
2. Runnable 인터페이스를 구현하는 방법
Runnable인터페이스는 오로지 run()만 정의되어 있는 간단한 인터페이스이다. Runnable인터페이스를 구현하기 위해서 해야 할 일은 추상메서드인 run()의 몸통 {}을 만들어 주는것 뿐이다.
 
- Runnable 인터페이스를 구현한 클래스를 작성한다.
- run() 메소드를 작성한다.
- Thread 객체를 생성하고 이 클래스 객체를 인수로 전달한다.
- start()를 호출하여 스레드를 시작한다.
 
1
2
3
4
5
6
7
8
9
10
public class T2 implements Runnable{
 
   //Runnable 인터페이스를 구현하고 run() 메소드를 작성함
   //이 클래스의 객체를 Thread 클래스의 생성자를 호출할 때 전달함
    @Override
    public void run() {
 
    }
}
 
cs

 

 
1
2
3
4
5
6
7
8
9
public class Test2 {
    public static void main(String[] args) {
        
        Thread t = new Thread(new T());
        t.start();
    }
}
 
cs
 

 


 

 
스레드 상태
 

 

 


 

 
Thread 클래스의 중요 메소드
 

 메소드 설 명 
 static void sleep(long mills)  Thread를 mills 시간 동안 재움 (mills : 밀리초)
 Thread가 sleep된 동안 인터럽트가 된다면
 InterruptedExeption이 발생함; 이 예외를 처리해야 함
 void run()  Thread 시작 시 JVM 의해 호출됨
 이 메소드 종료 시 Thread도 종료됨
 Thread가 수행할 작업을 이 메소드 안에 오버라이딩하여 작성
 void start()  JVM에게 Thread 실행을 요청 
 void join()  Thread가 종료할 때까지 기다림 
 static void yield()  Thread가 다른 Thread에게 실행을 양보함
 이때 Thread 스케쥴링이 실행되고 다른 Thread가 선택/실행됨 
 void interrupt()  Thread를 강제 중단 

 


 

 

스레드 실습

 

public class BeepPrintExample1 {
    public static void main(String[] args) {
        Toolkit toolkit = Toolkit.getDefaultToolkit();
        
        for(int i=0; i<5; i++) {
            toolkit.beep();
            
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        for (int i = 0; i < 5; i++) {
            System.out.println("띵");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
cs

 

위 예제는 비프음 발생시키고자 하는 예제이다 메인 스레드만 사용하므로 0.5초마다 비프음이 들리고 나서 0.5초마다 '띵'이 출력된다. 이제 스레드를 사용하여 두가지 작업이 동시에 같이 되도록 해보자

 

Runnable 인터페이스를 활용한 Thread 실습

public class BeepTask implements Runnable{
 
    @Override
    public void run() {
        Toolkit toolkit = Toolkit.getDefaultToolkit();
        for (int i = 0; i < 5; i++) {
            toolkit.beep();
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
cs

 

public class BeepPrintExample2 {
    public static void main(String[] args) {
        Runnable beepTask = new BeepTask();
        Thread thread = new Thread(beepTask);
        thread.start();
        
        for (int i = 0; i < 5; i++) {
            System.out.println("띵");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
cs

 

BeepTask 클래스에 Runnable 인터페이스를 상속받아 run() 메소드에 0.5초마다 비프음을 내는 코드를 작성하였고 BeepPrintExample2에서 thread.start()를 통해 비프음을 내는 동시에 '띵'이 출력되었다. 즉 동시에 두작업이 실행되는 것이다.

 

이번에는 Thread 하위 클래스로부터 생성해보자

 

public class BeepThread extends Thread{
    @Override
    public void run() {
        Toolkit toolkit = Toolkit.getDefaultToolkit();
        for (int i = 0; i < 5; i++) {
            toolkit.beep();
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
cs
public class BeepPrintExample3 {
    public static void main(String[] args) {
        Thread thread = new BeepThread();
        thread.start();
        
        for (int i = 0; i < 5; i++) {
            System.out.println("띵");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
cs
 
BeepThread 클래스에 Thread 클래스를 상속받아 run() 메소드를 정의하였고, BeepPrintExample3에서 thread.start()로 실행하였다. 실행 결과는 위와 같다. 사용은 implements Runable을 더 많이 사용한다.
 
 
Implement를 활용한 Thread 실습2
class MyRunnable implements Runnable {
    String myName;
    public MyRunnable(String name) {
        myName = name;
    }
    @Override
    public void run() {
        for (int i = 10; i >= 0; i--
            System.out.print(myName + i + " ");
    }
}
 
public class Test {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable("A"));
        Thread t2 = new Thread(new MyRunnable("B"));
        t1.start();
        t2.start();
    }
}
 
cs

 

 

최수경 교수님, 네트워크 프로그래밍, 삼육대학교(2018)

 

댓글

Designed by JB FACTORY