지금가지 필자가 작성한 모든 프로그램은, 컴퓨터가 실행할 시 필자가 작성한 main 함수 코드의 상단부터 순차적으로 실행하면서 내려오고, Main 함수 내 코드 실행이 완료되면 종료하는 형태로 진행되었다. 그래서 서로 다른 클래스의 매서드를 순차로 호출하면, 먼저 호출된 매서드가 종료되고 나서야 다음 호출된 매서드가 실행이 된다.
그러나 하나의 프로그램에서 순차적으로 코드를 진행하는 방식은 최근에는 아예 사용할 수 없는 방식이다. 예를 들어, 사람들이 의사소통을 위해 사용하는 메신저의 경우, 메세지 전송을 위해 글자를 적고 있는 와중에도 상대편이 보낸 메세지가 채팅창에 나타나야하기 때문이다. 만약 글자 입력하는 중이라고 상대방이 보낸 메세지가, 입력이 다 끝나고 도착하는 상황이 벌어진다면 메신저로의 의사소통은 지금처럼 활발하지 못할 것이다. 아래와 같은 상황이 발생할 것이기 때문에.
모든 코드가 순차적으로, 분기없이 진행되는 프로그램은 싱글 스레드(Single Thread)라고 한다. 프로그램이 메모리에 올라가 실행이 되면 프로세스라는 것이 발생하는데, 한 개의 프로세스만 발생하면 그 프로그램은 Single Thread로 작성되었다고 한다.
컴퓨터 이용자 대부분이 사용하는 윈도우는 OS 자체가 멀티 스레드(Multi Thread)로 작성되었다. 그렇기 때문에 필자가 코드를 작성하고 화면 캡쳐를 하면서 블로그에 글을 쓰는 것을 동시에 진행하는 것이 가능한 것이다. 최근 많은 사람들이 사용하는 구* 메일의 경우, 메일함을 열어놓고 있는 와중에 새 메일이 도착하면 새로 고침 버튼을 클릭하지 않더라도 새 메일이 자동으로 팝업되는데, 메일 서버 내 프로그램이 Multi Thread로 작성된 것이라 보면 된다.
즉, 서로 다른 매서드를 동시에 호출하는 것은 물론, 동시에 실행되도록 하기 위해서는 반드시 Multi Thread로 프로그램을 작성해야한다. 어떻게 해야할까?
1. Single Thread와 Multi Thread 개요
* 본 내용을 읽기 전에 이 포스팅에서 클래스와 메모리 사용 관련 내용을 먼저 확인할 것을 권장한다.
매서드는 호출이 되면 논리적으로 구분된 메모리 구역 중 call stack 부분에 호출한 메서드 정보가 적재된다. 만약 main 함수 내에서 Test1과 Test2의 인스턴스가 생성되고 인스턴스를 통해 매서드 호출이 이루어진다면 call stack의 가장 아래에는 main() 매서드가, 그 위에 호출된 순서에 따라 Test1 인스턴스 매서드와 Test2 인스턴스가 올라가게 된다. 매서드 실행이 완료되면 call stack에서 실행 완료된 매서드는 삭제되며, main() 매서드까지 완료되면 call stack에서 main() 매서드 역시 사라진다. Java에서는 main() 메서드가 call stack에서 사라지면 프로그램이 종료(Terminated, Exited)되었다고 본다.
서두에서 예시로 들었던 코드를 다시 보자.
메모리 상에서는 어떻게 보더라도 t1.run()과 t2.run()이 동시에 존재하기 어려운 구조다. 어떻게 하든지 t1.run()과 t2.run()이 동시에 call Stack에 올라가 있도록 만들어야 동시 실행 가능성을 조금이나마 높일 수 있을 듯 하다. 만약 t1.run() 내부에 t2.run()이 동작하도록 만들면 어떨까?
t1.run()과 t2.run()이 call stack에 동시에 존재하긴 하나, t2.run()이 종료되기 전에는 t1.run()이 실행을 시작도 하지 못하는 상황이 벌어진다. 정리하자면, 두 매서드를 동시에 동작시키기 위해서는 call stack을 둘 이상으로 분할하여, 두 call stack 내 적재된 매서드를 동시에 실행할 수 있어야 한다.
위에서 예시로 설명한 내용은 하나의 call stack을 사용한, 즉 single thread에 대한 내용이다. multi thread는 call stack을 하나가 아닌 여러 개를 사용하는 방식인데, call stack이 물리적으로 나뉘어진 것이 아니라 컴퓨터에서 논리적으로 나눈 것이라 보면 된다.
위의 구조에서 main() 함수가 먼저 종료되고 t2.run()이 실행중이라도 t2.run()이 실행을 완료할 때까지 프로그램은 종료되지 않는다. 반대의 경우도 마찬가지다. 이는 뒤에서 예시로 다시 살펴보려 한다.
그럼, 논리적으로 call stack을 나누는 Thread는 어떻게 사용해야할까?
2. Thread 클래스와 Runnable 인터페이스
매서드를 별도의 call stack에 저장하기 위해 사용하는 클래스/인터페이스는 Thread와 Runnable이다. 둘 다 java.lang 기본 패키지에 포함되어 있는데 Thread는 클래스, Runnable은 인터페이스로 정의되어 있다.
Java Documentation 문서를 보면 Thread 클래스가 Runnable 인터페이스를 구현하고 있음을 알 수 있다. 그럼 Runnable 인터페이스는 어떤 추상 매서드를 선언하고 있을까?
단순히 run() 이라는 매서드 하나만을 선언하고 있다. Runnable 인터페이스의 run() 매서드는 Runnable 인터페이스를 구현한 클래스에서 Thread의 생성과 시작을 위해 사용된다고 나온다. 즉, Thread를 만들 매서드가 정의된 클래스는 반드시 Thread 클래스를 상속받거나 Runnable 인터페이스를 구현해야한다.
Test1과 Test2 클래스에 각각 Thread 클래스와 Runnable 인터페이스를 상속/구현해보자.
이제 Test1과 Test2가 Thread를 생성하기 위한 사전 준비는 끝났다. 그러나 이들 클래스를 호출하는 것으로는 별도의 call statck이 생성되지 않는다. call stack을 별도로 생성하기 위해서는 main() 함수에서 Thread 클래스의 인스턴스를 생성해주어야 한다.
필자가 생성한 Test1과 Test2는 각각 Thread 클래스와 Runnable 인터페이스를 상속/구현하기 때문에 Test1과 Test2는 Runnable 객체로 지정하는 것이 가능하다. 생성자를 보면 Thread(Runnable target)이 존재하는데, 필자는 이 생성자를 이용하여 test1만 새 Thread를 만들어보려 한다.
이제 thread1.run() 매서드, 즉 t1.run() 매서드와 t2.run() 매서드는 별개의 call stack에서 실행될 수 있는 환경에 놓였다. 하지만 아직까지는 thread1.run()과 t2.run()을 호출하더라도 동시에 진행되는 결과는 나타나지 않는다.
왜 그럴까? t1의 경우 call stack을 별도로 생성하기는 했지만, run()을 생성한 call stack이 아니라 기본 call stack에 적재되도록 코드가 작성되었기 때문이다.
t1.run()을 새 call stack으로 이동시키기 위해서, Thread에 존재하는 start()라는 매서드를 사용해야 한다. 이 start() 매서드는 매서드를 호출한 인자가 Runnable().run()을 정의하고 있는 경우, 별도의 call stack에서 run() 매서드를 실행할 수 있도록 만든다. 즉, 위의 그림에서 t1.run()을 새 call stack 으로 옮기기 위해서는 t1.run()을 호출하는 것이 아니라 t1.start() 매서드를 호출해야 한다는 말이다.
thread1.run() 코드를 thread1.start()로 변경하여 적용해보자. 이전과 달리, 두 매서드가 동시에 실행되는 것이 확인된다. 필자는 차이를 확인하기 위해 Test2의 for 문 int i 범위를 10~20으로 재정의했다.
Multi Thread로 진행되는 코드는, 두 call stack에 호출된 모든 매서드가 종료되어야만 프로그램이 완전히 종료된다. 즉, 위의 예시에서 Main()이 먼저 종료되더라도 thread1은 동작중이기 때문에 콘솔에 글자는 계속 찍히게 된다. 필자는 t2.run() 부분을 Test2의 for 문으로 대체한 뒤, 15가 찍힐 무렵 예외를 발생시켜 고의로 main() 함수를 중지시켜보려 한다.
이번에는 t2로 별도의 thread를 만들어 진행해보려 한다. main() 매서드의 for 문은 범위를 20~30으로 재정의해서 매서드들이 어떻게 실행되는지 확인해보려한다.
Thread 구현 시 Thread 클래스와 Implements 인터페이스를 사용하는 두 가지 방식에 대해 소개를 했는데, 사실 Implements 인터페이스로 구현하는 경우가 대부분이다. Thread 클래스를 상속하는 경우, 해당 클래스가 다른 클래스를 상속할 수 없다는 문제가 발생하기 때문이다.
3. Thread 우선순위 제어
두 개의 Thread가 선언되고 start()로 실행되면, 어떤 것이 먼저 실행될까? Java의 Thread는 OS(필자의 경우 window)의 프로세스 스케쥴링을 따르며, 두 Thread의 우선순위가 동일하다면 어떤 것이 실행될지는 알 수 없다. 위에서 살펴보았듯이 말이다.
그럼, Thread의 우선순위를 정할 수 있을까? for 문으로 숫자를 출력하는 클래스 Test1(0~9)과 Test2(10~19)에 Thread를 적용하고, 이들의 우선순위를 확인해보자.
Thread의 우선순위는 매서드 getPriority()를 통해 확인할 수 있다. Thread 우선순위는 정수 1부터 10 사이의 숫자가 반환되는데, Thread의 우선순위가 지정되어 있지 않는 경우 기본값은 5다. 우선순위는 숫자가 높을수록 높다.
우선순위를 변경하는 매서드는 (이제 얼핏 감이 오시겠지만) setPriority()라는 매서드다. 이 매서드는 매개인자로 정수, 즉 지정할 우선순위 값을 받는다.
thread1에 setPriority(1)을 적용하고 각 Thread의 우선순위를 출력하니, Thread1의 우선순위가 변경된 것이 확인된다. 이 상태에서 Thread를 실행하면, thread2가 thread1 보다 항상 먼저 실행된다.
우선순위의 최대, 최소값은 상수 MAX_PRIORITY, MIN_PRIORITY로 지정되어 있다.
우선순위를 설정하더라도 사용하는 PC의 CPU(Central Process Unit, 중앙제어장치) 코어 수에 따라 결과는 다르게 나타날 수 있다. 필자는 싱글 코어 CPU인 PC를 사용중인데, 하나의 코어에서 여러 Thread를 실행하기 때문에 우선순위가 높은 Test2가 항상 먼저 실행된다. 하지만 멀티 코어 CPU를 사용하면, CPU가 프로그램 내 Thread를 두 개 이상의 코어에게 분담하기 때문에 마치 우선순위가 동일한 Thread를 동시에 실행하는 효과가 나타난다.
4. Thread의 그룹 관리
개별적으로 만들어진 Thread들은 그룹으로 묶어서 관리하는 것도 가능하다. ThreadGroup이라는 클래스를 통해 Thread 그룹을 형성할 수 있다.
그룹을 생성하는 것은 매우 간단하다. 생성자의 매개인자로 그룹명을 문자열로 지정해주기만 하면 된다.
그룹 안에 또 다른 Thread 그룹을 만드는 것도 가능하다. 생성자 중 ThreadGroup(ThreadGroup tgroup, String name) 으로 정의된 생성자가 있는데, 매개인자로 입력되는 ThreadGroup을 상위 그룹으로 지정하겠다는 의미다. 필자는 tGroup1_1이라는 이름으로 그룹을 만들어, tGroup1 아래에 위치시켜보려 한다.
Thread 그룹의 구조를 확인하는 매서드는 ThreadGroup의 list()라는 매서드다. 이 매서드는 인스턴스로 호출된 ThreadGroup이 하위 그룹을 가지는 경우, 자기 자신을 포함한 그룹 구조를 화면에 표시한다.
그런데, 필자는 Thread 그룹을 생성하기만 했을 뿐, 개별 Thread가 그룹 내에 포함되도록 코드를 작성하지 않았다. 개별 Thread를 Thread 그룹에 추가하는 방법은 매우 간단한데, Thread 생성자의 첫 번 째 매개인자에 ThreadGroup의 변수명을 작성하면 된다. 필자는 thread1은 tGroup1로, thread2와 thread3은 tGroup1_1로 지정하려한다.
각 Thread가 start()에 의해 동작할 때, ThreadGroup은 그룹 내 활성화(run()이 실행되는)된 Thread의 수를 반환하는 매서드를 가지고 있다.
activeCount() 매서드는 자신의 그룹 뿐만 아니라 하위 그룹에 속한 Thread 역시 활성화된 경우 집계에 포함시킨다. 따라서 tGroup1은 thread1만 포함되어 있지만 하위 그룹인 tGroup1_1에 포함된 활성 Thread로 인해 3이라는 값이 출력되는 것이다.
Thread 외에도 활성화 된 ThreadGroup의 수를 반환하는 activeGroupCount() 매서드도 존재한다. 이 메서드는 활성화 된 Thread가 아니라 Group의 수량을 반환하지만, Thread와 달리 자기 자신을 포함하지 않고 하위 그룹만 고려한다.
그룹에 속한 Thread의 우선순위는 Thread에서 직접 지정해주어야 하지만, 그룹 내 Thread의 최대 우선순위 값을 제한하는 것이 가능하다. 그룹 내 Thread에 지정할 수 있는 우선순위 최대값을 확인하려면 getMaxPriority() 매서드를 사용하며, 특정 우선순위를 그룹에 적용하려면 setMaxPriority() 매서드를 사용하면 된다. 그룹 내부에 존재하는 Thread의 우선순위는 getMaxPriority()의 반환값을 초과하지 못한다.
Thread는 우선순위가 기본값 5로 설정되어 있다. 하지만 ThreadGroup은 우선순위가 기본값 10이다. 아무래도 그룹에 속할 Thread가 우선순위값을 어떻게 가지고 있을지 모르기 때문에 ThreadGroup의 우선순위를 10으로 정한 것이 아닌가 싶다.
이제 tGroup1_1의 최대 우선순위값을 3으로 변경하려 한다. 내부의 Thread도 Group 최대 우선순위에 맞게 변경될까?
빨간 박스를 보면 Group의 우선순위 최대값이 제한되더라도 기존에 존재하던 Thread의 우선순위는 변하지 않는다. 대신, setMaxPriority() 적용 후, 그룹 내 Thread의 우선순위를 변경하게 되면, 그룹의 최대 우선순위값을 초과하지 못하게 된다. 주황색 박스 부분을 보면 그룹 내 thread2의 우선순위를 10으로 변경했지만, Group의 우선순위 상한은 3으로 지정되어 thread2 역시 우선순위가 3으로 제한된다.
thread3은 여전히 우선순위가 5다. 그럼 이 상태에서 Thread가 실행되는 순서는 어떻게 될까? 그룹 우선순위가 정해져 있지만 thread 자체의 우선순위는 변하지 않기 때문에 thread1 = thread3 > thread2 순서로 진행된다.
이제 근본적인 질문을 던져보자. Java의 main 매서드를 실행하는 Thread는 무엇일까? Java는 JVM에 main과 system이라는 Thread를 가진다. main은 코드 실행에 있어 가장 최상위 Thread이며, system은 코드 실행 시 발생하는 Null Pointer 변수들의 가비지 컬렉션(Garbage Collection)을 수행하는 Thread다(조금 더 정확히 이야기하자면 system 그룹 하위의 Finalizer라는 Thread에 의해 실행된다.).
그리고 프로그래머가 생성한 모든 Thread, ThreadGropu은 main Thread 그룹 아래에 존재하게 된다.
다음 포스팅에서는 Thread의 실행제어 및 Daemon Thread에 대해 알아보려 한다.
Fin.
'Java > Java Basic' 카테고리의 다른 글
[Java Basic] 44 - Java Thread 3 - Thread 동기화(Synchronized, wait(), notify()) (0) | 2022.09.23 |
---|---|
[Java Basic] 43 - Java Thread 2 - Thread 실행 제어 및 Daemon Thread (0) | 2022.09.20 |
[Java Basic] 41 - Java Annotation (0) | 2022.09.15 |
[Java Basic] 40 - 열거형(enum) 개요와 사용법 (0) | 2022.09.13 |
[Java Basic] 39 - Generic 개요와 사용법 (0) | 2022.09.12 |
댓글