Thread 실습 - Java
지난 번에 쓰레드(Thread) 개념과 간단한 실습 코드를 통해 쓰레드가 어떤 개념이고 어떻게 활용하는지 간략하게 알아보았다. 하지만 쓰레드의 생명주기를 이해하고 프로그램 구성에 맞춰 쓰레드를 활용해야 효과적으로 쓰레드를 제대로 활용할 수 있다. 이번 글을 통해서 쓰레드의 생명주기와 예제 코드를 통한 쓰레드의 본격적인 활용법에 대해 알아보고자 한다.
Thread의 생명주기
쓰레드는 생성이 되면 실행 가능한 상태로 진입하게 된다. run() 호출을 통해 쓰레드가 실행되면 sleep(), join(), wait() 을 통해 쓰레드가 대기 상태로 진입하고, 대기 상태가 외부의 명령으로 인해 해제가 되면 다시 실행가능한 상태로 진입하게 된다. 만약 쓰레드가 실행되고 아무런 interrupt를 받지 않게 되면 쓰레드 수행을 모두 완료하게 되면 종료(terminated) 상태로 진입하여 쓰레드가 모든 작업을 수행하고 종료하게 된다.
Thread 우선순위
쓰레드에 우선순위를 부여하여 더 높은 우선순위를 가진 쓰레드가 먼저 수행되도록 설정할 수 있다. 이러한 우선순위 설정으로 가장 중요도가 높은 작업을 수행하는 쓰레드를 먼저 처리할 수 있기에 쓰레드의 작업 처리와 관련된 변수를 사전에 차단할 수 있다.
직접 쓰레드의 우선순위를 부여하고, 설정한 우선순위대로 쓰레드가 동작하는지 확인해보자.
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.StringTokenizer;
class ThreadPriority extends Thread {
public ThreadPriority(String str) {
setName(str);
}
@Override
public void run() {
super.run();
for (int i = 0; i <= 5; i++) {
System.out.println(i + " : 우선 순위 : " + getPriority());
}
}
}
public class PriorityTest {
public static void main(String[] args) throws Exception {
ThreadPriority t1 = new ThreadPriority("첫번째 쓰레드");
ThreadPriority t2 = new ThreadPriority("두번째 쓰레드");
ThreadPriority t3 = new ThreadPriority("세번째 쓰레드");
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
int first = Integer.parseInt(st.nextToken());
int second = Integer.parseInt(st.nextToken());
t1.setPriority(first);
t2.setPriority(second);
t3.setPriority(Thread.MIN_PRIORITY);
t1.start();
t2.start();
t3.start();
}
}
Thread 클래스를 상속받아 'ThreadPriority'라는 새로운 클래스를 생성하여 부여받은 우선순위를 출력하도록 정의했다. main 함수에서 3개의 객체를 생성하여 2개의 객체에는 직접 입력한 우선순위를, 나머지 하나의 객체에는 최소 우선순위를 부여하여 쓰레드를 시작하도록 코드를 작성했다.
출력한 결과에서 알 수 있듯이 높은 숫자의 우선순위를 부여받은 쓰레드가 가장 먼저 작업을 처리하게 되고, 우선순위가 가장 낮은 쓰레드가 가장 나중에 처리되는 것을 확인할 수 있다. 결국엔 출력 결과를 통해 각 쓰레드가 부여받은 우선순위대로 작업을 처리하는 것을 볼 수 있다.
(우선순위를 부여했음에도 불구하고 쓰레드의 작업 처리 순서가 무작위로 진행되는 경우가 흔치 않게 볼 수 있다. 이에 대한 문제점은 별도로 확인해봐야 할 듯 하다.)
Thread의 시작과 종료
쓰레드를 시작하고나서 해당 쓰레드가 종료될 때까지 기다리고 그 다음 쓰레드가 마저 작업을 처리한다음 종료되도록 설정할 수 있다. join() 호출을 통해서 해당 쓰레드가 종료될 때까지 다른 쓰레드가 대기할 수 있는데 직접 예제 코드와 출력 결과를 통해 알아본다.
class JoinThread extends Thread {
JoinThread(String s) {
setName(s);
}
@Override
public void run() {
super.run();
for (int i = 0; i < 5; i++) {
System.out.println(getName() + " 쓰레드 : " + i);
}
}
}
public class JoinTest {
public static void main(String[] args) throws Exception {
JoinThread t1 = new JoinThread("첫 번째");
JoinThread t2 = new JoinThread("두 번째");
JoinThread t3 = new JoinThread("세 번째");
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
}
}
쓰레드 관련한 'JoinThread' 클래스를 정의하고 이와 관련된 객체 3개를 생성하여 차례대로 join() 호출하여 쓰레드가 하나씩 순서대로 종료될 수 있도록 코드를 작성했다. 이에 대한 결과는 어떻게 될 지 출력 결과를 통해 확인해보자.
출력 결과를 통해 알 수 있듯이 join()을 먼저 호출한 쓰레드 순서대로 작업이 먼저 처리되어 종료가 되고, 나머지 쓰레드도 마찬가지로 차례로 작업을 다 끝내고 종료되는 것을 알 수 있다. 이러한 결과를 보았을 때, 쓰레드가 무작위로 작업을 끝내는 것을 방지할 수 있어 개발자에 의해 쓰레드의 예기치 못한 동작을 제어할 수 있게 된다.
sleep()
전공자들이나 Java 언어를 학습했던 사람들 모두 한번씩은 쓰레드의 sleep() 기능을 써봤을 것이다. 그만큼 쓰레드를 학습하는데 자주 나오고 단순한 기능이다. 말 그대로 해당 쓰레드를 일정 시간동안 잠깐 잠들게 하는 함수이다. 긴말 필요없이 예제 코드를 통해 실습해보자.
class ThreadSleep extends Thread {
@Override
public void run() {
super.run();
for(int i = 0; i < 5; i++){
System.out.println("Thread Sleep count : "+ i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class SleepTest {
public static void main(String[] args) {
ThreadSleep t = new ThreadSleep();
t.start();
}
}
'ThreadSleep' 클래스를 정의하여 Thread.sleep()을 사용하여 쓰레드가 잠깐의 시간동안 잠들게 한다음 마저 작업을 수행하도록 코드를 작성했다. sleep()이 과연 어떻게 동작하는지 출력결과를 통해 확인해본다.
출력 결과에서 확인할 수 있듯이 sleep()을 사용하면 쓰레드가 작업 처리 도중 일정 시간동안 잠들게 되면서 그만큼 작업 처리 시간이 지연되는 것을 볼 수 있다. 소스 코드 작성 시 특정 쓰레드를 잠시 잠들게 하여 개발자가 의도한 흐름대로 작업 처리가 이루어질 수 있도록 프로그램을 구성할 수 있다.
synchronized
쓰레드의 개념을 알고 있는 사람은 '멀티 쓰레드(Multi Thread)'에 대한 개념을 알고 있을 것이다. 멀티 쓰레드는 말 그대로 복수의 쓰레드를 의미하며, 하나의 프로세스 안의 여러 쓰레드가 일정 시간동안 작업을 동시에 처리하는 것을 의미한다. Java에서는 main 함수가 메인 쓰레드에서 동작하고, main 함수 내에서 별도 쓰레드를 생성하여 작업을 수행시키면 메인 쓰레드와 별도로 서브 쓰레드가 작업을 수행하면서 동시에 작업을 처리하는 멀티 쓰레드를 구현할 수 있다. 하지만 복수의 쓰레드가 동시에 동일한 자원에 접근하지 못하도록 이를 방지해야 하는데, 왜 방지해야 하는지 아래 예시 코드와 출력 결과를 통해 알아보자.
public class SyncTest {
static int num = 0;
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = 10;
System.out.println(num);
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = 20;
System.out.println(num);
}
});
t1.start();
t2.start();
}
}
일정 시간동안 쓰레드를 잠들게 한 후 멤버 변수인 num에 각각 10, 20이라는 값을 저장하는 2개의 쓰레드를 각각 정의하였다. num에 10을 저장하는 쓰레드를 먼저 실행시키도록 코드를 구성하였기에 num은 최종적으로 20이란 값을 가져야 한다. 과연 의도한대로 결과를 출력하는지 출력 결과를 통해 확인해보자.
출력 결과에서 알 수 있듯이 num 값이 차례로 10, 20으로 출력되는 것이 아닌 20이 두 번 출력되는 것을 확인할 수 있다. 이는 두 개의 쓰레드가 동시에 멤버 변수에 접근하면서 num의 값이 예기치 못한 동일한 값으로 출력되는 것이다. 동시에 동일한 자원에 접근하다보니 어느 특정 쓰레드가 다른 쓰레드와 공유하는 자원에 접근하여 수행한 작업 내용이 프로그램에 반영되지 못하는 것이다.
이러한 관점에서 여러 개의 쓰레드가 작업을 처리하는 과정에서 동일한 자원에 동시에 접근하는 것을 방지하고자 하는 목적으로 등장한 개념이 바로 '동기화'이다. Java에서는 'synchronized' 예약어를 통해 동기화를 구현하여 멀티 쓰레드의 문제를 해결할 수 있다. 아래의 예시를 통해서 확인해보자.
public class SyncTest {
static int num = 0;
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(new Runnable() {
@Override
synchronized public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = 10;
System.out.println(num);
}
});
Thread t2 = new Thread(new Runnable() {
@Override
synchronized public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = 20;
System.out.println(num);
}
});
t1.start();
t2.start();
}
}
두 쓰레드에 모두 'synchronized' 예약어를 사용하여 동시에 특정 자원에 접근하지 못하도록 정의하였다. 과연 쓰레드가 동시에 접근하지 않고 각자가 작업을 처리한 결과가 나타나는지 출력 결과를 통해 확인해보자.
출력 결과에서 알 수 있듯이 각 쓰레드가 수행한 작업 결과가 나타나는 것을 확인할 수 있다. 결국엔 각자가 멤버 변수에 다른 시기에 접근하여 작업을 처리했다는 사실을 알 수 있다. synchronized 키워드를 통해 멀티 쓰레드 환경에서 각 쓰레드가 동시에 자원에 접근하는 것을 방지하여 개발자가 의도한대로 쓰레드가 동작할 수 있다.
wait()과 notify()
쓰레드를 잠들게 할 수 있을 뿐만 아니라, 잠시 대기 상태에 있도록 지정한다음 이후에 다시 작업을 처리하도록 함수 호출을 통해 쓰레드를 깨워줄 수 있다. wait()을 통해 쓰레드를 대기 상태로 지정하고, notify()를 통해 특정 쓰레드를 깨우거나 notifyAll()을 통해 대기 상태에 있는 모든 쓰레드를 깨울 수 있다. 예제 코드를 통해 어떻게 구현할 수 있는지 알아보자.
class SyncThread extends Thread {
int total = 0;
@Override
public void run() {
synchronized (this) {
for (int i = 0; i < 5; i++) {
total += i;
System.out.println("total = " + total);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
notify();
}
}
}
public class SyncTest {
public static void main(String[] args) throws Exception {
SyncThread t = new SyncThread();
t.start();
synchronized (t) {
try {
System.out.println("쓰레드가 완료될 때까지 기다린다.");
t.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread 수행 결과 : " + t.total);
}
}
wait()을 통해 메인 쓰레드를 대기 상태로 전환시키고 서브 쓰레드가 수행을 완료하고 나서 notify()를 호출하여 대기 상태에 있는 쓰레드 중 무작위로 특정 쓰레드를 선정하여 실행 가능한 상태로 깨워준다. 예시에서는 대기 상태에 있는 쓰레드가 메인 쓰레드 밖에 없기에 메인 쓰레드를 깨워주어 서브 쓰레드가 작업을 완료하고 나서 마저 메인 쓰레드가 작업을 수행하게 된다. 소스 코드에서 의도한 대로 쓰레드가 동작하여 원하는 결과를 보여줄 지 출력 결과를 통해 확인해보자.
메인 쓰레드가 대기 상태에 들어간 이후 서브 쓰레드가 작업을 완료한대로 메인 쓰레드가 깨어나 마저 작업을 수행하는 모습이 출력 결과를 통해 나타난 것을 볼 수 있다. 이처럼 wait()와 notify()를 통해 쓰레드를 대기 상태로 전환하고 이후 다시 실행 가능한 상태로 바꾸어 마저 작업을 수행할 수 있도록 쓰레드를 제어할 수 있다. 추가로 필요하다면 notifyAll()을 통해 대기 상태에 있는 모든 쓰레드를 깨워 모두 작업을 다시 수행시키도록 할 수 있다.
지난번에 이어 직접 실습하면서 쓰레드가 어떠한 기능을 수행하는지에 대해 알아보았다. Java에서의 쓰레드 기능은 지금까지 소개한 것이 전부가 아니라 아직 더 남아있다. 이후에 여유가 된다면 마저 쓰레드의 주요 기능에 대해 직접 실습하며 알아보고 싶다. Java에서 쓰레드의 기능이 이처럼 많은 만큼, 프로그램을 개발하는 과정에서 쓰레드를 유용하게 쓸 수 있다. 실제로 프로젝트를 진행하면서 얼마나 쓰게 될 지는 잘 모르겠지만 이번 기회를 통해서 쓰레드가 어떤 기능을 가지고 있는 지에 대해 알아볼 수 있어 유익했었던 것 같다. 개발자는 항상 프로그램이 효율적으로 동작하기 위한 고민을 가지고 구현 작업을 진행해야 하기에 쓰레드의 개념에 대해 확실히 이해하는게 도움이 되지 않을까 하는 생각이 든다.
참고
- https://mozi.tistory.com/552
[JAVA] 자바 스레드 synchronized 동기화 방법
자바 스레드의 모순 자바 스레드는 프로그램을 병렬처리 흐름으로 할 수 있기 때문에 꼭 필요한 기능입니다. 그러나 스레드를 여러 개 사용할 때에는 주의해야 합니다. 그 이유는 여러개의 스레
mozi.tistory.com
- https://programmers.co.kr/learn/courses/9/lessons/278#
자바 중급 - 쓰레드와 상태제어(wait, notify)
쓰레드와 상태제어(wait, notify) wait와 notify는 동기화된 블록안에서 사용해야 한다. wait를 만나게 되면 해당 쓰레드는 해당 객체의 모니터링 락에 대한 권한을 가지고 있다면 모니터링 락의 권한을
programmers.co.kr
- https://jamssoft.tistory.com/200
condition(wait, notify[All] ) : Java 쓰레드 동기화(Synchronization)
wait(), notify(), notifyAll() 스레드간에 서로 변수를 동시에 바꿔 발생하는 문제는 지난 글의 내용이고 이제 쓰레드간의 정보의 전달을 위해 사용하는 방법을 배워보자, 정확히 주로 정보의 전달에
jamssoft.tistory.com
'기본기 다지기 > 문법' 카테고리의 다른 글
enum & struct (0) | 2022.06.12 |
---|---|
Thread란? (0) | 2022.04.03 |
추상클래스(abstract class)와 인터페이스(interface) (0) | 2022.03.27 |
static 이란? (0) | 2022.03.13 |