쓰레드(Thread)
프로세스
운영체제로부터 작업을 할당받는 작업의 단위이며, 실행 중인 하나의 어플리케이션 (ex : 크롬을 새 창으로 2개를 띄웠다면, 2개의 프로세스가 실행 중이라 말할 수가 있다.
쓰레드
프로세스가 할당 받은 자원을 이용하는 실행의 단위이며, 한 프로세스에서 동작되는 여러 실행 흐름이라고 볼 수가 있다.
1. JVM에 의해 하나의 프로세스가 발생하고 main( ) 안의 실행문 들이 하나의 스레드입니다.
2. main( ) 이외의 또 다른 스레드를 만들려면 Thread 클래스를 상속하거나 Runnable 인터페이스를 구현합니다.
3. 다중 스레드 작업 시에는 각 스레드 끼리 정보를 주고받을 수 있어 처리 과정의 오류를 줄일 수 있습니다.
4. 프로세스끼리는 정보를 주고받을 수 없습니다.
쓰레드 사용이유
1. 메모리 절약
-> OS마다 다르지만, 무슨 작업을 수행하려고 할 때 JVM은 적어도 32~64MB 물리 메모리 점유한다.
근데 스레드는 1MB 이내의 메모리만 점유한다. 그래서 스레드를 '경량 프로세스'라고도 부른다.
2. 프로세스 콘텍스트 스위칭(Context Switching)에 비해 오버헤드 절감
멀티 프로세스로 실행되는 작업을 멀티 스레드로 실행하게 되면 프로세스를 생성하여 자원을 할당하는 과정도 줄어들뿐더러 프로세스를 콘텍스트 스위칭(Context Switching)하는 것보다 오버헤드를 더 줄일 수 있게 된다.
3. 작업들 간 통신 비용 절감
멀티 쓰레드
여러 스레드를 동시에 실행시키는 응용프로그램을 작성하는 기법을 말합니다.(주어진 자원을 극한으로 사용하기 위한 모델)
- > 유튜브 노래를 들으면서 워드 작성을 할 수 있게 해준다.
장점
1. 메모리 공유로 인한 시스템 자원 소모가 줄어 듭니다.
2. 동시에 두가지 이상의 활동을 하는 것이 가능해집니다.
단점
1. 서로 자원을 소모하다가 충돌이 일어날 가능성이 존재합니다.
2. 코딩이 난해해져 버그생성확률이 높아집니다.
Thread의 생명 주기
1. Runnable (준비상태)
스레드가 실행되기 위한 준비단계입니다. CPU를 점유하고 있지않으며 실행(Running 상태)을 하기 위해 대기하고 있는 상태입니다. 코딩 상에서 start( ) 메소드를 호출하면 run( ) 메소드에 설정된 스레드가 Runnable 상태로 진입합니다. “Ready“ 상태라고도 합니다.
2. Running (실행상태)
CPU를 점유하여 실행하고 있는 상태이며 run() 메서드는 JVM만이 호출 가능합니다. Runnable(준비상태)에 있는 여러 스레드 중 우선 순위를 가진 스레드가 결정되면 JVM이 자동으로 run( ) 메소드를 호출하여 스레드가 Running 상태로 진입합니다.
3. Dead (종료상태)
Running 상태에서 스레드가 모두 실행되고 난 후 완료 상태입니다. “Done” 상태라고도 합니다.
4. Blocked (지연 상태)
CPU를 점유권을 상실한 상태입니다. 후에 특정 메서드를 실행시켜 Runnable(준비상태)로 전환합니다.
wait( ) 메소드에 의해 Blocked 상태가 된 스레드는 notify( ) 메소드가 호출되면 Runnable 상태로 갑니다. sleep(시간) 메소드에 의해 Blocked 상태가 된 스레드는 지정된 시간이 지나면 Runnable 상태로 갑니다.
'Thread vs Runnable'선택 기준
Thread 클래스를 확장하는 것이 실행 방법이 미세하게 더 간단하다는 것을 볼 수 있습니다.
하지만 자바에서는 다중 상속을 하용하지 않기 때문에, Thread 클래스를 확장하는 클래스는 다른 클래스를 상속받을 수 없습니다.
반면에 Runnable 인터페이스를 구현했을 경우에는 다른 인터페이스를 구현할 수 있을 뿐만 아니라, 다른 클래스도 상속받을 수 있습니다. 따라서 해당 클래스의 확장성이 중요한 상황이라면 Runnable 인터페이스를 구현하는 것이 더 바람직할 것입니다.
실제로 많은 개발자들이 대부분의 상황에서 Thread 클래스를 확장하기보다는 Runnable 클래스를 구현하는 것을 선호합니다.
Thread 클래스 확장하기
첫번째 방법으로 java.lang.Thread 클래스를 확장할 수 있습니다. Thread 클래스에는 상당히 많은 메소드가 있는데요. 그 중에서 run() 이라는 메소드만 오버라이드해주면 됩니다.
import java.util.Random;
public class MyThread extends Thread {
private static final Random random = new Random();
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println("- " + threadName + " has been started");
int delay = 1000 + random.nextInt(4000);
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("- " + threadName + " has been ended (" + delay + "ms)");
}
}
쓰레드마다 수행 시간을 다르게 하여 현실과 비슷한 상황을 연출하고 싶었습니다. 그래서 Thread.sleep() 메서드를 이용해여 1초 이상 6초 미만의 랜덤 딜레이를 주었습니다. 그리고 각 쓰레드의 시작과 종료 시점에 Thread.currentThread().getName() 메소드를 통해 쓰레드 이름이 출력되도록 하였습니다. 이 쓰레드 이름은 스트링을 인자로 받는 생성자를 통해 객체 생성 시점에 세팅될 것입니다.
Runnable 인터페이스 구현하기
Thread 확장 예제와 동일한 기능을 Runnable 인터페이스를 구현하여 작성해보았습니다. 클래스 이름 뒷 부분이 extends Thread에서 implements Runnable로 바뀐 것 빼고는 동일한 코드입니다. Runnable 인터페이스는 구현할 메소드가 run() 하나 뿐인 함수형 인터페이스입니다. 따라서 Java8에서 도입된 람다를 이용해서 좀 더 깔끔하게 구현할 수도 있습니다.
import java.util.Random;
public class MyRunnable implements Runnable {
private static final Random random = new Random();
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println("- " + threadName + " has been started");
int delay = 1000 + random.nextInt(4000);
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("- " + threadName + " has been ended (" + delay + "ms)");
}
}
실행하기
2가지 방법으로 작성한 클래스의 쓰레드 실행 방법이 약간 다릅니다. 두 가지 클래스 모두 Thread 클래스의 start() 메소드를 통해서 실행시킬 수 있는데요. Thread를 확장한 MyThread 클래스의 경우, 해당 객체에 start() 메소드를 직접 호출할 수 있습니다. 반면에 Runnable을 구현한 MyRunnable 클래스의 경우에는, Runnable 형 인자를 받는 생성자를 통해 별도의 Thread 객체를 생성 후 start() 메소드를 호출해야 합니다.
public class ThreadRunner {
public static void main(String[] args) {
// create thread objects
Thread thread1 = new MyThread();
thread1.setName("Thread #1");
Thread thread2 = new MyThread();
thread2.setName("Thread #2");
// create runnable objects
Runnable runnable1 = new MyRunnable();
Runnable runnable2 = new MyRunnable();
Thread thread3 = new Thread(runnable1);
thread3.setName("Thread #3");
Thread thread4 = new Thread(runnable2);
thread4.setName("Thread #4");
// start all threads
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
아래 실행 결과를 보시면 4개의 쓰레드가 순차적으로 실행되지 않고, 랜덤 딜레이 때문에 끝나는 시간도 재각기인 것을 알 수 있습니다. 그리고 매번 실행할 때마다 딜레이가 달라지기 때문에, 실행 결과가 항상 동일하지 않을 것입니다.
- Thread #2 has been started
- Thread #4 has been started
- Thread #3 has been started
- Thread #1 has been started
- Thread #2 has been ended (2630ms)
- Thread #1 has been ended (3655ms)
- Thread #4 has been ended (4126ms)
- Thread #3 has been ended (4942ms)
Daemon Thread
동일한 프로세스 안에서 다른 스레드의 수행을 돕는 스레드로 다른 스레드를 서비스 해주면서 다른 스레드가 모두 종료되면 자신도 종료되는 스레드이다.
프로그램이 종료되는 것을 막지 않으며 가비지 컬렉터나 메인 스레드가 데몬 스레드입니다.
스레드를 생성하고 시작하기 전에 setDaemon(true)를 설정
public class Thread3 extends Thread
{
public void run()
{
try
{
System.out.println("Daemon Thread Start!");
sleep(10000);
System.out.println("Daemon Thread End!");
}catch(Exception e){ }
}
public static void main(String args[])
{
Thread3 t = new Thread3();
// Thread가 종료되지 않기 때문에 끝나질 않음
t.setDaemon(true);
t.start();
System.out.println("Main Method End!");
}
}
스레드 우선순위
두가지 성질을 이용한다.
동시성(Concurrency)
싱글 코어에서 멀티스레드를 동작시키기 위한 방식으로, 멀티 태스킹을 위해 여러 개의 스레드가 '번갈아가면서' 실행되는 성질을 말한다. 그래서 이는 정말 물리적으로 동시에 동작하는 것은 아니다.
💡 멀티 코어에서 멀티 스레드를 이용하여 이 동시성을 만족할 경우에는 실제 물리적 시간으로 동시에 실행된다.
참고로 멀티 스레드로 이 동시성 이라는 특성을 만족시킬 수 있는 것이지 동시성과 멀티 스레드는 사실 항상 함께할 순 없는 개념이다. 그 반례로 싱글 스레드에서 코루틴(Coroutine)을 이용하여 동시성을 만족할 수 있다.
💡 코루틴(Coroutine)
싱글 스레드에서도 루틴(routine) 이라는 단위(맥락상 함수와 동일)로 루틴간 협력이 가능하며, 동시성 프로그래밍을 지원하고 비동기 처리를 쉽게 도와주는 개념을 말한다. 자세한 설명은 다른분이 설명해주신 이 글을 참고 바란다.
병렬성(Parallelism)
멀티 코어에서 멀티스레드를 동작시키는 방식으로, 한 개 이상의 스레드를 포함하는 각 코어들이 '동시에' 실행되는 성질을 말한다.
💡 동시성(Concurrency)의 '동시'와 병렬성의 설명에 있는 '동시' 때문에 헷갈릴 수 있으니 주의하기 바란다.
각 용어의 정확한 의미를 분석한 좋은 글이 있으니 참고 바란다.
보통 동시성에 비해서 병렬성은 각 코어들이 동시에 실행되므로 CPU의 유휴 시간(idle time)이 줄어들어 성능이 좋다. 그러나 CPU 수보다 처리해야 할 프로세스나 스레드 수가 많다면 CPU를 사용하기 전까지 대기가 발생한다.
자바의 스레드 스케줄링
우선 순위(Priority) 방식과 순환 할당(Round-Robin) 방식을 사용
- 우선 순위 방식(코드로 제어 가능) : 우선 순위가 높은 스레드가 실행 상태를 더 많이 가지도록 스케쥴링
- 순환 할당 방식(코드로 제어할 수 없음) : 시간 할당량(Time Slice)을 정해서 하나의 스레드를 정해진 시간만큼 실행하는 방식
https://ict-nroo.tistory.com/41 디테일한 내용
ref
https://post.naver.com/viewer/postView.nhn?volumeNo=7852076&memberNo=30800755 멀티스레드 쉬운 예제
https://beststar-1.tistory.com/26
https://beststar-1.tistory.com/6?category=976609
https://www.daleseo.com/java-thread-runnable/#%EC%8B%A4%ED%96%89%ED%95%B4%EB%B3%B4%EA%B8%B0
https://coding-factory.tistory.com/279
https://raccoonjy.tistory.com/15
https://sas-study.tistory.com/220?category=769494
'Java 공부' 카테고리의 다른 글
자바의 데이터 타입(Primitive type, Reference type) (0) | 2022.02.28 |
---|---|
Java 컴파일 과정 (0) | 2022.02.28 |
동기화(Synchronized ) vs 비동기화(Asynchronized) / 블로킹(blocking)과 논블로킹(non-blocking) (0) | 2021.11.14 |
Java 객체 직렬화(Serialization) 와 역직렬화(Deserialization) (0) | 2021.11.14 |
가비지 컬렉션(Garbage Collection)이란 (0) | 2021.11.13 |