Thread에 대해서 오래전에 정리했던 적이 있다. 하지만 일을 하다가.. 아직도 Thread에 대해서 잘모른다는 느낌이 들어서 더 자세하게 다시한번 정리를 하고자 한다.
https://codingstudy95.tistory.com/67
스레드
사전적 의미로 한 가닥의 실이라는 뜻으로 한가지 작업을 실행하기 위해 순차적으로 실행할 코드를 실처럼 이어놓았다고 해서 유래된 이름이다. 하나의 스레드는 하나의 코드 실행 흐름이므로
codingstudy95.tistory.com
스레드란 무엇인가!!?
자바에서 Thread(스레드)는 프로그램 내에서 동시에 실행되는 작업의 단위를 의미한다. 회사에서 여러 사람이 각자의 다른 일을 동시에 하는 것과 마찬가지로, 스레드는 하나의 프로그램 안에서 여러 작업을 동시에 처리할 수 있도록 해준다.
(예: 데이터 처리, 사용자 요청 처리, 파일 입출력 등)
- 단일 스레딩 : 한 사람이 모든 일을 순서대로 처리하는 것 -> 모든 작업이 순차적으로 처리되므로, 하나의 작업이 오래 걸리면 다른 작업들도 지연된다.
- 멀티스레딩 : 여러 사람이 동시에 각자 일을 분담해서 처리하는 것 -> 여러 스레드가 동시에 작업을 처리하여, 한 작업이 늦어도 다른 작업은 계속 진행될 수 있다.
그럼 자바에서는 왜 멀티스레딩이 필요할까?
식당에서 한 사람이 모든 요리를 한다면 주문이 많은 경우 오래 걸리겠지만, 여러 요리사가 동시에 각자 다른 요리를 준비하면, 음식이 빨리 준비되는것과 비슷하다.
사용자 경험 향상
한 번에 한 작업만 처리한다면, 사용자가 어떤 요청을 할 때마다 기다려야 한다. 하지만 멀티스레딩을 사용하면, 여러 작업이 동시에 처리되어 응답 속도가 빨라지고 사용자 경험이 개선된다.
자원 활용의 극대화
컴퓨터는 여러 CPU 코어를 가지고 있는데, 멀티스레딩을 통해 이 코어들을 동시에 사용할 수 있다. 즉, 컴퓨터의 능력을 최대한 활용하여 더 빠르고 효율적으로 작업할 수 있다.
자바에서 스레드를 만들어보자 자바에서는 스레드를 만드는 방법이 두 가지 있다.
1. Thread 클래스 상속하기
// MyThread.kt
class MyThread : Thread() {
// run 메서드를 재정의하여 스레드가 실행할 작업을 정의합니다.
override fun run() {
// 스레드가 실행될 때, "Hello from MyThread!"를 5번 출력합니다.
for (i in 1..5) {
println("Hello from MyThread! - $i")
// 잠깐 멈추는 시간 (1000밀리초 = 1초)
Thread.sleep(1000)
}
}
}
fun main() {
// MyThread 클래스의 인스턴스를 생성하고, start()를 호출하면 스레드가 실행됩니다.
val thread = MyThread()
thread.start()
}
- MyThread는 Thread 클래스를 상속받아 만든 새로운 스레드 클래스이다.
- run() 메서드 안에 스레드가 해야 할 일을 작성한다.
- thread.start() 를 호출하면, 새로운 스레드가 시작되어 run() 메서드의 내용이 실행된다.
2. Runnable 인터페이스 구현하기
또 다른 방법은 Runnable 인터페이스를 구현하는 것이다. 이 방법은 클래스 상속의 제약을 피할 수 있다는 장점이 있다.
// MyRunnable.kt
class MyRunnable : Runnable {
override fun run() {
// 스레드가 실행될 때, "Hello from MyRunnable!"를 5번 출력합니다.
for (i in 1..5) {
println("Hello from MyRunnable! - $i")
Thread.sleep(1000)
}
}
}
fun main() {
// Runnable 인터페이스를 구현한 MyRunnable 인스턴스를 Thread에 전달하여 실행합니다.
val runnable = MyRunnable()
val thread = Thread(runnable)
thread.start()
}
- MyRunnable 은 Runnable 인터페이스를 구현하여, run() 메서드 안에 작업 내용을 정의한다.
- 이 객체를 Thread 생성자에 넘겨주고, start() 를 호출하면 스레드가 실행된다.
스레드의 생명주기와 상태
자바 스레드는 여러 상태를 가진다. 각 상태는 스레드가 어떤 작업을 하고 있는지를 나타낸다.
- New: 스레드가 생성되었지만 아직 실행되지 않은 상태
- Runnable: 실행 중이거나 실행 준비가 된 상태
- Blocked/Waiting: 다른 스레드에 의해 잠시 멈춰 있는 상태
- Timed Waiting: 일정 시간 후에 다시 실행될 상태
- Terminated: 스레드의 작업이 모두 끝난 상태
한 사람이 일어나서 출근 준비를 하는 것처럼, 스레드도 만들어진 후 실행 준비, 작업 중, 대기, 그리고 작업 종료의 과정을 거친다.
근데 문제가 발생할 수 있다. 멀티스레딩에서 여러 스레드가 동시에 같은 데이터를 수정하려 할 때 문제가 발생할 수 있다.
이를 경쟁 조건 (Race Condition) 이라고 하며, 이를 해결하기 위해 동기화(Synchronization) 를 사용한다.
예를 들어 콘서트 티켓을 예매할때 한 좌석을 동시에 두명이 예매하려고 할때 좌석을 누구에게 할당해야 할까? 이런 문제를 해결하려면, 한 사람이 작업을 끝낼 때까지 기다리도록 해야 한다.
synchronized 키워드 사용 예제
class Counter {
var count: Int = 0
// synchronized를 사용해 여러 스레드가 동시에 count를 수정하지 않도록 보호합니다.
@Synchronized
fun increment() {
count++
}
}
fun main() {
val counter = Counter()
val threads = mutableListOf<Thread>()
// 10개의 스레드를 생성하여 동시에 increment()를 호출합니다.
for (i in 1..10) {
val thread = Thread {
for (j in 1..1000) {
counter.increment()
}
}
threads.add(thread)
thread.start()
}
// 모든 스레드가 끝날 때까지 대기합니다.
threads.forEach { it.join() }
// 10개의 스레드가 각각 1000번씩 increment했으므로, 최종 결과는 10000이어야 합니다.
println("최종 count: ${counter.count}") // 결과: 10000
}
- @Synchronized 어노테이션을 사용해 increment() 메서드에 동시에 접근하는 것을 막는다.
- 여러 스레드가 동시에 increment() 를 호출해도, 동기화 덕분에 안전하게 실행된다.
내가 회사에서 일하면서 실제로 겪은 스레드 문제가 있다.
경쟁 조건과 데드락
- 경쟁조건 : 여러 스레드가 동시에 데이터를 수정할 때 예상치 못한 결과가 발생하는것
- 데드락(DeadLock) : 두 스레드가 서로 상대방이 가진 자원을 기다리면서 무한 대기에 빠지는 상황이다.
동기화 블록이나 Lock 객체를 사용해, 자원에 접근하는 순서를 잘 관리해야 한다.
실제로 데드락에 빠지는 로직을 개발하여 정말..난리 난리가 났었던...일이...후..
스레드 풀 (Thread Pool)
매번 새로운 스레드를 생성하는 대신, 미리 일정 개수의 스레드를 만들어 두고 재사용하는 방법. 스레드의 생성 비용을 줄이고, 시스템 자원을 효율적으로 사용할 수 있다.
import java.util.concurrent.Executors
fun main() {
// 고정 크기의 스레드 풀 생성 (3개의 스레드)
val executor = Executors.newFixedThreadPool(3)
// 10개의 작업을 스레드 풀에 제출합니다.
for (i in 1..10) {
executor.submit {
println("작업 $i 시작: ${Thread.currentThread().name}")
Thread.sleep(1000)
println("작업 $i 완료: ${Thread.currentThread().name}")
}
}
// 스레드 풀 종료
executor.shutdown()
}
- Executors.newFixedThreadPool(3)를 통해 3개의 스레드로 이루어진 풀을 생성한다.
- 여러 작업이 동시에 제출되지만, 동시에 최대 3개 작업만 실행되고 나머지는 대기한다.
사용자 관점에서 한번 생각해보자
대부분의 웹 애플리케이션에서는 사용자가 데이터를 수정하고 저장을 누르면, 스레드 풀(Thread Pool) 에서 미리 만들어진 스레드 중 하나가 해당 요청을 처리한다.
즉, 사용자가 콘텐츠를 수정하고 저장 버튼을 클릭하면
- 웹 서버는 이미 생성되어 대기 중인 스레드 풀에서 하나의 스레드를 할당한다
- 해당 스레드가 수정 작업 로직을 실행하고
- 작업이 완료되면 그 스레드는 스레드 풀로 돌아가 재사용된다.
이제 스레드에 대해 확실히 알게 된 것 같다.
'Back-End > Java' 카테고리의 다른 글
try-catch (0) | 2024.07.06 |
---|---|
스레드 (0) | 2024.07.05 |
프로세스 (0) | 2024.07.04 |
Java HTTP 통신 (1) | 2024.06.12 |
Exception과 Transaction (0) | 2024.02.05 |