From 74f39c237562b0afdb4085ae2171be37e6331a42 Mon Sep 17 00:00:00 2001 From: Ilya Gorbunov Date: Fri, 25 Sep 2015 04:09:05 +0300 Subject: [PATCH] Provide LazyThreadSafetyMode.PUBLICATION: concurrent non-blocking initializations, single publication. --- libraries/stdlib/src/kotlin/util/Lazy.kt | 6 ++ libraries/stdlib/src/kotlin/util/LazyJVM.kt | 34 +++++++++++ libraries/stdlib/test/utils/LazyJVMTest.kt | 68 ++++++++++++++++++++- 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/libraries/stdlib/src/kotlin/util/Lazy.kt b/libraries/stdlib/src/kotlin/util/Lazy.kt index 062fc50cc00..32875bc0aa8 100644 --- a/libraries/stdlib/src/kotlin/util/Lazy.kt +++ b/libraries/stdlib/src/kotlin/util/Lazy.kt @@ -43,6 +43,12 @@ public enum class LazyThreadSafetyMode { */ SYNCHRONIZED, + /** + * Initializer function can be called several times on concurrent access to uninitialized [Lazy] instance value, + * but only first returned value will be used as the value of [Lazy] instance. + */ + PUBLICATION, + /** * No locks are used to synchronize the access to the [Lazy] instance value; if the instance is accessed from multiple threads, its behavior is undefined. * diff --git a/libraries/stdlib/src/kotlin/util/LazyJVM.kt b/libraries/stdlib/src/kotlin/util/LazyJVM.kt index 39a087eb3e1..e6a07c64efb 100644 --- a/libraries/stdlib/src/kotlin/util/LazyJVM.kt +++ b/libraries/stdlib/src/kotlin/util/LazyJVM.kt @@ -3,6 +3,8 @@ package kotlin +import java.io.Serializable + /** * Creates a new instance of the [Lazy] that uses the specified initialization function [initializer] @@ -28,6 +30,7 @@ public fun lazy(initializer: () -> T): Lazy = SynchronizedLazyImpl(initial public fun lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy = when (mode) { LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer) + LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer) LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer) } @@ -43,3 +46,34 @@ public fun lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy = * Also this behavior can be changed in the future. */ public fun lazy(lock: Any?, initializer: () -> T): Lazy = SynchronizedLazyImpl(initializer, lock) + + +private class SafePublicationLazyImpl(initializer: () -> T) : Lazy(), Serializable { + private var initializer: (() -> T)? = initializer + @Volatile private var _value: Any? = UNINITIALIZED_VALUE + private val final: Any = UNINITIALIZED_VALUE + + override val value: T + get() { + if (_value === UNINITIALIZED_VALUE) { + val newValue = initializer!!() + if (valueUpdater.compareAndSet(this, UNINITIALIZED_VALUE, newValue)) { + 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) + + companion object { + private val valueUpdater = java.util.concurrent.atomic.AtomicReferenceFieldUpdater.newUpdater( + SafePublicationLazyImpl::class.java, + Any::class.java, + "_value") + } +} \ No newline at end of file diff --git a/libraries/stdlib/test/utils/LazyJVMTest.kt b/libraries/stdlib/test/utils/LazyJVMTest.kt index 9925f11a40d..97649fbd09b 100644 --- a/libraries/stdlib/test/utils/LazyJVMTest.kt +++ b/libraries/stdlib/test/utils/LazyJVMTest.kt @@ -4,12 +4,78 @@ package test.utils import kotlin.* import kotlin.test.* import java.io.* +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicInteger +import kotlin.concurrent.thread import org.junit.Test as test class LazyJVMTest { + @test fun synchronizedLazy() { + val counter = AtomicInteger(0) + val lazy = lazy { + val value = counter.incrementAndGet() + Thread.sleep(100) + value + } + + val accessThreads = listOf(lazy, lazy).map { thread { it.value } } + accessThreads.forEach { it.join() } + + assertEquals(1, counter.get()) + } + + @test fun externallySynchronizedLazy() { + val counter = AtomicInteger(0) + var initialized: Boolean = false + val runs = ConcurrentHashMap() + val lock = Any() + + val initializer = { + val value = counter.incrementAndGet() + runs += (value to initialized) + Thread.sleep(100) + initialized = true + value + } + val lazy1 = lazy(lock, initializer) + val lazy2 = lazy(lock, initializer) + + val accessThreads = listOf(lazy1, lazy2).map { thread { it.value } } + accessThreads.forEach { it.join() } + + assertEquals(2, counter.get()) + for ((counter, initialized) in runs) { + assertEquals(initialized, counter == 2, "Expected uninitialized on first, initialized on second call: initialized=$initialized, counter=$counter") + } + } + + @test fun publishOnceLazy() { + val counter = AtomicInteger(0) + var initialized: Boolean = false + val runs = ConcurrentHashMap() + + val initializer = { + val value = counter.incrementAndGet() + runs += (value to initialized) + Thread.sleep((3 - value) * 100L) + initialized = true + value + } + val lazy = lazy(LazyThreadSafetyMode.PUBLICATION, initializer) + + val accessThreads = listOf(lazy, lazy).map { thread { it.value } } + accessThreads.forEach { it.join() } + + assertEquals(2, counter.get()) + assertEquals(2, lazy.value) + for ((counter, initialized) in runs) { + assertFalse(initialized, "Expected uninitialized on first and second run") + } + } + @test fun lazyInitializationForcedOnSerialization() { - for(mode in listOf(LazyThreadSafetyMode.SYNCHRONIZED, LazyThreadSafetyMode.NONE)) { + for(mode in listOf(LazyThreadSafetyMode.SYNCHRONIZED, LazyThreadSafetyMode.PUBLICATION, LazyThreadSafetyMode.NONE)) { val lazy = lazy(mode) { "initialized" } assertFalse(lazy.isInitialized()) val lazy2 = serializeAndDeserialize(lazy)