티스토리 뷰
스레드(Thread)
컴퓨터에서 동작하고 있는 프로그램을 프로세스(Process)라 부른다.
프로세스는 프로그램에 사용되는 데이터와 메모리 등의 자원과 스레드로 구성된다.
스레드(Thread)란 그 프로세스 내에서 실제로 작업을 수행하는 주체를 말하는데,
당연하게도 모든 프로세스에는 기본적으로 하나 이상의 스레드가 존재한다.
참고로 스레드를 직역하면 실, 줄거리, 맥락 등의 단어가 되며
프로그램 내에서 실행되는 프로그램 제어 흐름이라는 느낌으로 받아들이면 편하다.
또한 하나의 스레드를 가지는 프로그램을 싱글 스레드 프로그램이라고 부르며
두 개 이상의 스레드를 가지는 프로그램을 멀티 스레드 프로그램이라 부른다.
멀티 스레딩은 하나의 프로그램 안에서 여러 작업을 동시에 수행하는 데 있어서 핵심적인 역할을 수행한다.
메인 스레드 외에 작업 스레드를 활용한다는 것의 의미는
- 작업 스레드가 수행할 코드를 작성
- 작업 스레드를 생성하여 실행
한다는 의미이다.
자바에서 스레드를 생성하는 방법은 다음의 두 가지 방법이 있는데,
- Runnable 인터페이스 구현
- Thread 클래스를 상속
이 그것이다. 두 방법 모두 run() 메서드를 오버라이딩해 작업하고 싶은 내용을 입력하면 된다.
1. Runnable 인터페이스 구현
임의의 클래스를 만들어 Runnable 인터페이스를 구현한다.
인터페이스에 run()이 정의되어 있으므로 반드시 run()을 구현한다.
public class ThreadExample1 {
public static void main(String[] args) {
//Runnable 인터페이스를 구현한 객체 생성
Runnable task1 = new ThreadTask1();
//Runnable 구현 객체를 인자로 전달하면서 Thread 클래스를 인스턴스화 하여 스레드를 생성
Thread thread1 = new Thread(task1);
// 위의 두 줄을 아래와 같이 한 줄로 축약할 수도 있다.
// Thread thread1 = new Thread(new ThreadTask1());
//작업 스레드를 실행시켜, run() 내부의 코드를 처리
thread1.start();
for(int i = 0; i < 9999; i++) {
System.out.print("@");
}
}
}
// Runnable 인터페이스를 구현하는 클래스
class ThreadTask1 implements Runnable {
//run 메서드 바디에 스레드가 수행할 작업 내용 작성
public void run() {
for(int i = 0; i < 9999; i++){
System.out.print("#");
}
}
}
run() 메서드를 오버라이딩 해 #을 9999번 찍는 메서드를 생성했다.
이를 실행시키기 위해 start() 메서드를 이용했으며,
메인 스레드에 작업을 시키기 위해 for문으로 @를 9999번 찍는 반복문을 입력했다.
위 코드를 실제로 작동시켜 보면 #와 @이 무작위로 번갈아 찍히는 것을 확인할 수 있다.
이는 메인 스레드와 작업 스레드가 동시에 병렬로 실행되었기 때문이다.
2. Thread 클래스 상속
Thread 클래스를 상속받는 경우에도 위와 마찬가지 과정을 거친다.
public class ThreadExample2 {
public static void main(String[] args) {
Thread thread2 = new ThreadTask2();
// 작업 스레드를 실행시켜, run() 내부의 코드를 처리하도록 합니다.
thread2.start();
// 반복문 추가
for (int i = 0; i < 9999; i++) {
System.out.print("@");
}
}
}
class ThreadTask2 extends Thread {
public void run() {
for (int i = 0; i < 9999; i++) {
System.out.print("#");
}
}
}
차이점이 있다면 이번에는 Thread를 직접 인스턴스화 하지 않는다는 것이다.
결과는 위와 동일하다는 것을 쉽게 확인할 수 있다.
익명 객체를 이용한 스레드 생성
앞선 과정에선 ThreadTask1, ThreadTask2 클래스를 만들어 그 안에 run()을 오버라이딩 했다.
하지만 과정을 하나씩 줄여서 익명 객체를 활용해 스레드를 생성할 수도 있다. 각각 예를 들면 다음과 같다.
Runnable 익명 구현 객체를 활용한 스레드 생성
public class ThreadExample1 {
public static void main(String[] args) {
// 익명 Runnable 구현 객체를 활용하여 스레드 생성
Thread thread1 = new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 9999; i++) {
System.out.print("#");
}
}
});
thread1.start();
for (int i = 0; i < 9999; i++) {
System.out.print("@");
}
}
}
Thread 익명 하위 객체를 활용한 스레드 생성
public class ThreadExample2 {
public static void main(String[] args) {
// 익명 Thread 하위 객체를 활용한 스레드 생성
Thread thread2 = new Thread() {
public void run() {
for (int i = 0; i < 9999; i++) {
System.out.print("#");
}
}
};
thread2.start();
for (int i = 0; i < 9999; i++) {
System.out.print("@");
}
}
}
스레드의 이름, 주소 값
기본 스레드는 'main'이라는 이름을 가지며 작업 스레드는 'Thread-n'이라는 이름을 가진다.
스레드의 이름은 getName() 메서드를 이용해 조회할 수 있으며 setName() 메서드를 통해 설정할 수 있다.
public class ThreadExample3 {
public static void main(String[] args) {
Thread thread3 = new Thread(new Runnable() {
public void run() {
System.out.println("Get And Set Thread Name");
}
});
System.out.println(Thread.currentThread().getName());
thread3.start();
System.out.println("thread3.getName() = " + thread3.getName());
thread3.setName("Code States");
System.out.println("thread4.getName() = " + thread3.getName());
}
}
// 출력 결과
main
thread3.getName() = Thread-0
thread3.getName() = Code States
중간에 사용된 currentThread() 메서드는 Thread 클래스의 정적 메서드로서
현재 실행 중인 스레드의 주소 값을 반환한다.
스레드의 동기화
스레드의 동기화란 여러 스레드가 동일한 데이터를 사용할 때 발생할 수 있는 오류를 방지하는 기법이다.
스레드의 동기화에선 임계 영역(Critical Section)과 락(Lock)이 중요한데
임계 영역으로 설정한 구역은 동시에 데이터를 사용할 수 없는 지역이고
락을 획득한 스레드만 접근 권한이 주어지는 방식이라고 할 수 있다.
public class ThreadExample6 {
public static void main(String[] args) {
Runnable threadTask6 = new ThreadTask6();
Thread thread6_1 = new Thread(threadTask6);
Thread thread6_2 = new Thread(threadTask6);
thread6_1.setName("김코딩");
thread6_2.setName("박자바");
thread6_1.start();
thread6_2.start();
}
}
class Account {
//잔액을 나타내는 변수
private int balance = 1000;
public int getBalance() {
return balance;
}
//인출 성공시 true, 실패치 false 반환
public boolean withdraw(int money) {
//인출 가능 여부 판단
if(balance >= money) {
//if문의 실행부에 진입하자마자 해당 스레드를 일시 정지 시키고
//다른 스레드에게 우선권을 강제로 넘김
//문제상황을 의도해 추가한 코드
try {Thread.sleep(1000);} catch (Exception error){}
//잔액에서 인출금을 깎아 새로운 잔액 기록
balance -= money;
return true;
}
return false;
}
}
class ThreadTask6 implements Runnable {
Account account = new Account();
public void run() {
while(account.getBalance() > 0) {
//100 ~ 300의 인출금을 랜덤으로 배정
int money = (int)(Math.random() * 3 + 1) * 100;
//withdraw를 실행시키는 동시에 인출 성공 여부를 변수에 할당
boolean denied = !account.withdraw(money);
//인출 결과 확인
//만약 withdraw가 false를 리턴하였다면 해당 내역에 -> DENIED 출력
System.out.println(String.format("Wirhdraw %d$ By %s. Balance: %d$ %s",
money, Thread.currentThread().getName(), account.getBalance(), denied ? "-> DENIED": ""));
}
}
}
위 코드를 실행해 보면 아래와 같이 의도하지 않은 결과가 출력됨을 알 수 있다.
Wirhdraw 200$ By 김코딩. Balance: 800$
Wirhdraw 300$ By 박자바. Balance: 500$
Wirhdraw 200$ By 김코딩. Balance: 300$
Wirhdraw 200$ By 박자바. Balance: 100$
Wirhdraw 200$ By 박자바. Balance: 100$ -> DENIED
Wirhdraw 100$ By 김코딩. Balance: -100$
Wirhdraw 100$ By 박자바. Balance: -100$
이는 두 스레드가 같은 객체를 공유해 작업을 했기 때문인데,
위에 적은 대로 임계 영역을 설정해주면 문제는 해결된다.
이때 사용하는 키워드가 synchronized인데, 임계 영역을 설정하고 싶은 메서드 혹은 특정 구간 앞에 붙여주면 된다.
위 코드의 경우에는 withdraw()의 반환 타입 앞에 synchronized를 붙여줌으로써 문제가 간단하게 해결된다.
public class ThreadExample6 {
public static void main(String[] args) {
Runnable threadTask6 = new ThreadTask6();
Thread thread6_1 = new Thread(threadTask6);
Thread thread6_2 = new Thread(threadTask6);
thread6_1.setName("김코딩");
thread6_2.setName("박자바");
thread6_1.start();
thread6_2.start();
}
}
class Account {
//잔액을 나타내는 변수
private int balance = 1000;
public int getBalance() {
return balance;
}
//인출 성공시 true, 실패치 false 반환
public synchronized boolean withdraw(int money) {
//인출 가능 여부 판단
if(balance >= money) {
//if문의 실행부에 진입하자마자 해당 스레드를 일시 정지 시키고
//다른 스레드에게 우선권을 강제로 넘김
//문제상황을 의도해 추가한 코드
try {Thread.sleep(1000);} catch (Exception error){}
//잔액에서 인출금을 깎아 새로운 잔액 기록
balance -= money;
return true;
}
return false;
}
}
class ThreadTask6 implements Runnable {
Account account = new Account();
public void run() {
while(account.getBalance() > 0) {
//100 ~ 300원의 인출금을 랜덤으로 배정
int money = (int)(Math.random() * 3 + 1) * 100;
//withdraw를 실행시키는 동시에 인출 성공 여부를 변수에 할당
boolean denied = !account.withdraw(money);
//인출 결과 확인
//만약 withdraw가 false를 리턴하였다면 해당 내역에 -> DENIED 출력
System.out.println(String.format("Wirhdraw %d$ By %s. Balance: %d$ %s",
money, Thread.currentThread().getName(), account.getBalance(), denied ? "-> DENIED": ""));
}
}
}
스레드의 상태와 실행 제어
스레드에는 총 여섯 가지 상태가 존재한다.
상태 | 열거 상수 | 설명 |
객체 생성 | NEW | 스레드 객체가 생성. 아직 start() 메소드가 호출되지 않은 상태 |
실행 대기 | RUNNABLE | 실행 상태로 언제든지 갈 수 있는 상태 |
일시 정지 |
WAITING | 다른 스레드가 통지할 때까지 기다리는 상태 |
TIMED_WAITING | 주어진 시간 동안 기다리는 상태 | |
BLOCKED | 사용하고자 하는 객체의 락이 풀릴 때까지 기다리는 상태 | |
종료 | TERMINATED | 실행을 마친 상태 |
각각에 대한 다른 설명은 아래와 같다.
- NEW : 스레드가 생성되었지만 아직 실행할 준비가 되지 않음
- RUNNABLE : 스레드가 실행되고 있거나 실행 준비되어 스케쥴링을 기다리는 상태
- WAITING : 다른 스레드가 notify(), notifyAll()을 불러주기 기다리고 있는 상태(동기화)
- TIMED_WAITING : 스레드가 sleep(n) 호출로 인해 n 밀리 초 동안 잠을 자고 있는 상태
- BLOCKED : 스레드가 I/O 작업을 요청하면 자동으로 스레드를 BLOCK 상태로 만든다.
- TERMINATED : 스레드가 종료된 상태
스레드의 상태는 다른 스레드나 메서드에 의해 변경이 가능하며, 아래에서는 그림에 나온 메서드들에 대해 살핀다.
sleep(long n) - n 밀리초 동안 스레드를 멈춤
스레드 동기화 예제에서 먼저 사용된 sleep()은 Thread 클래스의 클래스 메서드이다.
따라서 위에서 볼 수 있듯이 Thread.sleep(n)으로 호출할 수 있으며,
약 n밀리초 동안(오차가 존재한다) 스레드의 실행을 멈춘다.
다른 말로 하면 스레드의 상태가 실행에서 TIME_WAITING으로 전환된다고 할 수도 있다.
sleep()에 의해 일시 정지된 스레드는 다음의 경우에 실행 대기(RUNNABLE) 상태로 복귀한다
- n밀리 초가 흐른 경우
- interrupt()를 호출한 경우
interrupt()를 호출하는 경우는 반드시 try{} catch(){} 문으로 예외처리를 해주어야 하는데,
이는 interrupt() 호출 시 기본적으로 예외가 발생하기 때문이다.
try { Thread.sleep(1000); } catch (Exception error) {}
interrupt() - 일시 정지 상태의 스레드를 RUNNABLE로 복귀
sleep(), wait(), join() 메서드는 스레드의 흐름을 일시 정지시킨다.
멈춘 스레드가 아닌 다른 스레드에서 멈춘_스레드_명. interrupt()를 호출하면,
sleep(), wait(), join()에 예외가 발생하게 되어 일시 정지가 풀리게 된다.
public class ThreadExample7 {
public static void main(String[] args) {
Thread thread7 = new Thread() {
public void run() {
try {
while (true) Thread.sleep(1000);
}
catch (Exception e) {}
System.out.println("Woke Up!");
}
};
System.out.println("thread7.getState() = " + thread7.getState());
thread7.start();
System.out.println("thread7.getState() = " + thread7.getState());
while(true) {
if(thread7.getState() == Thread.State.TIMED_WAITING) {
System.out.println("thread7.getState() = " + thread7.getState());
break;
}
}
thread7.interrupt();
while (true) {
if (thread7.getState() == Thread.State.RUNNABLE) {
System.out.println("thread7.getState() = " + thread7.getState());
break;
}
}
while(true) {
if(thread7.getState() == Thread.State.TERMINATED) {
System.out.println("thread7.getState() = " + thread7.getState());
break;
}
}
}
}
// 출력 결과
thread7.getState() = NEW
thread7.getState() = RUNNABLE
thread7.getState() = TIMED_WAITING
Woke Up!
thread7.getState() = RUNNABLE
thread7.getState() = TERMINATED
yield() - 다른 스레드에게 실행을 양보
스레드가 처리하는 작업은 반복문인 경우가 많다.
가끔 이 반복문이 무의미한 반복을 하게 되는 경우가 생기는데, yield()는 그때 사용한다.
public void run() {
while (true) {
if (example) {
...
}
}
}
위와 같은 코드를 볼 때, example의 값이 false여도 while문은 계속해서 반복된다.
이럴 때 이 스레드를 실행하는 것보다 다른 스레드를 실행하는 것이 효율성 측면에서 도움이 된다.
public void run() {
while (true) {
if (example) {
...
}
else Thread.yield();
}
}
yield()를 호출하면 example의 값이 false인 경우 무의미한 반복을 멈추고 RUNNABLE 상태로 바뀌며,
자신에게 남은 실행시간을 대기열에서 우선순위가 높은 스레드에게 양보한다.
join() - 다른 스레드의 작업이 끝날 때까지 대기
join()은 특정 스레드가 작업하는 동안 자신을 일시 정지 상태로 만드는 메서드이다.
sleep()과 유사하게 밀리초 단위로 시간을 설정할 수도 있으며,
interrupt()가 호출되거나 join()호출시 지정했던 스레드가 작업을 마치면 실행 대기(RUNNABLE) 상태로 돌아간다.
사용 방법도 sleep() 메서드와 유사하지만 join()은 특정 스레드의 인스턴스 메서드라는 차이가 존재한다.
public class ThreadExample8 {
public static void main(String[] args) {
SumThread sumThread = new SumThread();
sumThread.setTo(100);
sumThread.start();
//메인 스레드가 sumThread의 작업이 끝날 때가지 기다립니다.
try { sumThread.join(); } catch (Exception e) {}
System.out.println(String.format("1부터 %d까지의 합: %d", sumThread.getTo(), sumThread.getSum()));
}
}
class SumThread extends Thread {
private long sum;
private int to;
public long getSum() {
return sum;
}
public int getTo() {
return to;
}
public void setTo(int to) {
this.to = to;
}
public void run() {
for(int i = 1; i <= to; i++) {
sum += i;
}
}
}
// 출력 결과
1부터 100까지의 합: 5050
wait(), notify() - 스레드 간 협업에 사용
wait()과 notify()는 두 개 이상의 스레드가 공유하는 공유 객체를 컨트롤하기 위해 사용된다.
자신의 작업이 끝나면 상대방의 일시 정지를 풀어주고, 자신은 일시 정지 상태로 만들면서
오류를 방지하고 효과적으로 협업을 할 수 있다.
스레드 A, 스레드 B가 협업하는 상황을 가정해 보면 아래와 같은 플로우로 협업이 진행된다.
- 스레드 A가 공유 객체에 자신의 작업을 완료한다
- 스레드 B와 교대하기 위해 notify()를 호출한다
- 스레드 B가 실행 대기(RUNNABLE) 상태가 되면 스레드 A는 wait()을 호출하여 일시 정지 상태가 된다
- 스레드 B가 공유 객체에 자신의 작업을 완료한다
- 스레드 A와 교대하기 위해 notify()를 호출한다
- 스레드 A가 실행 대기(RUNNABLE) 상태가 되면 스레드 B는 wait()을 호출하여 일시 정지 상태가 된다
- 반복
public class ThreadExample9 {
public static void main(String[] args) {
WorkObject sharedObject = new WorkObject();
ThreadA threadA = new ThreadA(sharedObject);
ThreadB threadB = new ThreadB(sharedObject);
threadA.start();
threadB.start();
}
}
class WorkObject {
public synchronized void methodA() {
System.out.println("ThreadA의 methodA Working");
notify();
try{ wait(); } catch (Exception e) {}
}
public synchronized void methodB() {
System.out.println("ThreadB의 methodB Working");
notify();
try{ wait(); } catch (Exception e) {}
}
}
class ThreadA extends Thread {
private WorkObject workObject;
public ThreadA(WorkObject workObject) {
this.workObject = workObject;
}
public void run() {
for(int i = 0; i < 10; i++){
workObject.methodA();
}
}
}
class ThreadB extends Thread {
private WorkObject workObject;
public ThreadB(WorkObject workObject) {
this.workObject = workObject;
}
public void run() {
for(int i = 0; i < 10; i++) {
workObject.methodB();
}
}
}
//출력 결과
ThreadA의 methodA Working
ThreadB의 methodB Working
ThreadA의 methodA Working
ThreadB의 methodB Working
ThreadA의 methodA Working
ThreadB의 methodB Working
ThreadA의 methodA Working
ThreadB의 methodB Working
ThreadA의 methodA Working
ThreadB의 methodB Working
ThreadA의 methodA Working
ThreadB의 methodB Working
ThreadA의 methodA Working
ThreadB의 methodB Working
ThreadA의 methodA Working
ThreadB의 methodB Working
ThreadA의 methodA Working
ThreadB의 methodB Working
ThreadA의 methodA Working
ThreadB의 methodB Working
wait() 메서드도 호출 시 try{} catch(){} 문으로 감싸준 것을 확인할 수 있다.
'Java+Spring > Java' 카테고리의 다른 글
[Java]배열 내용 출력하기 (Arrays.toString()) (0) | 2022.07.24 |
---|---|
[Java]재귀 함수(Recursive Functions) 예제 (0) | 2022.07.23 |
[Java]재귀 함수(Recursive Functions) (0) | 2022.07.23 |
[Java]스트림 생성, 중간 연산, 최종 연산 (0) | 2022.07.20 |
[Java]스트림(Stream) (0) | 2022.07.20 |
[Java]람다(Lambda), 함수형 인터페이스(Functional Interface), 메서드 레퍼런스(Method Reference) (0) | 2022.07.20 |
- Total
- Today
- Yesterday
- 맛집
- 백준
- Python
- 남미
- 야경
- a6000
- Algorithm
- 기술면접
- 동적계획법
- 유럽여행
- 세계여행
- 면접 준비
- 세계일주
- RX100M5
- spring
- 리스트
- 여행
- 자바
- 지지
- 세모
- Backjoon
- 칼이사
- 유럽
- 중남미
- 스트림
- 스프링
- 알고리즘
- 파이썬
- BOJ
- java
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |