본문으로 바로가기

자바 쓰레드(Thread)에 대하여

category Java 개발 이야기 2018. 11. 24. 20:05
336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.



먼저 쓰레드 프로그래밍에 대해서 먼저 알아야할 용어가 있다.


TASK 


일 혹은 작업이라고 하며, 프로세스와 스레드까지 의미한다. 테스크는 우리가 쉽게 저바할 수 있는 익스플로러, 워드프로세서와 같은 큰프로그램부터, 계산기 덧겜과 같이 작은 연산 작업까지 포함하는 개념이다.


프로세스(Process)


OS로부터 자원을 할당 받아 동작하는 독립된 프로그램을 의미한다. 즉 익스플로러를 종료한다고 해서 워드 프로세서가 종료되지 않는다. 특정 프로그램을 실행하면 OS에서는 리소스라고 부르는 자원을 할당 받는다. 이 때 리소스는 CPU나 메모리 등을 의미한다. 이를 자바와 연계하면 자바 명령어를 실행하면 JVM은 OS 로부터 리소스를 할당 받아 실해된다. 실행된 JVM이 하나의 프로세스이다. 한 가지 주의할 점은 같은 프로그램을 여러개 실행한다고 해도, 할당받은 자원은 서로 공유하지 않는다.


스레드(Thread)


하나의 프로세스에서 실행하는 작업의 단위이다. 쉽게 얘기하자면 비행기 게임에서 비행기 외에 여러개의 비행기들이 화면에 나타나고, 각각 독립적으로 비행한다. 여기서 볼수 있는 비행기 모두느를 스레드로 이해하면 된다. 즉 비행기 프로그램이 하나의 프로세스이며, 프로그램은 동시에 여러 작업을 수행하여 화면에 보여주는데 이것이 스레드이다. 프로세스와 달리 스레드는 자원을 공유하며 실행할 수 있는 특징이 있다.

기본적으로 하나의 프로세스를 실행하면 하나의 스레득 실행된다. 이 스레드를 메인 스레드라고 하며, 프로세스의 시작이 된다. 프로세스 내부에 하나의 스레드가 동작하는 것을 싱글 스레드 프로세스라고 한다. 반대로 프로세스 내부에 여러 개의 스레드가 동작하는 것을 멀티 스레드 프로세스라고 한다. 


멀티 프로세싱 그리고 멀티 스레드


멀티 프로세싱은 여러 개의 CPU 프로세스가 서로 협력하여 작업을 병행 처리 하는 기법이다. 각 CPU 프로세스들은 독립적이므로 하나 혹은 여러 대의 컴퓨터에서 분산 처리될 수 있는 장점이 있다. 반면에 각 프로세스는 자원을 독립적으로 할당받아야 하므로, 자원의 소모가 많고 서로 간의 통신과 작업을 나누고 합치는 과정이 복잡할 수밖에 없다.


멀티 스레딩은 하나의 프로그램에서 여러 개의 스레드가 실행되는 것을 의미한다. 멀티 스레드는 자원을 공유할 수 있으므로 가볍다. 또한 각각의 스레드를 제어할 수 있기 때문에 성능을 향상시킬 수 있다. 


동작하고 있는 프로그램을 프로세스라고 하며, 보통 한 개의 프로세스는 한 가지 일을 하지만, 쓰레드를 이용하여 프로세스 내에서 동시에 여러가지 일을 할 수 있게 된다.


동시에 여러가지 일을 할 수 있다는게 무슨 의미냐면, 기본적으로 아래 포문과 같이 Main에서 실행을 하면 첫 번째 포문이 끝난 후 2번째 포문이 시작된다. 하지만 Thread를 사용하면 동시에 실행을 할 수 있다.


1
2
3
4
5
6
7
8
9
10
11
public class ThreadTest {
    public static void main(String[] args) {
        for(int i=0; i < 10; i++) {
            System.out.println("첫 for문 : " + i+1);
        }
        
        for(int i=0; i < 10; i++) {
            System.out.println("두번째 for문 : " + i+1);
        }
    }
}



Thread를 통해 for문을 동시에 실행할 수 있다. 아래 코드에 대한 설명은 Thread 클래스를 상속 받아 Thread 클래스에서 제공하는 run() 메서드를 재정의 한다. Thread를 상속받은 클래스를 객체화 하여 start() 메서드를 실행하면 run() 메서드에서 정의한 내용들이 실행된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ThreadTest {
    public static void main(String[] args) {
        ThreadClass thread1 = new ThreadClass();
        thread1.setName("첫 번째 Thread");
        thread1.start();
        
        ThreadClass thread2 = new ThreadClass();
        thread2.setName("두 번째Thread");
        thread2.start();
        System.out.println("프로그램 종료");
    }
}
 
class ThreadClass extends Thread {
    @Override
    public void run() {
        for(int i=0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + " : " + (i + 1));
        }
    }
}



실행 결과


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
프로그램 종료
첫 번째 Thread : 1
첫 번째 Thread : 2
첫 번째 Thread : 3
첫 번째 Thread : 4
두 번째 Thread : 1
두 번째 Thread : 2
두 번째 Thread : 3
두 번째 Thread : 4
두 번째 Thread : 5
첫 번째 Thread : 5
첫 번째 Thread : 6
첫 번째 Thread : 7
두 번째 Thread : 6
두 번째 Thread : 7
두 번째 Thread : 8
첫 번째 Thread : 8
첫 번째 Thread : 9
첫 번째 Thread : 10
두 번째 Thread : 9
두 번째 Thread : 10



위 예제를 보면 쓰레드가 모두 수행되고 종료되기 전에 main 메소드가 먼저 종료되어 버렸다. 그렇다면 모든 쓰레드가 종료된 후에 main 메서드가 종료되지 않으려면 join 메서드를 사용하면 된다. 아래와 같은 코드를 이용하게 된다면 Thread가 종료된 후에 프로그램이 종료하게 된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        List<Thread> list = new ArrayList<Thread>();
        
        ThreadClass thread1 = new ThreadClass();
        thread1.setName("첫 번째 Thread");
        
        ThreadClass thread2 = new ThreadClass();
        thread2.setName("두 번째 Thread");
        
        list.add(thread1);
        list.add(thread2);
        
        for(int i=0; i < list.size(); i++) {
            Thread thread = list.get(i);
            thread.start();
        }
        
        for(int i=0; i < list.size(); i++) {
            Thread thread = list.get(i);
            thread.join();
        }
        
        System.out.println("프로그램 종료");
    }
}
 
class ThreadClass extends Thread {
    @Override
    public void run() {
        for(int i=0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + " : " + (i + 1));
        }
    }
}



자바에서는 스레드 프로그램이과 관련되어 여러 클래스와 다양한 방법을 제시하고 있다. 

보통 쓰레드 객체를 만들어 위와 같이 Thread를 start해서 사용하지만 자바에서 제공하는 아래와 같ㅇ Runnable라는 인터페이스를 이용하여 구현하는 방법이 더 많이 사용되어진다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ThreadTest {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i=0; i < 10; i++) {
                    System.out.println("Runnable을 이용한 for 문 : " + (i+1));
                }
            }
        });
        
        thread.start();
    }
}



스레드는 독립저긴 실행 흐름으로 부모 스레드가 종료되어도 자식 스레드의 실행 흐름에 영향을 미치지 않는다. 프로세스 시작 시 최초 실행되는 static main 메서드는 JVM에 의해 생성된 main() 스레드에서 실행된다. 모든 메서드는 각각의 실행 흐름 즉, 스레드 내에서 동작한다. 그레서 모든 스레드가 종료되어야 프로그램이 종료된다. 그 예로 이 블로그의 첫 번째 예제를 보면 프로그램 종료가 콘솔에 보여 지지만, 자식 스레드로 for문안에 내용이 콘솔에 계속 나타나는 것을 볼 수 있다.


싱글 스레드와 멀티 스레드 


