Skip to content

코루틴 컨텍스트와 디스패처

코루틴은 항상 Kotlin 표준 라이브러리에 정의된 CoroutineContext 타입의 값으로 표현되는 특정 컨텍스트에서 실행됩니다.

코루틴 컨텍스트는 다양한 요소들의 집합입니다. 주요 요소는 이전에 다루었던 코루틴의 Job과 이 섹션에서 다룰 디스패처입니다.

디스패처와 스레드

코루틴 컨텍스트는 해당 코루틴이 실행에 어떤 스레드를 사용할지 결정하는 코루틴 디스패처 (CoroutineDispatcher 참조)를 포함합니다. 코루틴 디스패처는 코루틴 실행을 특정 스레드로 제한하거나, 스레드 풀로 디스패치하거나, 제한 없이 실행되도록 할 수 있습니다.

launchasync와 같은 모든 코루틴 빌더는 새 코루틴의 디스패처 및 다른 컨텍스트 요소를 명시적으로 지정하는 데 사용될 수 있는 선택적 CoroutineContext 매개변수를 받습니다.

다음 예제를 실행해 보세요:

kotlin
import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    launch { // context of the parent, main runBlocking coroutine
        println("main runBlocking      : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Unconfined) { // not confined -- will work with main thread
        println("Unconfined            : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Default) { // will get dispatched to DefaultDispatcher 
        println("Default               : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(newSingleThreadContext("MyOwnThread")) { // will get its own new thread
        println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
    }
}

전체 코드는 여기에서 확인할 수 있습니다.

다음 출력을 생성합니다 (순서는 다를 수 있습니다):

text
Unconfined            : I'm working in thread main
Default               : I'm working in thread DefaultDispatcher-worker-1
newSingleThreadContext: I'm working in thread MyOwnThread
main runBlocking      : I'm working in thread main

launch { ... }가 매개변수 없이 사용될 때, 해당 코루틴은 시작된 CoroutineScope로부터 컨텍스트(및 디스패처)를 상속받습니다. 이 경우, main 스레드에서 실행되는 메인 runBlocking 코루틴의 컨텍스트를 상속받습니다.

Dispatchers.Unconfinedmain 스레드에서 실행되는 것처럼 보이는 특별한 디스패처이지만, 사실 나중에 설명할 다른 메커니즘입니다.

기본 디스패처는 스코프에서 다른 디스패처가 명시적으로 지정되지 않았을 때 사용됩니다. 이는 Dispatchers.Default로 표현되며, 공유 백그라운드 스레드 풀을 사용합니다.

newSingleThreadContext는 코루틴이 실행될 스레드를 생성합니다. 전용 스레드는 매우 값비싼 리소스입니다. 실제 애플리케이션에서는 더 이상 필요하지 않을 때 close 함수를 사용하여 해제하거나, 최상위 변수에 저장하여 애플리케이션 전체에서 재사용해야 합니다.

언컨파인드 vs 컨파인드 디스패처

Dispatchers.Unconfined 코루틴 디스패처는 호출자 스레드에서 코루틴을 시작하지만, 첫 번째 중단점까지만 그렇습니다. 중단 후에는 호출된 중단 함수에 의해 완전히 결정되는 스레드에서 코루틴을 재개합니다. 언컨파인드 디스패처는 CPU 시간을 소모하지 않거나 특정 스레드에 제한된 공유 데이터(예: UI)를 업데이트하지 않는 코루틴에 적합합니다.

반면에, 디스패처는 기본적으로 외부 CoroutineScope로부터 상속됩니다. 특히 runBlocking 코루틴의 기본 디스패처는 호출자 스레드에 제한되므로, 이를 상속하면 예측 가능한 FIFO 스케줄링으로 이 스레드에 실행을 제한하는 효과가 있습니다.

kotlin
import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    launch(Dispatchers.Unconfined) { // not confined -- will work with main thread
        println("Unconfined      : I'm working in thread ${Thread.currentThread().name}")
        delay(500)
        println("Unconfined      : After delay in thread ${Thread.currentThread().name}")
    }
    launch { // context of the parent, main runBlocking coroutine
        println("main runBlocking: I'm working in thread ${Thread.currentThread().name}")
        delay(1000)
        println("main runBlocking: After delay in thread ${Thread.currentThread().name}")
    }
}

전체 코드는 여기에서 확인할 수 있습니다.

출력 결과:

text
Unconfined      : I'm working in thread main
main runBlocking: I'm working in thread main
Unconfined      : After delay in thread kotlinx.coroutines.DefaultExecutor
main runBlocking: After delay in thread main

따라서 runBlocking {...}으로부터 컨텍스트를 상속받은 코루틴은 main 스레드에서 계속 실행되는 반면, 언컨파인드 코루틴은 delay 함수가 사용하는 기본 executor 스레드에서 재개됩니다.

언컨파인드 디스패처는 코루틴의 나중 실행을 위한 디스패칭이 필요 없거나, 코루틴 내의 일부 작업이 즉시 수행되어야 하기 때문에 바람직하지 않은 부작용을 일으킬 수 있는 특정 코너 케이스에서 유용할 수 있는 고급 메커니즘입니다. 언컨파인드 디스패처는 일반적인 코드에서 사용해서는 안 됩니다.

코루틴 및 스레드 디버깅

코루틴은 한 스레드에서 중단되고 다른 스레드에서 재개될 수 있습니다. 단일 스레드 디스패처를 사용하더라도 특별한 도구가 없다면 코루틴이 무엇을, 어디서, 언제 수행했는지 파악하기 어려울 수 있습니다.

IDEA로 디버깅하기

Kotlin 플러그인의 코루틴 디버거는 IntelliJ IDEA에서 코루틴 디버깅을 단순화합니다.

디버깅은 kotlinx-coroutines-core 버전 1.3.8 이상에서 작동합니다.

Debug 도구 창에는 Coroutines 탭이 있습니다. 이 탭에서는 현재 실행 중인 코루틴과 중단된 코루틴에 대한 정보를 찾을 수 있습니다. 코루틴은 실행 중인 디스패처별로 그룹화됩니다.

Debugging coroutines

코루틴 디버거를 사용하면 다음을 수행할 수 있습니다.

  • 각 코루틴의 상태를 확인합니다.
  • 실행 중인 코루틴과 중단된 코루틴 모두의 로컬 및 캡처된 변수 값을 확인합니다.
  • 전체 코루틴 생성 스택과 코루틴 내부의 호출 스택을 확인합니다. 이 스택에는 표준 디버깅 중에는 손실될 수 있는 변수 값을 포함한 모든 프레임이 포함됩니다.
  • 각 코루틴의 상태와 스택이 포함된 전체 보고서를 얻습니다. 이를 얻으려면 Coroutines 탭 내부를 마우스 오른쪽 버튼으로 클릭한 다음 Get Coroutines Dump를 클릭합니다.

코루틴 디버깅을 시작하려면 중단점을 설정하고 애플리케이션을 디버그 모드로 실행하기만 하면 됩니다.

코루틴 디버깅에 대한 자세한 내용은 튜토리얼에서 알아보세요.

로깅을 사용한 디버깅

코루틴 디버거 없이 스레드를 사용하는 애플리케이션을 디버깅하는 또 다른 방법은 각 로그 문에서 로그 파일에 스레드 이름을 출력하는 것입니다. 이 기능은 로깅 프레임워크에서 보편적으로 지원됩니다. 코루틴을 사용할 때, 스레드 이름만으로는 많은 컨텍스트를 제공하지 않으므로, kotlinx.coroutines는 이를 더 쉽게 만들기 위한 디버깅 기능을 포함합니다.

다음 코드를 -Dkotlinx.coroutines.debug JVM 옵션과 함께 실행하세요:

kotlin
import kotlinx.coroutines.*

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

fun main() = runBlocking<Unit> {
    val a = async {
        log("I'm computing a piece of the answer")
        6
    }
    val b = async {
        log("I'm computing another piece of the answer")
        7
    }
    log("The answer is ${a.await() * b.await()}")
}

전체 코드는 여기에서 확인할 수 있습니다.

세 개의 코루틴이 있습니다. runBlocking 내부의 메인 코루틴(#1)과 지연된 값 a(#2) 및 b(#3)를 계산하는 두 개의 코루틴입니다. 이들은 모두 runBlocking의 컨텍스트에서 실행되며 메인 스레드에 제한됩니다. 이 코드의 출력은 다음과 같습니다:

text
[main @coroutine#2] I'm computing a piece of the answer
[main @coroutine#3] I'm computing another piece of the answer
[main @coroutine#1] The answer is 42

log 함수는 대괄호 안에 스레드 이름을 출력하며, 현재 실행 중인 코루틴의 식별자가 덧붙여진 main 스레드임을 확인할 수 있습니다. 디버깅 모드가 켜져 있을 때 이 식별자는 생성된 모든 코루틴에 순차적으로 할당됩니다.

디버깅 모드는 JVM이 -ea 옵션으로 실행될 때도 켜집니다. 디버깅 기능에 대한 자세한 내용은 DEBUG_PROPERTY_NAME 속성 문서를 참조하십시오.

스레드 간 점프

다음 코드를 -Dkotlinx.coroutines.debug JVM 옵션과 함께 실행하세요(디버그 참조):

kotlin
import kotlinx.coroutines.*

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

fun main() {
    newSingleThreadContext("Ctx1").use { ctx1 ->
        newSingleThreadContext("Ctx2").use { ctx2 ->
            runBlocking(ctx1) {
                log("Started in ctx1")
                withContext(ctx2) {
                    log("Working in ctx2")
                }
                log("Back to ctx1")
            }
        }
    }
}

전체 코드는 여기에서 확인할 수 있습니다.

위 예제는 코루틴 사용의 새로운 기술을 보여줍니다.

첫 번째 기술은 지정된 컨텍스트와 함께 runBlocking을 사용하는 방법을 보여줍니다. 두 번째 기술은 withContext를 호출하는 것을 포함하며, 이는 현재 코루틴을 중단하고 새 컨텍스트로 전환할 수 있습니다. 단, 새 컨텍스트가 기존 컨텍스트와 다른 경우에만 해당됩니다. 특히 다른 CoroutineDispatcher를 지정하면 추가적인 디스패칭이 필요합니다. 즉, 해당 블록은 새 디스패처에서 스케줄링되고, 완료되면 실행이 원래 디스패처로 돌아옵니다.

결과적으로 위 코드의 출력은 다음과 같습니다:

text
[Ctx1 @coroutine#1] Started in ctx1
[Ctx2 @coroutine#1] Working in ctx2
[Ctx1 @coroutine#1] Back to ctx1

위 예제는 Kotlin 표준 라이브러리의 use 함수를 사용하여 newSingleThreadContext로 생성된 스레드 리소스가 더 이상 필요하지 않을 때 적절하게 해제하는 방법을 보여줍니다.

컨텍스트 내의 Job

코루틴의 Job은 해당 컨텍스트의 일부이며, coroutineContext[Job] 표현식을 사용하여 컨텍스트에서 검색할 수 있습니다.

kotlin
import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    println("My job is ${coroutineContext[Job]}")
}

전체 코드는 여기에서 확인할 수 있습니다.

디버그 모드에서는 다음과 같은 출력을 생성합니다:

My job is "coroutine#1":BlockingCoroutine{Active}@6d311334

CoroutineScopeisActivecoroutineContext[Job]?.isActive == true에 대한 편리한 단축어일 뿐이라는 점에 유의하세요.

코루틴의 자식

한 코루틴의 CoroutineScope 내에서 다른 코루틴이 시작될 때, 새로운 코루틴은 CoroutineScope.coroutineContext를 통해 컨텍스트를 상속받고, 새로운 코루틴의 Job은 부모 코루틴 Job의 _자식_이 됩니다. 부모 코루틴이 취소되면 모든 자식 코루틴도 재귀적으로 취소됩니다.

그러나 이 부모-자식 관계는 다음 두 가지 방법 중 하나로 명시적으로 재정의될 수 있습니다.

  1. 코루틴을 시작할 때 다른 스코프가 명시적으로 지정되면(예: GlobalScope.launch), 부모 스코프로부터 Job을 상속받지 않습니다.
  2. 새로운 코루틴의 컨텍스트로 다른 Job 객체가 전달되면(아래 예제 참조), 이는 부모 스코프의 Job을 재정의합니다.

두 경우 모두, 시작된 코루틴은 시작된 스코프에 묶이지 않고 독립적으로 작동합니다.

kotlin
import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    // launch a coroutine to process some kind of incoming request
    val request = launch {
        // it spawns two other jobs
        launch(Job()) { 
            println("job1: I run in my own Job and execute independently!")
            delay(1000)
            println("job1: I am not affected by cancellation of the request")
        }
        // and the other inherits the parent context
        launch {
            delay(100)
            println("job2: I am a child of the request coroutine")
            delay(1000)
            println("job2: I will not execute this line if my parent request is cancelled")
        }
    }
    delay(500)
    request.cancel() // cancel processing of the request
    println("main: Who has survived request cancellation?")
    delay(1000) // delay the main thread for a second to see what happens
}

전체 코드는 여기에서 확인할 수 있습니다.

이 코드의 출력은 다음과 같습니다:

text
job1: I run in my own Job and execute independently!
job2: I am a child of the request coroutine
main: Who has survived request cancellation?
job1: I am not affected by cancellation of the request

부모의 책임

부모 코루틴은 항상 모든 자식의 완료를 기다립니다. 부모는 시작한 모든 자식을 명시적으로 추적할 필요가 없으며, 마지막에 Job.join을 사용하여 자식을 기다릴 필요도 없습니다.

kotlin
import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    // launch a coroutine to process some kind of incoming request
    val request = launch {
        repeat(3) { i -> // launch a few children jobs
            launch  {
                delay((i + 1) * 200L) // variable delay 200ms, 400ms, 600ms
                println("Coroutine $i is done")
            }
        }
        println("request: I'm done and I don't explicitly join my children that are still active")
    }
    request.join() // wait for completion of the request, including all its children
    println("Now processing of the request is complete")
}

전체 코드는 여기에서 확인할 수 있습니다.

결과는 다음과 같습니다:

text
request: I'm done and I don't explicitly join my children that are still active
Coroutine 0 is done
Coroutine 1 is done
Coroutine 2 is done
Now processing of the request is complete

디버깅을 위한 코루틴 이름 지정

코루틴이 자주 로깅하고 동일한 코루틴에서 오는 로그 기록을 연관시키기만 하면 될 때에는 자동으로 할당된 ID가 유용합니다. 그러나 코루틴이 특정 요청을 처리하거나 특정 백그라운드 작업을 수행하는 데 사용될 때에는 디버깅 목적으로 명시적으로 이름을 지정하는 것이 좋습니다. CoroutineName 컨텍스트 요소는 스레드 이름과 동일한 목적을 가집니다. 디버깅 모드가 켜져 있을 때 이 코루틴을 실행하는 스레드 이름에 포함됩니다.

다음 예제는 이 개념을 보여줍니다:

kotlin
import kotlinx.coroutines.*

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

fun main() = runBlocking(CoroutineName("main")) {
    log("Started main coroutine")
    // run two background value computations
    val v1 = async(CoroutineName("v1coroutine")) {
        delay(500)
        log("Computing v1")
        6
    }
    val v2 = async(CoroutineName("v2coroutine")) {
        delay(1000)
        log("Computing v2")
        7
    }
    log("The answer for v1 * v2 = ${v1.await() * v2.await()}")
}

전체 코드는 여기에서 확인할 수 있습니다.

-Dkotlinx.coroutines.debug JVM 옵션으로 생성되는 출력은 다음과 유사합니다:

text
[main @main#1] Started main coroutine
[main @v1coroutine#2] Computing v1
[main @v2coroutine#3] Computing v2
[main @main#1] The answer for v1 * v2 = 42

컨텍스트 요소 결합

때때로 코루틴 컨텍스트에 여러 요소를 정의해야 할 필요가 있습니다. 이를 위해 + 연산자를 사용할 수 있습니다. 예를 들어, 명시적으로 지정된 디스패처와 명시적으로 지정된 이름을 동시에 사용하여 코루틴을 시작할 수 있습니다.

kotlin
import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    launch(Dispatchers.Default + CoroutineName("test")) {
        println("I'm working in thread ${Thread.currentThread().name}")
    }
}

전체 코드는 여기에서 확인할 수 있습니다.

-Dkotlinx.coroutines.debug JVM 옵션으로 이 코드의 출력은 다음과 같습니다:

text
I'm working in thread DefaultDispatcher-worker-1 @test#2

코루틴 스코프

컨텍스트, 자식, Job에 대한 지식을 통합해 봅시다. 애플리케이션에 수명 주기를 가진 객체가 있지만, 해당 객체가 코루틴은 아니라고 가정해 봅시다. 예를 들어, Android 애플리케이션을 작성 중이며, Android 액티비티의 컨텍스트에서 다양한 코루틴을 시작하여 데이터 가져오기 및 업데이트, 애니메이션 수행 등 비동기 작업을 수행한다고 가정해 봅시다. 이러한 코루틴은 메모리 누수를 방지하기 위해 액티비티가 파괴될 때 취소되어야 합니다.

물론 액티비티와 코루틴의 수명 주기를 연결하기 위해 컨텍스트와 Job을 수동으로 조작할 수 있지만, kotlinx.coroutines는 이를 캡슐화하는 추상화(CoroutineScope)를 제공합니다. 모든 코루틴 빌더가 그 위에 확장 함수로 선언되어 있으므로, 코루틴 스코프에 이미 익숙하실 것입니다.

우리는 액티비티의 수명 주기에 묶인 CoroutineScope 인스턴스를 생성하여 코루틴의 수명 주기를 관리합니다. CoroutineScope 인스턴스는 CoroutineScope() 또는 MainScope() 팩토리 함수를 통해 생성할 수 있습니다. 전자는 일반적인 목적의 스코프를 생성하는 반면, 후자는 UI 애플리케이션을 위한 스코프를 생성하고 Dispatchers.Main을 기본 디스패처로 사용합니다.

kotlin
class Activity {
    private val mainScope = MainScope()
    
    fun destroy() {
        mainScope.cancel()
    }
    // to be continued ...

이제 정의된 mainScope를 사용하여 이 Activity 스코프 내에서 코루틴을 시작할 수 있습니다. 데모를 위해, 각기 다른 시간 동안 지연되는 10개의 코루틴을 시작합니다.

kotlin
    // class Activity continues
    fun doSomething() {
        // launch ten coroutines for a demo, each working for a different time
        repeat(10) { i ->
            mainScope.launch {
                delay((i + 1) * 200L) // variable delay 200ms, 400ms, ... etc
                println("Coroutine $i is done")
            }
        }
    }
} // class Activity ends

main 함수에서는 액티비티를 생성하고, 테스트 doSomething 함수를 호출한 다음, 500ms 후에 액티비티를 파괴합니다. 이렇게 하면 doSomething에서 시작된 모든 코루틴이 취소됩니다. 액티비티 파괴 후에는 더 이상 메시지가 출력되지 않는 것을 통해 이를 확인할 수 있습니다. 조금 더 오래 기다려도 마찬가지입니다.

kotlin
import kotlinx.coroutines.*

class Activity {
    private val mainScope = CoroutineScope(Dispatchers.Default) // use Default for test purposes
    
    fun destroy() {
        mainScope.cancel()
    }

    fun doSomething() {
        // launch ten coroutines for a demo, each working for a different time
        repeat(10) { i ->
            mainScope.launch {
                delay((i + 1) * 200L) // variable delay 200ms, 400ms, ... etc
                println("Coroutine $i is done")
            }
        }
    }
} // class Activity ends

fun main() = runBlocking<Unit> {
    val activity = Activity()
    activity.doSomething() // run test function
    println("Launched coroutines")
    delay(500L) // delay for half a second
    println("Destroying activity!")
    activity.destroy() // cancels all coroutines
    delay(1000) // visually confirm that they don't work
}

전체 코드는 여기에서 확인할 수 있습니다.

이 예제의 출력은 다음과 같습니다:

text
Launched coroutines
Coroutine 0 is done
Coroutine 1 is done
Destroying activity!

보시다시피, 처음 두 코루틴만 메시지를 출력하고 나머지는 Activity.destroy()mainScope.cancel() 단일 호출에 의해 취소됩니다.

Android는 수명 주기를 가진 모든 엔티티에서 코루틴 스코프를 기본적으로 지원한다는 점에 유의하세요. 해당 문서를 참조하십시오.

스레드 로컬 데이터

때때로 스레드 로컬 데이터를 코루틴으로 전달하거나 코루틴 간에 전달하는 것이 편리할 수 있습니다. 그러나 코루틴은 특정 스레드에 묶여 있지 않으므로, 수동으로 처리하면 상용구 코드가 발생할 가능성이 높습니다.

ThreadLocal의 경우, asContextElement 확장 함수가 도움이 될 수 있습니다. 이 함수는 주어진 ThreadLocal의 값을 유지하고 코루틴이 컨텍스트를 전환할 때마다 해당 값을 복원하는 추가 컨텍스트 요소를 생성합니다.

작동 방식을 시연하기 쉽습니다.

kotlin
import kotlinx.coroutines.*

val threadLocal = ThreadLocal<String?>() // declare thread-local variable

fun main() = runBlocking<Unit> {
    threadLocal.set("main")
    println("Pre-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
    val job = launch(Dispatchers.Default + threadLocal.asContextElement(value = "launch")) {
        println("Launch start, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
        yield()
        println("After yield, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
    }
    job.join()
    println("Post-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
}

전체 코드는 여기에서 확인할 수 있습니다.

이 예제에서는 Dispatchers.Default를 사용하여 백그라운드 스레드 풀에서 새 코루틴을 시작합니다. 따라서 이 코루틴은 스레드 풀의 다른 스레드에서 작동하지만, 코루틴이 어떤 스레드에서 실행되든 threadLocal.asContextElement(value = "launch")를 사용하여 지정한 스레드 로컬 변수의 값을 여전히 가집니다. 따라서 (디버그 사용 시) 출력은 다음과 같습니다.

text
Pre-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'
Launch start, current thread: Thread[DefaultDispatcher-worker-1 @coroutine#2,5,main], thread local value: 'launch'
After yield, current thread: Thread[DefaultDispatcher-worker-2 @coroutine#2,5,main], thread local value: 'launch'
Post-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'

해당 컨텍스트 요소를 설정하는 것을 잊기 쉽습니다. 코루틴에서 접근하는 스레드 로컬 변수는 코루틴을 실행하는 스레드가 다른 경우 예상치 못한 값을 가질 수 있습니다. 이러한 상황을 피하려면 ensurePresent 메서드를 사용하여 잘못된 사용 시 빠르게 실패(fail-fast)하도록 하는 것이 좋습니다.

ThreadLocal은 일등석 지원을 받으며 kotlinx.coroutines가 제공하는 모든 기본 요소와 함께 사용할 수 있습니다. 그러나 한 가지 중요한 제한 사항이 있습니다. 스레드 로컬이 변경될 때 새 값이 코루틴 호출자에게 전파되지 않으며(컨텍스트 요소는 모든 ThreadLocal 객체 접근을 추적할 수 없기 때문), 업데이트된 값은 다음 중단 시 손실됩니다. 코루틴에서 스레드 로컬의 값을 업데이트하려면 withContext를 사용하세요. 자세한 내용은 asContextElement를 참조하세요.

또는, class Counter(var i: Int)와 같은 변경 가능한 박스(mutable box)에 값을 저장할 수 있으며, 이 박스는 다시 스레드 로컬 변수에 저장됩니다. 그러나 이 경우, 이 변경 가능한 박스 내 변수에 대한 잠재적인 동시 수정을 동기화할 전적인 책임은 사용자에게 있습니다.

고급 사용의 경우, 예를 들어 로깅 MDC, 트랜잭션 컨텍스트 또는 데이터를 전달하기 위해 내부적으로 스레드 로컬을 사용하는 다른 라이브러리와 통합하려면 구현해야 할 ThreadContextElement 인터페이스 문서를 참조하십시오.