안녕하세요 JAVA 기초 공부 중인 jju_developer입니다~
오늘은 자바의 스레드의 기본 개념 및 제어 상태에 대해 알아보도록 하겠습니다~ :)
JAVA에서의 스레드란?
JVM가 운영체제의 역할을 히며,
자바에는 프로세스가 존재하지 않고 스레드만 존재하며, 자바 스레드는 JVM에 의해 스케줄 되는 실행 단위 코드 블록입니다.
스레드를 구현하는 방법은
Thread클래스를 상속받는 방법과
Runnable인터페이스를 구현하는 방법,
어느 쪽을 선택해도 별 차이는 없지만 Thread클래스를 상속받으면 다른 클래스를 상속받을 수 없기 때문에, Runnable인터페이스를 구현하는 방법이 일반적입니다.
✔스레드의 일반적인 상태는
스레드 객체 생성 New ▶▶ start() ▶▶ 실행 대기 Runnable ⏸ ▶▶실행▶▶ 실행 대기 Runnable ⏸ ▶▶종료 Terminated
스레드는 한 개만 존재할 때도 있지만,
노래를 들으면서 문자를 보내는 것처럼 동시에 작업하는 경우도 많습니다.
두 개의 스레드가 동시에 돌아가면서 하나의 메모리를 사용할 때, 서로 다른 값을 저장하는데 헷갈림이 없도록
하나의 스레드가 실행되는 동안에 다른 스레드는 일시 정지 상태가 되어야 합니다.
*스레드에 일시 정지 상태 도입한 경우
스레드는 동시에 실행하는것처럼 보이지만 사실은 조금씩 실행하고 다른 스레드를 실행하는 작업을 반복하는 것입니다.
그때 기존 실행하던 스레드가 대기하는 상태가 runnable상태이며,
일시 정지하는 상태는 3가지 경우가 있습니다.
1. Blocked : 사용하는 객체의 락이 풀릴 때까지 기다리는 상태, 다른 스레드가 동기화 메서드를 호출한 경우
2. Waiting : 다른 스레드가 통지할 때까지 기다리는 상태, wait() 메소드를 호출한 경우
3. Timed_waiting :sleep() 메소드를 호출한 경우
✔스레드의 상태 제어
CPU에서 스레드1, 스레드2, 스레드3 멀티 스레드를 실행하고자 하는 경우
실행 중인 스레드의 상태를 변경하고 상태 변화를 가져오는 메서드의 종류에 대해 알아보겠습니다~
위 그림에서 취소선을 가진 메서드는 스레드의 안전성을 헤친다고 하여 더 이상 사용하지 않도록 권장된 Deprecated 메서드들입니다.
실행 중인 단계에서 일시정지 상태로 갈 때 자주 쓰는 Sleep() 코드를 보겠습니다.
✔스레드의 Sleep() 코드 예제
※Toolkit 은 소리가 나는 클래스입니다.
import java.awt.Toolkit;
public class SleepExample {
public static void main(String[] args) {
Toolkit toolkit = Toolkit.getDefaultToolkit();
for(int i=0; i<10; i++) {
toolkit.beep();
try {
Thread.sleep(3000);
} catch(InterruptedException e) {
}
}
}
}
toolkit.beep()를 실행하다가 자체적으로 sleep을 보냅니다. => 1초 대기를 하며
해당 코드는 1초마다 beep 사운드를 낼 수 있도록 구현하였습니다.
자기의 main 스레드를 일시 정지 상태로 보내게 됩니다.
1초 동안 runnable 상태가 되며, 실행 대기 상태에서 CPU가 다시 실행대기 상태에 있는 것을 실행하게 됩니다.
✔다른 스레드에게 실행 양보 => yield() 메서드
자기 스레드를 양보하는 Thread
그렇다면 예제를 만들어 보겠습니다.
구현 내용:
스레드 A, 스레드B 가 있을 때
처음에는 round-robin으로 두 개의 스레드가 번갈아 가면서 실행하다가
3초 후에는 스레드 A 가 양보하여 스레드 B의 실행문만 출력되게 됩니다.
3초 후 스레드 A는 더 이상 양보하지 않으며 round-robin 방식으로 번갈아 가면서 실행됩니다.
3초 후에 둘 다 stop이 true가 되면서 스레드 A와 스레드 B가 종료가 됩니다.
<스레드 A 클래스 extends Thread>
public class ThreadA extends Thread {
public boolean stop = false;
public boolean work = true;
public void run() {
while(!stop) {
if(work) {
System.out.println("ThreadA 작업 내용");
} else {
Thread.yield();
}
}
System.out.println("ThreadA 종료");
}
}
<스레드 B 클래스 extends Thread>
public class ThreadB extends Thread {
public boolean stop = false;
public boolean work = true;
public void run() {
while(!stop) {
if(work) {
System.out.println("ThreadB 작업 내용");
} else {
Thread.yield();
}
}
System.out.println("ThreadB 종료");
}
}
<main 클래스>
public class YieldExample {
public static void main(String[] args) {
ThreadA threadA = new ThreadA();
ThreadB threadB = new ThreadB();
threadA.start();
threadB.start(); //처음에는 ABAB 돌다가
try {
Thread.sleep(3000); //3초후에는
} catch (InterruptedException e) {
}
threadA.work = false;//=> 스레드A 가 양보=> 스레드B만 실행됨
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
}
threadA.work = true;//=> 스레드A 가 양보안함-> round-robin 방식으로 번갈아 가면서 실행됨.
try {
Thread.sleep(3000); //3초 후에 둘다 stop이 true가 되면서 ThreadA 종료가 된다.
} catch (InterruptedException e) {
}
threadA.stop = true;
threadB.stop = true;
}
그렇다면
데몬스 레드란 무엇일까요?
✔데몬 스레드
주 스레드의 작업 돕는 보조적인 역할 수행하는 스레드
주 스레드가 종료되면 데몬 스레드는 강제적으로 자동 종료
메인 스레드를 보조하는 역할을 합니다.
데몬 스레드는 보통 어떻게 쓰일까요?
예를 들면 워드 프로세서의 자동 저장 기능이나 미디어 플레이어의 자동으로 다음 곡을 재생시키는 기능들이 있습니다.
그렇다면 데몬스레드는 어떻게 만들까요?
✔데몬 스레드 생성 예제
AutoSaveThread 는 Thread를 상속받는 하위 클래스입니다.
run() 메서드를 재정의하며, 1초 단위로 쉬었다가 작업 내용을 저장하게 구현하였습니다.
public class AutoSaveThread extends Thread {
public void save() {
System.out.println("작업 내용을 저장함.");
}
@Override
public void run() {
while(true) {
try {
Thread.sleep(1000); //1초 정지
} catch (InterruptedException e) {
break;
}
save(); //저장메소드 실행
}
}
public class DaemonExample {
// 주 스레드 == main스레드
public static void main(String[] args) {
AutoSaveThread autoSaveThread = new AutoSaveThread(); //main스레드에서 작업 스레드 생성
autoSaveThread.setDaemon(true); // 작업 스레드를 데몬 스레드로 변경함.
autoSaveThread.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
}
System.out.println("메인 스레드 종료");
}
}
원래는 작업 스레드와 주(메인) 스레드와 관계가 없었지만
주 스레드에서 작업 스레드를 생성 후 그 작업 스레드를 데몬 스레드로 변경한다면,
메인 스레드가 종료되면 작업 스레드도 자동으로 종료가 됩니다. 왜냐면! 데몬 스레드는 보조 역할이기 때문입니다!
만약 데몬스레드로 변경을 하지 않는다면,
메인 스레드가 종료되더라도 작업 스레드는 종료되지 않습니다.
계속 작업을 하게 될 겁니다.
✔스레드풀 이란?
스레드가 많아지게 된다면 병렬 작업을 하게 되는데
예를 들어
Task가 1~10개가 된다고 한다면
CPU의 개수와 상관없이 스레드가 하나라면 10개의 task를 처리하는데 10초가 걸립니다.
이때 스레드를 많이 생성하여 CPU와 병렬 상태로 만들고 작업을 나눈다면 10초가 5초로, 5초가 2.5초로 나뉘게 되어
작업을 더욱 빠르게 처리할 수 있습니다.
하지만 스레드가 늘어나면 메모리 사용량이 증가하고, 스레드 생성 후 스케줄링을 해야 하여 CPU가 바빠지기 때문에
마냥 스레드를 많이 만드는 것이 해결책은 아닙니다.
하여, 적절히 사용해야 하는 것이 관건입니다.
스레드 풀(Thread Pool)
• 작업 처리에 사용되는 스레드를 제한된 개수만큼 미리 생성
• 작업 큐(Queue)에 들어오는 작업들을 하나씩 스레드가 맡아 처리
• 작업 처리가 끝난 스레드는 작업 결과를 애플리케이션으로 전달
• 스레드는 다시 작업 큐에서 새로운 작업을 가져와 처리
쉽게 은행으로 예를 들자면
task 가 고객이고 고객들은 대기를 하고 은행 창고에서 밀려있는 고객들을 빠르게 처리하기 위하여
은행 창구를 늘리는(스레드를 늘리는 방법) 방법과 비어있는 은행 창구에 고객을 배정하는 (비어있는 스레드 n개에게 작업을 할당하는) 방법을 생각하면 이해하기 쉬우실 겁니다.
※Queue라는 작업 구조는 먼저 들어오는 작업을 먼저 처리하는 것입니다. FIFO
※(Stack 메모리의 구조는 LIFO 구조입니다. Last In First Out)
ExecutorService 인터페이스와 Executors 클래스
• 스레드 풀 생성, 사용 - java.util.concurrent 패키지에서 제공
• Executors의 정적 메소드 이용 - ExecutorService 구현 객체 생성
• 스레드 풀 = ExecutorService 객체
• ExecutorService가 동작하는 방식
✔스레드풀 생성 및 종료
newCachedThreadPool()
• int 값이 가질 수 있는 최대값만큼 스레드가 추가되지만, 운영체제의 성능과 상황에 따라 달라진다.
• 1개 이상의 스레드가 추가되었을 경우 60초 동안 추가된 스레드가 아무 작업을 하지 않으면 추가된 스레드를 종료하고 풀에서 제거한다.
newFixedThreadPool(int nThreads) => 내가 만들고 싶은 스레드 개수를 Fix시킵니다.
• 코어 스레드 개수와 최대 스레드 개수가 매개값으로 준 nThreads이다.
• 스레드가 작업 처리하지 않고 놀고 있더라도 스레드 개수가 줄지 않는다.
위 두 가지 메소드도 내부적으로 ThreadPoolExecutor 객체를 생성해서 리턴한다.
첫번쩨 방법: Executors 클래스의 두 가지 메소드
ExecutorService executorService = Executors.newCachedThreadPool();
ExecutorService executorService = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors()
);
두번쩨 방법: 위 메소드를 사용하지 않고 ThreadPoolExecutor 객체를 생성
ExecutorService threadPool = new ThreadPoolExecutor(
3, //코어 스레드 개수
100, //최대 스레드 개수
120L, //놀고 있는 시간
TimeUnit.SECONDS, //놀고 있는 시간 단위
new SynchronousQueue<Runnable>(); //작업 큐
);
다시 정리하자면,
스레드 풀에 스레드를 생성하고 작업을 배정하고 실행합니다.
그렇다면,
ExecutorService 의 객체를 생성하여 Thread Pool을 생성하게 됩니다.
=>
ExecutorService executorService = Executors.newFixedThreadPool(2);
스레드 풀을 실행할때에는 Runnable 인터페이스의 익명구현 객체를 runnable 필드에 넣고
그 runnable을 executorService.execute(runnable);안에 넣어 사용합니다.
해당 코드는 아래와 같습니다.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
public class ExecuteVsSubmitExample {
public static void main(String[] args) throws Exception {
// <Thread Pool 생성>
ExecutorService executorService = Executors.newFixedThreadPool(2);
for (int i = 0; i < 10; i++) {
// <인터페이스 Runnable = 익명 구현객체를 대입했습니다.>
Runnable runnable = new Runnable() {
@Override
public void run() {
// 스레드 총 개수 및 작업 스레드 이름 출력
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executorService;
int poolSize = threadPoolExecutor.getPoolSize();
String threadName = Thread.currentThread().getName();
System.out.println("[총 스레드 개수: " + poolSize + "] 작업 스레드 이름: " + threadName);
// 예외 발생 시킴
int value = Integer.parseInt("삼");
}
};
//Thread Pool 실행
executorService.execute(runnable);
// executorService.submit(runnable);
Thread.sleep(10);
}
executorService.shutdown();
}
}
Executors를 호출할때 newFixedThreadPool(2)를 지정해줌으로 써
executorService의 객체를 생성했습니다.
Task는 익명 구현객체로 구현하여 runnable에 담았습니다.
for문으로 돌면서 나를 돌리고 있는 현재 pool의 size를 return하고 현재 돌고있는 스레드의 이름은 무엇인지를 run하는 메서드를 구현했습니다.
int value = Integer.parseInt("삼"); 으로 예외를 강제로 만들어 executorService.shutdown(); 셧다운을 한 결과 입니다.
처음에 스레드 만들때 2개를 만든다고 선언했는데 스레드가 죽으면서 스레드가 새로운 것이 생성되지만,
스레드의 개수는 2개를 유지하면서 새로운 스레드를 만들고 기존은 종료되고를 반복하는 겁니다.
스레드가 미리 만들어져있으니까, 처리 속도는 빠릅니다.
오늘은 스레드의 컨셉을 이해하셨으면 그것으로 충분합니다.
Java 코드와 DB 사이에 Connection을 통해야 하는데 이 connection은 메모리를 차지하기 때문에
제한사항이 생깁니다.
제한을 걸지 않으면 connection이 무한으로 증가해 DB가 다운될 위험성이 있습니다.
그래서!!
자바와 DB 사이에 connection풀을 두는 겁니다.
스레드 풀과 동일한 컨샙이며, DBCP이라고 합니다.. 데이터베이스 커넥션 풀의 줄입말 입니다.
오늘도 고생하셨습니다.
'주니어 기초 코딩공부' 카테고리의 다른 글
JA14장 람다식 (0) | 2022.12.01 |
---|---|
13장 제네릭 (0) | 2022.12.01 |
01 java 기초_사칙연산 메서드 구현 조건문 예제 (2) | 2022.11.07 |
01 java 기초_Switch Case 조건문 예제 (0) | 2022.11.05 |
01 java 기초_if 조건문 예제 (0) | 2022.11.05 |