IOSchedのコードを読んでいたら、ちょっと気になるExtensionを見つけました。
/** * Implementation of lazy that is not thread safe. Useful when you know what thread you will be * executing on and are not worried about synchronization. */ fun <T> lazyFast(operation: () -> T): Lazy<T> = lazy(LazyThreadSafetyMode.NONE) { operation() }
lazy
ってThread Safeだったの???
Lazyとは
Lazy - Kotlin Programming Language
https://kotlinlang.org/docs/reference/delegated-properties.html#lazy
Androidではコンストラクタとライフサイクルメソッドの処理順番がずれるため、特にContextを引数に利用するフィールドに利用する傾向があるかと思います。 RecyclerViewのAdapterにContextが必要なケースとか、NotificationBuilderをなんども使いまわすとか、そういうパターンです。
さて、肝心のスレッドについては下記の記述があります。
By default, the evaluation of lazy properties is synchronized: the value is computed only in one thread, and all threads will see the same value. If the synchronization of initialization delegate is not required, so that multiple threads can execute it simultaneously, pass LazyThreadSafetyMode.PUBLICATION as a parameter to the lazy() function. And if you're sure that the initialization will always happen on a single thread, you can use LazyThreadSafetyMode.NONE mode, which doesn't incur any thread-safety guarantees and the related overhead.
デフォルトでは同期的な処理となっています。lazy
に与えられた式の計算はただ1つのスレッドで行われ、取得はキャッシュされた計算結果へのアクセスです。
同期的な処理であるため、1つの lazy
に対して複数のスレッドから同時にアクセスすることを考慮した実装となります。
このため同期的な処理が不要である場合用に LazyThreadSafetyMode.PUBLICATION
、逆に1つのスレッドからのみ取得することが保証されている場合用に LazyThreadSafetyMode.NONE
が用意されています。
LazyThreadSafetyMode
LazyThreadSafetyMode
は Lazyクラスに定義されています。
kotlin/libraries/stdlib/jvm/src/kotlin/util/LazyJVM.kt at master · JetBrains/kotlin · GitHub
/** * Specifies how a [Lazy] instance synchronizes initialization among multiple threads. */ public enum class LazyThreadSafetyMode { /** * Locks are used to ensure that only a single thread can initialize the [Lazy] instance. */ SYNCHRONIZED, /** * Initializer function can be called several times on concurrent access to uninitialized [Lazy] instance value, * but only the first returned value will be used as the value of [Lazy] instance. */ PUBLICATION, /** * No locks are used to synchronize an access to the [Lazy] instance value; if the instance is accessed from multiple threads, its behavior is undefined. * * This mode should not be used unless the [Lazy] instance is guaranteed never to be initialized from more than one thread. */ NONE, }
LazyThreadSafetyMode.SYNCHRONIZED
LazyJVM.kt
に実装があります。
kotlin/libraries/stdlib/jvm/src/kotlin/util/LazyJVM.kt at master · JetBrains/kotlin · GitHub
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable { private var initializer: (() -> T)? = initializer @Volatile private var _value: Any? = UNINITIALIZED_VALUE // final field is required to enable safe publication of constructed instance private val lock = lock ?: this override val value: T get() { val _v1 = _value if (_v1 !== UNINITIALIZED_VALUE) { @Suppress("UNCHECKED_CAST") return _v1 as T } return synchronized(lock) { val _v2 = _value if (_v2 !== UNINITIALIZED_VALUE) { @Suppress("UNCHECKED_CAST") (_v2 as T) } else { val typedValue = initializer!!() _value = typedValue initializer = null typedValue } } } override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet." private fun writeReplace(): Any = InitializedLazyImpl(value) }
get()の中をみていくと @Volatile
で宣言された _value
フィールドに対して次の処理をしています。
_value
がUNINITIALIZED_VALUE
ではないか確認UNINITIALIZED_VALUE
ではなければ_value
を返却
synchonized
にて自分を指定し処理をブロック- 自身がブロックされていた処理であれば
_value
の値を返却 lazy
に与えられたinitializer
を処理initializer
の結果を_value
にキャッシュ- 保持していた
initializer
を(不要なため)破棄 initializer
の結果を返却
この順序で処理するため、複数のスレッドからアクセスされても問題が発生することなく、一度計算された値を使いまわすことができます。
LazyThreadSafetyMode.PUBLICATION
LazyJVM.kt
に実装があります。
kotlin/libraries/stdlib/jvm/src/kotlin/util/LazyJVM.kt at master · JetBrains/kotlin · GitHub
private class SafePublicationLazyImpl<out T>(initializer: () -> T) : Lazy<T>, Serializable { @Volatile private var initializer: (() -> T)? = initializer @Volatile private var _value: Any? = UNINITIALIZED_VALUE // this final field is required to enable safe publication of constructed instance private val final: Any = UNINITIALIZED_VALUE override val value: T get() { val value = _value if (value !== UNINITIALIZED_VALUE) { @Suppress("UNCHECKED_CAST") return value as T } val initializerValue = initializer // if we see null in initializer here, it means that the value is already set by another thread if (initializerValue != null) { val newValue = initializerValue() if (valueUpdater.compareAndSet(this, UNINITIALIZED_VALUE, newValue)) { initializer = null return newValue } } @Suppress("UNCHECKED_CAST") return _value as T } override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet." private fun writeReplace(): Any = InitializedLazyImpl(value) companion object { private val valueUpdater = java.util.concurrent.atomic.AtomicReferenceFieldUpdater.newUpdater( SafePublicationLazyImpl::class.java, Any::class.java, "_value" ) } }
_value
フィールドは、SYNCHRONIZED
と同様に @Volatile
で宣言されています。
大きな違いは initializer
が @Volatile
で宣言されていることです。
_value
がUNINITIALIZED_VALUE
ではないか確認UNINITIALIZED_VALUE
ではなければ_value
を返却
initializer
がnullかどうかを確認-
initializer
がnullならば、他のスレッドで処理が終わっていると判断し_value
を返却 initializer
の処理結果をnewValue
に保持valueUpdater
により_value
の値とnewValue
の値を比較- 異なっていなければ
_value
の値を返却
- 異なっていなければ
- 異なっていれば
newValue
を_value
にセット - 保持していた
initializer
を(不要なため)破棄 newValue
を返却
の順で処理をしていきます。
SafePublicationLazyImpl
においては _value
の更新処理を AtomicReferenceFieldUpdater
のみにすることで、複数のスレッドから同時にアクセスしている場合に対応しています。
なお AtomicReferenceFieldUpdater
についてはOracleのDocをご参照ください。 newUpdater
により更新するフィールドをCompanion Objectとして保持していることに留意すれば、Docで概要がつかめるかと思います。
LazyThreadSafetyMode.NONE
Lazy.kt
に実装があります。
kotlin/libraries/stdlib/src/kotlin/util/Lazy.kt at master · JetBrains/kotlin · GitHub
internal class UnsafeLazyImpl<out T>(initializer: () -> T) : Lazy<T>(), Serializable { private var initializer: (() -> T)? = initializer private var _value: Any? = UNINITIALIZED_VALUE override val value: T get() { if (_value === UNINITIALIZED_VALUE) { _value = initializer!!() initializer == null } return _value as T } override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet." private fun writeReplace(): Any = InitializedLazyImpl(value) }
これまでと比べると非常にシンプルです。
_value
がUNINITIALIZED_VALUE
ではないか確認UNINITIALIZED_VALUE
ではなければ_value
を返却
initializer
の結果を_value
にキャッシュ- 保持していた
initializer
を(不要なため)破棄 _value
を返却
複数のスレッドからのアクセスは考慮されていない一方で、考慮しなくて良い状況ならば不要な処理がない実装となっています。
所感
Androidの開発においては、UIスレッドのみからアクセスする状況であれば LazyThreadSafetyMode.NONE
の利用をしても良さそうです。(実際、IOSchedでは複数箇所で使っています。)
ただ、後ほどリファクタリング時に lazy
か lazyFast
かを考慮する必要が生じるため、一定以上の技術力のあるチームでなければ安全側に倒して lazy
のみで実装するのも良さそうです。
一方 LazyThreadSafetyMode.PUBLICATION
については、必要な状況があまり思いつきません。サーバーサイドKotlinなら、有用なのかなと思っています。
Kotlinのリポジトリ内でいくつか使われているようなので、知見がまとまったらまた書きたいなと思います。
まとめ
lazy
には3つのモードがあるSYNCHRONIZED
PUBLICATION
NONE
- 何も指定しない
lazy
はSYNCHRONIZED
モードのため同期的な処理 - Androidであれば、利用するスレッドが決まっている場合に
NONE
モードを利用するのも良さそう
ご指摘ありましたらぜひコメントください!