자바 - 쓰레드(Thread)

honeyricecake·2022년 5월 30일
0

학교 수업 - 자바

목록 보기
13/16

시작에 앞서 이는 https://wikidocs.net/230 의 게시글을 공부하고 정리한 글임을 밝힙니다

Thread

동작하고 있는 프로그램을 프로세스(Process)라고 한다.
보통 한 개의 프로세스는 한 가지의 일을 하지만, 쓰레드를 이용하면 한 프로세스 내에서 두 가지 또는 그 이상의 일을 동시에 할 수 있다.

<예제 1>

public class Sample extends Thread {
    public void run() {  // Thread 를 상속하면 run 메서드를 구현해야 한다.
        System.out.println("thread run.");
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        sample.start();  // start()로 쓰레드를 실행한다.
    }
}

Sample 클래스가 Thread 클래스를 상속했다. Thread 클래스의 run 메소드를 구현하면 위 예제와 같이 simple.start() 실행 시 sample 객체의 run메소드가 수행된다.

(쓰레드 클래스는 start 실행 시 run 메소드가 수행되도록 내부적으로 동작한다.)

위 예제를 실행하면 thread run이라는 문장이 출력될 것이다.

<예제 2>

public class Sample extends Thread {
    int seq;

    public Sample(int seq) {
        this.seq = seq;
    } // Sample 클래스의 생성자

    public void run() {  // start 메소드 실행 시 실행될 함수
        System.out.println(this.seq + " thread start.");  // 쓰레드 시작하면서 해당 내용 출력
        try {
            Thread.sleep(1000);  // 1초 대기한다.
        } catch (Exception e) {
        }
        System.out.println(this.seq + " thread end.");  // 쓰레드 종료 
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {  // 총 10개의 쓰레드를 생성하여 실행한다.
            Thread t = new Sample(i);
            t.start();
        }
        System.out.println("main end.");  // main 메소드 종료
    }
}

총 10개의 쓰레드를 실행시키는 예제이다.
어떤 쓰레드인지 확인하기 위해서 쓰레드마다 생성자에 순번을 부여했다. 그리고 쓰레드 메소드 수행 시 시작과 종료를 출력하게 했고 시작과 종료 사이에 1초의 간격이 생기도록 Thread.sleep(1000)을 작성했다. -> ms단위

[실행 결과]

0번 쓰레드부터 9번 쓰레드까지 순서대로 실행이 되지 않고 그 순서가 일정치 않은 것을 보면 쓰레드는 순서에 상관없이 동시에 실행된다는 사실을 알 수 있다.

/
Q. 그럼 쓰레드를 어떻게 생성하든 쓰레드이기만 하면 동시에 실행되는건가? 아니면 같은 클래스이면? 쓰레드를 동시에 실행하지 않는 경우도 필요하지 않을까?

A.
(1)

import java.util.*;

class Another extends Thread
{
	int seq;
	
	public Another(int seq) {
		this.seq = seq;
	}
	
	public void run()
	{
		System.out.println(this.seq + " Another thread start.");  // 쓰레드 시작하면서 해당 내용 출력
        try {
            Thread.sleep(1000);  // 1초 대기한다.
        } catch (Exception e) {
        }
        System.out.println(this.seq + " Another thread end.");  // 쓰레드 종료 
	}
}

public class Unit13 extends Thread {
    int seq;

    public Unit13(int seq) {
        this.seq = seq;
    } // Sample 클래스의 생성자

    public void run() {  // start 메소드 실행 시 실행될 함수
        System.out.println(this.seq + " thread start.");  // 쓰레드 시작하면서 해당 내용 출력
        try {
            Thread.sleep(1000);  // 0.5초 대기한다.
        } catch (Exception e) {
        }
        System.out.println(this.seq + " thread end.");  // 쓰레드 종료 
    }

    public static void main(String[] args) {
    	
    	ArrayList<Thread> threads = new ArrayList();
    	
        for (int i = 0; i < 10; i++) {  // 총 10개의 쓰레드를 생성하여 실행한다.
            Thread t = new Unit13(i);
            t.start();
            
            Thread a = new Another(i);
            a.start();
        }
        
        System.out.println("main end.");  // main 메소드 종료
    }
}

위의 실행 결과를 통해 두개의 클래스를 만들어도 동시에 실행됨을 알 수 있다.

단, main메소드 종료 후에도 실행되는 쓰레드들이 있음을 알 수 있는데 이는 좀 더 개념을 확실히 해야한다는 것을 느껴 다음 게시글에서 좀 더 자세히 알아보겠다./

더욱 재밌는 사실은 쓰레드가 종료되기 전에 main 메소드가 종료되었다는 사실이다. main 메소드 종료 시 "main end."라는 문자열이 출력되는데 위 결과를 보면 중간쯤에 출력되어 있다.

그리고 쓰레드가 종료되기 전에 main 메소드가 종료되었다는 사실을 알 수 있다.

Join

위 예제를 보면 쓰레드가 모두 수행되고 종료되기 전에 main 메소드가 먼저 종료되어 버렸다.
그렇다면 모든 쓰레드가 종료된 후에 main메소드를 종료시키고 싶은 경우에는 어떻게 해야할까?

<예제 3>

import java.util.ArrayList;

public class Sample extends Thread {
    int seq;
    public Sample(int seq) {
        this.seq = seq;
    }  // 생성자

    public void run() {
        System.out.println(this.seq+" thread start.");
        try {
            Thread.sleep(1000);
        }catch(Exception e) {
        }
        System.out.println(this.seq+" thread end.");
    }  // 여기까지는 위의 예제와 동일

    public static void main(String[] args) {
        ArrayList<Thread> threads = new ArrayList<>();
        // Thread 자료형의 배열 threads
        for(int i=0; i<10; i++) {
            Thread t = new Sample(i);  // 업캐스팅(Thread는 Sample의 부모 클래스)
            t.start();
            threads.add(t);
        }

        for(int i=0; i<threads.size(); i++) {
            Thread t = threads.get(i);
            try {
                t.join(); // t 쓰레드가 종료할 때까지 기다린다.
            }catch(Exception e) {
            }
        }
        System.out.println("main end.");
    }
}

생성된 쓰레드를 담기 위해서 ArrayList 객체인 threads를 만든 후 쓰레드 생성시 생성된 객체를 threads에 저장했다.

그리고 main 메소드가 종료되기 전에 threads에 담긴 각각의 thread에 join메소드를 호출하여 쓰레드가 종료될 때까지 대기하도록 했다.

쓰레드의 join메소드는 쓰레드가 종료될 때까지 기다리게 하는 메소드이다.

Q. 종료될 때까지 기다린다는 것의 주체는?
A. 메인 함수!

쓰레드 프로그래밍 시 가장 많이 실수하는 부분이 바로 쓰레드가 종료되지 않았는데 쓰레드가 종료된 줄 알고 그 다음 로직을 수행하게 만드는 일이다. 쓰레드가 종료된 후 그 다음 로직을 수행해야 할 때 꼭 필요한 join 메소드를 꼭 기억하자.

  1. join 미사용 예제

public class Unit13 extends Thread {
    int seq;

    public Unit13(int seq) {
        this.seq = seq;
    } // Sample 클래스의 생성자

    public void run() {  // start 메소드 실행 시 실행될 함수
        System.out.println(this.seq + " thread start.");  // 쓰레드 시작하면서 해당 내용 출력
        try {
            Thread.sleep(1000);  // 1초 대기한다.
        } catch (Exception e) {
        }
        System.out.println(this.seq + " thread end.");  // 쓰레드 종료 
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {  // 총 10개의 쓰레드를 생성하여 실행한다.
            Thread t = new Unit13(i);
            t.start();
        }
        
        for (int i = 0; i < 10; i++) {
            System.out.println("print" + i);
        }
        
        System.out.println("main end.");  // main 메소드 종료
    }
}
  1. join 사용 예제
import java.util.*;

public class Unit13 extends Thread {
    int seq;

    public Unit13(int seq) {
        this.seq = seq;
    } // Sample 클래스의 생성자

