Kotlin Lazyを読む

IOSchedのコードを読んでいたら、ちょっと気になるExtensionを見つけました。

github.com

/**
 * 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()
}

iosched/shared/src/main/java/com/google/samples/apps/iosched/shared/util/Extensions.kt at main · google/iosched · GitHub

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 フィールドに対して次の処理をしています。

  1. _valueUNINITIALIZED_VALUE ではないか確認
    • UNINITIALIZED_VALUE ではなければ _value を返却
  2. synchonized にて自分を指定し処理をブロック
  3. 自身がブロックされていた処理であれば _value の値を返却
  4. lazy に与えられた initializer を処理
  5. initializer の結果を _value にキャッシュ
  6. 保持していた initializer を(不要なため)破棄
  7. 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 で宣言されていることです。

  1. _valueUNINITIALIZED_VALUE ではないか確認
    • UNINITIALIZED_VALUE ではなければ _value を返却
  2. initializer がnullかどうかを確認
  3. initializer がnullならば、他のスレッドで処理が終わっていると判断し _value を返却
  4. initializer の処理結果を newValue に保持
  5. valueUpdater により _value の値と newValue の値を比較
    • 異なっていなければ _value の値を返却
  6. 異なっていれば newValue_value にセット
  7. 保持していた initializer を(不要なため)破棄
  8. newValue を返却

の順で処理をしていきます。 SafePublicationLazyImpl においては _value の更新処理を AtomicReferenceFieldUpdater のみにすることで、複数のスレッドから同時にアクセスしている場合に対応しています。

なお AtomicReferenceFieldUpdater についてはOracleのDocをご参照ください。 newUpdater により更新するフィールドをCompanion Objectとして保持していることに留意すれば、Docで概要がつかめるかと思います。

docs.oracle.com

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)
}

これまでと比べると非常にシンプルです。

  1. _valueUNINITIALIZED_VALUE ではないか確認
    • UNINITIALIZED_VALUE ではなければ _value を返却
  2. initializer の結果を _value にキャッシュ
  3. 保持していた initializer を(不要なため)破棄
  4. _value を返却

複数のスレッドからのアクセスは考慮されていない一方で、考慮しなくて良い状況ならば不要な処理がない実装となっています。

所感

Androidの開発においては、UIスレッドのみからアクセスする状況であれば LazyThreadSafetyMode.NONE の利用をしても良さそうです。(実際、IOSchedでは複数箇所で使っています。) ただ、後ほどリファクタリング時に lazylazyFast かを考慮する必要が生じるため、一定以上の技術力のあるチームでなければ安全側に倒して lazy のみで実装するのも良さそうです。

一方 LazyThreadSafetyMode.PUBLICATION については、必要な状況があまり思いつきません。サーバーサイドKotlinなら、有用なのかなと思っています。 Kotlinのリポジトリ内でいくつか使われているようなので、知見がまとまったらまた書きたいなと思います。

まとめ

  1. lazy には3つのモードがある
    1. SYNCHRONIZED
    2. PUBLICATION
    3. NONE
  2. 何も指定しない lazySYNCHRONIZED モードのため同期的な処理
  3. Androidであれば、利用するスレッドが決まっている場合に NONE モードを利用するのも良さそう

ご指摘ありましたらぜひコメントください!