멀티 스레딩의 경우 여러 스레드가 동시에 처리되는 것 처럼 보이나 사실 스레드 스케줄러에 의해 순차/병행 처리된다. 스레드 스케쥴러는 스레드의 상태를 관리하고, 스레드들 중 어떤 스레드를 실행해야 할지 결정한다. 스레드 스케줄러는 사전에 실행된 스레드들의 순서로 결정하는것이 아니라, 우선 순위 및 대기 시간에 의해 결정하므로 결과는 매번 달라진다.


스레드 스케줄러가 하는 일


스레드 스케줄러는 스레드를 관리하는 프로세스다. 여러 스레드들이 서로 공평하게 시스템 리소스를 사용하여 동작할 수 있도록 조정하는 역할을 담당한다. 스레드A와 스레드B가 동시에 생성되었다고 가정하면, 시스템 입장에서는 공평하게 시스템을점유해서 작업이 처리되어야 한다. 

스레드가 실행 중일 때 sleep(), wait(), join(), I/O block 이 발생할 경우 스레드의 상태는 WAITING, BLOCKED 상태로 변경된다. WAITING 혹은 BLOCKED 상태인 스레드는 sleep time-out, notify(), interrupt(), I/O 처리가 완료되면 다시 Runnable 상태로 변경되고 스레드 스케줄러가 Running 상태로 변경할 때까지 기다린다.


스레드 우선 순위와 실행 시간


스레드의 우선 순위를 조정하여 스레드가 얻는 실행 시간을 조정할 수 잇다. 우선 수니우는 1~10까지 설정 가능하며 값이 높을수록 우선 순위는 높아진다. 그리고 이 우선 순위는 실행 대기 중인 스레드에 상대적이다. 즉, 대기 중인 스레드의 우선 순위 값이 모두 10이면 서로 동등한 상태다. 우선 순위의 기본값은 Thread 클래스의 NORM_PRIORITY 변수 값이다.


1
2
3
Thread.MAX_PRIORITY        //10
Thread.NORM_PRIORITY     //5
Thread.MIN_PRIORITY        //1



아래 예제와 결과를 보자


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class Test {
    public static void main(String[] args) {
        MarkThread th1 = new MarkThread("-""th1");
        th1.setPriority(Thread.MIN_PRIORITY);
        
        MarkThread th2 = new MarkThread("*""th2");
        th2.setPriority(Thread.NORM_PRIORITY);
        
        MarkThread th3 = new MarkThread("/""th3");
        th3.setPriority(Thread.MAX_PRIORITY);
        
        th1.start();
        th2.start();
        th3.start();
    }
}
 
class MarkThread extends Thread {
    
    private String marked;
    private String name;
    
    public MarkThread(String marked, String name) {
        this.marked = marked;
        this.name = name;
    }
    
    @Override
    public void run() {
        for(int i=0; i < 100; i++) {
            System.out.print(marked);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        System.out.print("\n" + name + "종료");
    }
}



실행 결과


1
2
3
4
5
6
7
8
9
10
11
12
13
*-//*-/-*/-*/-*/-*/*-*/-*-/*-/
*-/*-//-**-/*-//-*/-*/*-/*-/*-
/*-/*-/*-/*-/-*/-*/*-/*-/-*/*-
/*-/*-/*-/-*/*-/*-/-*/*-/*-/*-
/*-/*-/*-/-*/*-/*-/-*/*-/-*/*-
/*-/*-/*-/*-/-*/*-/*-/*-/-*/-*
/*-/*-/*-/*-/*-/*-/-*/-*/*-/-*
/*-/*-/*-/*-/*-/*-/*-/*-/*-/*-
/-*/*-/*-/-*/*-/*-/*-/*-/-*/-*
/*-/*-/*-/*-/*-/*-/*-/*-/*-/*-
th3종료
th2종료
th1종료



실행 결과를 보면 우선순위가 높은 3 > 2 > 1 순으로 종료되는 것을 확인할 수 있다.


일단 여기까지... 자바 스레드에 대한 기본 사용법이다. 데몬 스레드, 스레드 그룹, 인터럽트, notify(), join() 등등 스레드에 대해 알아야할 내용들이 많지만 책이나 블로그를 통해 찾아보면 될 것 같다.