‫آیا متغیرهای Lazy کاتلین ترد سیف هستند؟

با توجه به مستندات مربوطه کاتلین سه نوع پیاده‌سازی مختلف برای Lazy دارد


public enum class LazyThreadSafetyMode {
    SYNCHRONIZED,
    PUBLICATION,
    NONE,
}

SYNCHRONIZED : تنها یک ترد میتواند آن را مقداردهی کند

PUBLICATION: متد مقداردهنده در شرایط همزمانی بین ترد ها ممکن است چندین مرتبه صدا شود. اما اولین مقداری که برگردانده شود بعنوان مقدار اصلی آن استفاده میشود.

NONE: هیچ محدودیتی برای سینکرونایز کردن متد مقداردهنده آن انجام نشده و رفتار آن در شرایط همزمانی مشخص نیست. تنها زمانی قابل استفاده‌است که مطمئن باشیم تنها روی یک ترد مقداردهی خواهد شد.

همچنین نیاز به گفتن نیست که خود Lazy یک اینترفیس با یک متد و یک متغیر که و مقدار متغیر رو نگه میداره:

public interface Lazy<out T> {
    public val value: T
    public fun isInitialized(): Boolean
}

منبع

تنها پیاده‌سازی موجود در StdLib پیاده‌سازی مربوط به NONE میباشد که کدش اینه:


internal object UNINITIALIZED_VALUE

// internal to be called from lazy in JS
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
            }
            @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)
}

internal class InitializedLazyImpl<out T>(override val value: T) : Lazy<T>, Serializable {
    override fun isInitialized(): Boolean = true
    override fun toString(): String = value.toString()
}

همونطور که میبینیم هیچ تدبیری برای تردسیف بودن مقداردهی متغیر مربوطه درنظر گرفته نشده و تنها درصورت UNINITIALIZED_VALUE بودن آن متد initializer آن را صدا میزند.

نکته جالب این پیاده‌سازی وجود متد writeReplace که خروجی Any داره و private هم هست و هیچ جایی صدا زده نشده! پس کاربردش چیه؟

دلیلش توی Serializable وجود داره که توی لینک منبع ۲ توضیح داده شده.

منبع ۱ منبع ۲

این پیاده‌سازی با توجه به امکانات تردینگ موجود توی JVM انجام شده و همونطور که میبینیم با استفاده از یک lock فرآیند مقداردهی شدن متغیر داخلی را synchronize کرده که تنها یک ترد بتونه وارد بخش بعدی بشه

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

نکته اول اینکه این پیاده‌سازی از خود این کلاس بعنوان lock استفاده میکنه.

و دیگری اینکه وقتی ما متغیری رو بصورت زیر تعریف میکنیم، از همین پیاده‌سازی Lazy استفاده میکنیم

که یعنی ترد سیفه و با خیال راحت میتونیم تو شرایط همزمانیم ازش استفاده کنیم

private val lazyVariable by lazy { /* ... */ }


// uses 
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

منبع

پیاده‌سازی آخر هم مربوط به PUBLICATION عه که با رویکرد دیگه‌ای نوشته شده و بجای سینکرونایز کردن بخش لاک کردن تردها برای ورود به بخش مقداردهی، از توابع java.util.concurrent.atomic برای اتمیک کردن و به یکباره مقداردهی شدن متغیر استفاده کرده.

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 initialization of the 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"
        )
    }
}

نکته قابل توجه این پیاده‌سازی هم استفاه از AtomicReferenceFieldUpdater عه که تو بخش companion object اومده و بقول منبع۲ یه روش reflection-base برای آپدیت یکباره فیلدهای volatile عه.

منبع منبع۲

در نهایت هم بگم اینکه تنها مورد استفاده از Lazy محدود به lazy { ... } نمیشه و ما میتونیم نوع تردسیف بودن رو مشخص کنیم

private val lazyVariable by lazy { /* ... */ }


public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

یک فیلد Lazy با نوع سینکرونایزد میسازه و اگر موقع مقداردهی به exception بخوره دفعه بعدی که بخوایم بهش دسترسی پیدا کنیم مقداردهی رو مجدد انجام میده.


private val lazyVariable by lazy(LazyThreadSafetyMode.PUBLICATION) { /* ... */ }


public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
    when (mode) {
        LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
        LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
        LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
    }

میتونیم نوع پیاده‌سازی دلخواهمون رو مشخص کنیم.

private val LOCK = Any()
private val lazyVariable by lazy(LOCK) { /* ... */ }


public actual fun <T> lazy(lock: Any?, initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer, lock)

این هم یک متد دیگه برای استفاده از پیاده‌سازی سینکرونایزه، با این تفاوت که میتونیم آبجکت lock رو خودمون بهش پاس بدیم. توی موارد قبلی و بصورت پیش‌فرض از خود ابجکت بعنوان lock استفاده میکرد

منبع