    public void run() {  // start 메소드 실행 시 실행될 함수
        System.out.println(this.seq + " thread start.");  // 쓰레드 시작하면서 해당 내용 출력
        try {
            Thread.sleep(1000);  // 1초 대기한다.
        } catch (Exception e) {
        }
        System.out.println(this.seq + " thread end.");  // 쓰레드 종료 
    }

    public static void main(String[] args) {
    	
    	ArrayList<Thread> threads = new ArrayList();
    	
        for (int i = 0; i < 10; i++) {  // 총 10개의 쓰레드를 생성하여 실행한다.
            Thread t = new Unit13(i);
            threads.add(t);
            t.start();
        }
        
        for (int i = 0; i < 10; i++) {
            Thread t = threads.get(i);
            try {
				t.join();  // 잎서 실행했던 쓰레드들이 종료될 때까지 기다리기를 메인 메소드에 요청
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
        }
        
        for (int i = 0; i < 10; i++) {
            System.out.println("print" + i);
        }
        
        System.out.println("main end.");  // main 메소드 종료
    }
}

실행 결과

  1. join 사용 예제 (2)

일부만 기다리게 만들어보자.

import java.util.*;

public class Unit13 extends Thread {
    int seq;

    public Unit13(int seq) {
        this.seq = seq;
    } // Sample 클래스의 생성자

    public void run() {  // start 메소드 실행 시 실행될 함수
        System.out.println(this.seq + " thread start.");  // 쓰레드 시작하면서 해당 내용 출력
        try {
            Thread.sleep(2000);  // 0.5초 대기한다.
        } catch (Exception e) {
        }
        System.out.println(this.seq + " thread end.");  // 쓰레드 종료 
    }

    public static void main(String[] args) {
    	
    	ArrayList<Thread> threads = new ArrayList();
    	
        for (int i = 0; i < 10; i++) {  // 총 10개의 쓰레드를 생성하여 실행한다.
            Thread t = new Unit13(i);
            t.start();
            if(i < 5)
            {
            	try {
					t.join();
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
            }		
        }
        
        System.out.println("main end.");  // main 메소드 종료
    }
}

4 thread 까지는 이전 쓰레드가 종료될 때까지 기다렸다가 실행
이후 쓰레드들은 동시에 실행되었다가 종료됨을 볼 수 있다.

Runnable

보통 쓰레드 객체를 만들 때 위의 예처럼 Thread 를 상속하여 만들기도 하지만 보통은 Runnable 인터페이스를 구현하도록 하는 방법을 주로 사용한다. 왜냐하면 Thread 클래스를 상속하면 다른 클래스를 상속할 수 없기 때문이다.
(인터페이스는 다중 상속 가능)

위의 예제를 Runnable 인터페이스를 사용하는 방식으로 변경해보자.

import java.util.ArrayList;

public class Sample implements Runnable {
    int seq;
    public Sample(int seq) {
        this.seq = seq;
    }

    public void run() {
        System.out.println(this.seq+" thread start.");
        try {
            Thread.sleep(1000);  // Thread.sleep()이 가능한 이유 -> 얘도 쓰레드이므로
        }catch(Exception e) {
        }
        System.out.println(this.seq+" thread end.");
    }

    public static void main(String[] args) {
        ArrayList<Thread> threads = new ArrayList<>();  // ?? 왜 Thread 객체지? Runnable로 구현했는데? 
        for(int i=0; i<10; i++) {
            Thread t = new Thread(new Sample(i));  // new Thread를 이용하였기 때문
            // 근데 이런게 어떻게 가능하지?
            t.start();
            threads.add(t);
        }

        for(int i=0; i<threads.size(); i++) {
            Thread t = threads.get(i);
            try {
                t.join();
            }catch(Exception e) {
            }
        }
        System.out.println("main end.");
    }
}

-> new Thread(new Sample(i)) 가 가능한 이유를 보기 위해 쓰레드의 생성자를 보자.

쓰레드 생성자를 보면 Runnable 객체를 매개변수로 한 생성자가 있음을 알 수 있다.
이는 이전의 쓰레드를 상속한 클래스처럼 Runnable을 상속한 클래스를 사용할 수 있게 만들어준다.

기억하자.
Thread t = new Thread(new Sample(i));

0개의 댓글