본문 바로가기

프로그래밍/JAVA

[JAVA] 쓰레드(Thread) 1편

JAVA의 정석(2nd Edition) (남궁 성 著) 참조해 내용을 작성했으며

개인적인 공부 내용을 적은 것이므로 오류가 있을 수 있습니다.


0. 개요

<여러 프로세스들이 실행중이다>


운영체제(O/S) : 컴퓨터의 하드웨어를 사용하게 해주는 프로그램

프로세스(process) : 현재 실행되고 있는 프로그램

쓰레드(thread) : 프로세스 안에서 여러 작업들을 수행하기 위한 작업 단위 (노동자라고 생각하자)


윈도우에서 롤을 하면서 멜론으로 노래를 들을 수 있다. 롤도 프로세스, 멜론도 역시 프로세스다.

이를 멀티태스킹(multi-tasking, 다중작업)이라고 한다.


멀티태스킹과 마찬가지로 멀티쓰레딩 역시 하나의 프로세스 내에서 여러 쓰레드가 동시에 작업을 한다.

좀 더 미시적으로 들어간거라고 생각하면 되겠다.


위 내용을 숙지하고 본론에 들어가도록 하자.


1. 프로세스와 쓰레드


프로세스(process) : 실행 중인 프로그램. 프로그램을 실행하면 실행에 필요한 메모리를 할당받아 프로세스가 된다.


프로세스 = 프로그램 수행에 필요한 데이터 + 메모리 등의 자원 + 쓰레드(실제로 작업을 수행하는 부분)


싱글 쓰레드 프로세스 = 자원 + Thread

멀티 쓰레드 프로세스 = 자원 + Thread + Thread + ....


멀티 쓰레딩의 장점

  1. CPU의 사용률을 향상시킨다.

  2. 자원을 보다 효율적으로 사용할 수 있다.

  3. 사용자에 대한 응답성이 향상된다.

  4. 작업이 분리되어 코드가 간결해진다. 



2. 쓰레드의 구현과 실행


쓰레드를 만드는 두 가지 방법이 있다. 

1. Thread 클래스를 상속하는 법

2. Runnable 인터페이스를 구현하는 법

 

   1번의 방법은 다른 클래스를 상속받을 수 없기 때문에(단일 상속의 원칙)

   2번의 방법이 바람직하다. 재사용성이 높고, 코드의 일관성을 유지할 수 있다.


1.  Thread 클래스를 상속

<클래스 생성시 Thread 클래스를 상속받자>

<이후 run() 메서드를 오버라이딩 한다.>


