Thread 란?
Process: 데이터와 메모리, 스레드 등의 자원에 메모리 공간을 할당받아 실행 중인 프로그램
Thread : 프로세스 내에서 실제로 작업을 실행하는 여러 작업의 한 단위
- 스레드는 독립된 메모리 영역인 Stack을 갖는다.
- 스레드는 다른 스레드와 스택 메모리를 공유할 수 없다
- 프로세스의 힙은 속한 모든 Thread가 공유할 수 있다.
- 이 자원을 공유할 수 있기에 같은 자원을 두고 동시에 여러 가지 일을 수행할 수 있다.
Thread 구현 방법
- Runnable 인터페이스 구현
- Thread 클래스 상속
다중 상속이 불가능하므로 보통은 Runnable 인터페이스를 구현하는 방법으로 스레드를 생성한다.
kotlin에선 thread()
를 통해 Thread 클래스를 상속하고 있어,
thread 객체를 리턴하여 스레드를 생성할 수 있다.
fun main() {
println("${Thread.currentThread().name }: run thread")
thread() { println("${Thread.currentThread().name }: running") }
thread() { println("${Thread.currentThread().name }: running") }
thread() { println("${Thread.currentThread().name }: running") }
/*
출력
pool-1-thread-1: run thread
Thread-0: running
Thread-1: running
Thread-2: running
*/
}
동시성
- 동시에 여러 작업을 조금씩 나누어서 번갈아가며 수행하는
- 싱글 코어에서 멀티 스레드를 동작시키기 위한 방식,
멀티 태스킹을 위해 여러 개의 스레드가 번갈아가며 실행되는 것
문맥 교환(Context Switching)
멀티 스레드는 하나의 프로세스 내에서 둘 이상의 스레드가 동시에 작업을 수행하는 것을 의미합니다.
이때 각 스레드가 교체될 때 스레드 간의 문맥 교환(Context Switching)이 발생합니다. 이는 현재까지의 작업 상태나 다음 작업에 필요한 각종 데이터를 저장하고 읽어오는 것을 의미합니다.
다시말해 문맥 교환은 교체할 때까지의 작업한 내용을 저장하고 다른 스레드의 작업을 불러와 다시 작업할 수 있도록 준비하는 것인데요, 이 스레드가 교체될 때 생기는 작업 즉, 문맥 교환이 많으면 많을 수록 이 시간 동안 작업을 할 수 없으므로 컨텍스트 스위칭의 비용이 비싸다는 것입니다.
동시성 이슈
Thread-safe : 여러 스레드로부터 동시에 접근이 이루어져도 프로그램의 실행에 문제가 없음을 뜻한다.
스레드 안정성이 깨지는 상황을 통해 Thread-safe에 대해 자세히 알아보겠습니다. 간단한 예제로 유저가 사이트를 방문했을 때 본래 방문수에 1을 더한 값을 저장하고, 여러 사용자가 동시에 접근할 것이므로 멀티 스레드 환경에서 동작한다고 가정하겠습니다.
100명의 유저가 100번 방문했을 때
fun main() {
val visit = Visit()
for (i in 1..100) {
thread() {
for (j in 1..100) {
println(visit.view())
}
}.start()
}
}
class Visit {
private var count: Int = 0
fun view(): Int {
return ++count
}
}
100명의 100번 방문했으니 10000번의 방문수를 기대할 수 있다.
하지만 실제 값은 9999인데, 이 이유는 동시성 이슈가 존재하기 때문이다.
프로그램 상에서 실제로 하는 일은 다음과 같다
count
를 조회한다count
값을 +1 더하고 저장한다
여러 스레드에서 동시에 count 변수에 접근하여 동시에 1번 동작을 진행해 같은 count 값을 조회하고 두 개의 스레드가 1을 더하는 로직을 실행해도 2가 더해지는 것이 아닌 1만 더해지는 동작이 발생할 수 있다.
다시말해 프로세스가 진행 중일 때 1~100 스레드 중 45번 스레드와 46번 스레드가 count를 조회할 때 두 스레드가 count의 값인 45를 똑같이 조회를 한다면, 이 두 스레드가 45에 1을 더해도 결국 46으로 저장된다.
따라서 1~100번까지 순차적으로 count를 조회하고 +1을 더한 후 저장하지 않는 이상. 동시에 count를 조회하는 스레드가 생길 수 있으므로 이 동시성 이슈를 해결하여 Thread-safe한 프로그램을 작성해야 한다.
암시적 Lock (synchronized)
- 가장 간단한 방법이다.
- 하나의 스레드만 접근할 수 있도록 Lock을 거는 방식
메서드는 @Synchronized
어노테이션을, synchronized() {}
으로 코드 부분을 Lock 할 수도 있다.
@Synchronized
fun outSynchronizedView(): Int {
return ++count
}
fun inSynchronizedView(): Int {
synchronized(this) { return ++count }
}
명시적 Lock
명시적 Lock은 Java에서 synchronized 키워드 없이 Lock을 하는 것이지만, kotlin에서는
synchronized() {
//Lock을 걸 작업
}
위의 형태로 특정 부분을 Lock 할 수 있기 kotlin에선 사용하지 않아도 되지만,
Java에선 ReentrantLock 클래스를 통해 원하는 단위로 Lock을 걸 수 있다.
```kotlin
fun main() {
val visit = Visit()
for (i in 1..100) {
thread() {
for (j in 1..100) {
visit.getLock().lock()
println(visit.view())
visit.getLock().unlock()
}
}.start()
}
}
class Visit {
private var count: Int = 0
private val lock: Lock = ReentrantLock()
fun view(): Int {
return ++count
}
fun getLock(): Lock {
return lock
}
}
'Java' 카테고리의 다른 글
Test Fixture 란 (0) | 2023.04.21 |
---|---|
추상클래스 무엇이고, 왜, 어떻게 사용할까? (0) | 2023.03.24 |
[Java] 가비지 컬렉션 (Garbage Collection GC) (0) | 2022.08.13 |
추상 클래스 vs 인터페이스 (0) | 2022.04.12 |