public class MyThread1 extends Thread {
String str;

public MyThread1(String str){
this.str = str;
}

public void run(){ // run 메서드는 수행 흐름이 하나 더 생겼을 때의 메서드이다.
// 또 다른 메인메서드라고 생각하면 된다.
for(int i = 0; i < 10; i ++){
System.out.print(str);
try {
//컴퓨터가 너무 빠르기 때문에 수행결과를 잘 확인 할 수 없어서              // Thread.sleep() 메서드를 이용해서 조금씩 쉬었다가 출력할 수 있게한다.
Thread.sleep((int)(Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main (String[] args){
System.out.println("hello");
}
}

<쓰레드를 상속받는 클래스>

public class ThreadExam {

public static void main(String[] args){
MyThread1 t1 = new MyThread1("*");
MyThread1 t2 = new MyThread1("-");

t1.start(); // Thread 동작시 run()이 아닌 start()를 호출한다.
t2.start(); // start 메서드는 쓰레드가 실행준비를 하게 한다.
             // main 흐름 1개와, 쓰레드 흐름 2개(t1, t2)가 수행된다.

System.out.println("----*main end*----"); // main 흐름


}
}

<쓰레드를 실험하기 위한 클래스>


두 개의 클래스를 작성하고 실행시키면 위와 같은 결과를 얻을 수 있다.

sleep을 랜덤하게 주었기 때문에 '*'와 '-'가 나타나는 시간간격이 서로 다르다는 걸 알 수 있다.

뭔가 이를 통해서 로딩화면을 만들어 볼 수도 있지 않을까!?


2. Runnable 인터페이스 구현


public class MyThread2 implements Runnable {

String str;

public MyThread2(String str){
this.str = str;
}

public void run(){ // Runnable 인터페이스는 Run 메서드를 갖고 있다.

for(int i = 0; i < 10; i++){
System.out.println(str);

try {
Thread.sleep((int)(Math.random()*100));
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}

<위 쓰레드를 상속받는 예제를 Runnalbe 인터페이스로 구현한 것이다>


public class ThreadExam2 {

public static void main(String[] args) {
// TODO Auto-generated method stub
MyThread2 t1 = new MyThread2("*");
MyThread2 t2 = new MyThread2("-");

Thread thread1 = new Thread(t1);
Thread thread2 = new Thread(t2);
// MyThread는 Runnable 인터페이스의 구현. start() 메서드가 없고, run() 메서드가 존재
// 대신 쓰레드 객체를 만들어줘야 한다.
// Thread의 생성자를 보면 Runnable을 받을 수 있다.

thread1.start();
thread2.start();

System.out.println("main end!!!");
}
}


Rnnable 인터페이스를 구현을 제공하는 이유는 자바는 단일상속만 지원하기 때문이다.

만약 클래스가 다른 클래스를 상속받는다면, Thread 클래스를 상속받을 수 없기 때문이다.

이럴 경우에 대비해 인터페이스를 제공하는 것이다.


 쓰레드를 상속받는 법과 Runnable 인터페이스를 구현한 경우의 인스턴스 생성 방법이 다르다.

 아래 예제를 통해서 보도록 하자.

1. Thread 클래스를 상속(extends) 하면, Thread 클래스의 메서드를 직접 호출할 수 있다.


2. Runnable 인터페이스 구현의 경우 → Runnable 인터페이스를 구현한 클래스(ThreadEx2)의 인스턴스 생성 후, 이 인스턴스를 가지고 Thread클래스의 인스턴스를 생성할 때 생성자의 매개변수(r)을 제공해야 한다.

   ※ 쓰레드 생성 후에는 start()를 호출해야 작업을 시작할 수 있다.  


3. 둘의 차이점을 잘 모르겠다면 Runnable 인터페이스는 매개변수를 던져줘야 한다는 것만 기억하자.

4. 상속과 구현의 차이를 잘 보자. 상속의 경우 쓰레드 클래스의 메서드를 직접 호출할 수 있기 때문에  getName()을 호출하면 되지만, 구현의 경우 쓰레드 클래스의 static메서드인 currentThread()를 호출하여 쓰레드에 대한 참조를 얻어와야지만 getName() 호출이 길어진다.


5. ThreadEx1 과 ThreadEx2가 무작위하게 출력된다. 아직 synchronized하지 않아서 그렇다.  

   자세히 보면 '상속'과 '구현'에서 setName 메서드를 사용하는 방법이 서로 다르다는 것을 알 수 있다.


3. start() 와 run()

                                   

<JVM의 메모리구조>

쓰레드를 실행할 때 run()을 오버라이딩하지만, run() 아니라 start()를 호출한다.


start() : 새로운 호출스택을 생성한 뒤 run()을 첫 번째로 저장

run() : 생성된 쓰레드를 실행시키는 것이 아니라 단순히 클래스에 속한 메서드 하나를 호출함


<start()메서드를 사용하면 독립적인 작업을 수행하기 위한 그릇(호출택)을 새로 만든다고 보면 된다>

<호출스택이 2개가 되었기 때문에 스케줄러가 정한 순서에 의해 번갈아 가면서 실행된다>


4. 싱글쓰레드와 멀티쓰레드

그림만 봐도 그 차이점을 대충 알 수 있을 것이다. 당연히도 싱글 쓰레드의 작업시간이 더 빠르다.

멀티 쓰레드의 경우 작업전환(context switching) 시간이 더 걸리기 때문이다.


공장도 마찬가지다. 같은 생산 라인에서 A제품을 생산하다가 B제품으로 바로 바꿀 수 없다.

생산운영관리(OM)에서는 이를 셋업시간(set-up time)이라고 부른다. 0이라면 정말 좋겠지만, 어려운 일이다.


<싱글 쓰레드의 작업시간을 살펴보자. 9, 16밀리세컨드가 나온다.>


public class MyThread {

static long startTime = 0;

public static void main(String[] args){

multiThread multi = new multiThread();
multi.start();
startTime = System.currentTimeMillis();

for(int i=0; i<1000; i++){
System.out.print("-");
}
System.out.print("소요시간A: " + (System.currentTimeMillis()-MyThread.startTime) + "밀리세컨드");
}
}

class multiThread extends Thread {
public void run(){
for(int i=0; i<1000; i++){
System.out.print("|");
}

System.out.println("소요시간B: " + (System.currentTimeMillis()-MyThread.startTime) + "밀리세컨드");
}
}

<싱글은 합쳐서 25밀리세컨드였는데, 지금은 41 밀리세컨드의 작업시간이 걸린다>

<작업도 번갈아서 실행이 된다는 걸 알 수 있다>


<실행할 때마다 결과가 다른 이유, 바로 O/S의 프로세스 스케줄러에 영향을 받기 때문이다>

→ 자바의 쓰레드는 OS에 의해 종속적인 부분이 존재한다.



결론 : 만약 단순한 계산작업만 한다면 싱글쓰레드가 효율적이다

 하지만 CPU 이외의 자원을 사용하는 작업 ( EX : 사용자로부터 입력을 받거나, 프린트 출력)에는 

 멀티쓰레드가 적합하다.


<싱글스레드로 작업하면 ① 사용자의 입력을 마친 후 → ② 숫자 카운트가 진행된다.>


<멀티쓰레드로 작업하면, 숫자입력을 하기도 전에 카운팅이 시작된다>