Skip to content

Commit 08950a8

Browse files
committed
Replace synchronized(this) with lock-free initState in LazyList
JDK 23+ allocates `ObjectMonitor` instances in native memory eagerly in the single-threaded case under nested recursive synchronized blocks. This can lead to huge memory consumption while evaluating LazyLists. This commit replaces synchronized with a lock-free coordination.
1 parent c101b01 commit 08950a8

5 files changed

Lines changed: 249 additions & 38 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Scala (https://www.scala-lang.org)
3+
*
4+
* Copyright EPFL and Lightbend, Inc. dba Akka
5+
*
6+
* Licensed under Apache License 2.0
7+
* (http://www.apache.org/licenses/LICENSE-2.0).
8+
*
9+
* See the NOTICE file distributed with this work for
10+
* additional information regarding copyright ownership.
11+
*/
12+
13+
package scala.collection.immutable
14+
15+
import scala.language.`2.13`
16+
17+
/**
18+
* Base class for [[LazyList]] to split out code that uses concurrency utilities that are not available on Scala.js.
19+
*/
20+
abstract class LazyListBase[+A] private[immutable] (initialTail: AnyRef | Null) extends AbstractSeq[A] with Serializable {
21+
/** See [[LazyList._head]] for the possible states of this field. */
22+
@volatile private var _tail: AnyRef | Null /* () => LazyList[A] | Thread | InRace | LazyList[A] | Null */ = initialTail
23+
24+
private[immutable] def rawTail: AnyRef | Null = _tail
25+
26+
private[immutable] def setRawTail(value: AnyRef): Unit = _tail = value
27+
28+
private[immutable] def makeTailUpdater: LazyListBase.TailUpdater = LazyListBase.TailUpdater()
29+
}
30+
31+
private[immutable] object LazyListBase {
32+
final class TailUpdater {
33+
@inline def compareAndSet(ll: LazyListBase[?], expected: AnyRef, value: AnyRef): Boolean =
34+
if (ll._tail eq expected) { ll._tail = value; true } else false
35+
36+
@inline def getAndSet(ll: LazyListBase[?], value: AnyRef | Null): AnyRef | Null = {
37+
val old = ll._tail
38+
ll._tail = value
39+
old
40+
}
41+
}
42+
43+
def isCurrentThread(t: Thread): Boolean = true
44+
45+
def InRace(t: Thread): InRace = throw new Exception("unreachable")
46+
47+
final class InRace private[LazyListBase] (val owner: Thread) {
48+
def await(): Unit = ()
49+
def countDown(): Unit = ()
50+
}
51+
}

library/src/scala/collection/immutable/LazyList.scala

Lines changed: 70 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ package immutable
1717
import scala.language.`2.13`
1818
import java.io.{ObjectInputStream, ObjectOutputStream}
1919
import java.lang.{StringBuilder => JStringBuilder}
20-
2120
import scala.annotation.tailrec
2221
import scala.collection.generic.SerializeEnd
22+
import scala.collection.immutable.LazyListBase.InRace
2323
import scala.collection.mutable.{Builder, ReusableBuilder, StringBuilder}
2424
import scala.language.implicitConversions
2525
import scala.runtime.Statics
@@ -264,7 +264,7 @@ import scala.runtime.Statics
264264
*/
265265
@SerialVersionUID(4L)
266266
final class LazyList[+A] private (lazyState: AnyRef /* EmptyMarker.type | () => LazyList[A] */)
267-
extends AbstractSeq[A]
267+
extends LazyListBase[A](if (lazyState eq LazyList.EmptyMarker) null else lazyState)
268268
with LinearSeq[A]
269269
with LinearSeqOps[A, LazyList, LazyList[A]]
270270
with IterableFactoryDefaults[A, LazyList]
@@ -276,56 +276,86 @@ final class LazyList[+A] private (lazyState: AnyRef /* EmptyMarker.type | () =>
276276
private def this(head: A, tail: LazyList[A]) = {
277277
this(LazyList.EmptyMarker)
278278
_head = head
279-
_tail = tail
279+
setRawTail(tail)
280280
}
281281

282-
// used to synchronize lazy state evaluation
283-
// after initialization (`_head ne Uninitialized`)
282+
// `_head` and `_tail` are used to synchronize lazy state evaluation.
283+
//
284+
// initially, `_head` is `Uninitialized`. after initialization, `_head` holds:
284285
// - `null` if this is an empty lazy list
285-
// - `head: A` otherwise (can be `null`, `_tail == null` is used to test emptiness)
286+
// - `head: A` otherwise (can be the `null` value, `_tail == null` is used to test emptiness)
287+
//
288+
// `_tail` (declared in `LazyListBase`) can hold the following values:
289+
// - when `_head eq Uninitialized`
290+
// - `lazyState: () => LazyList[A]`
291+
// - while evaluating `lazyState`: the evaluating `Thread`
292+
// - if multiple threads attempt initialization: an `InRace` instance
293+
// - when `_head ne Uninitialized`
294+
// - `null` if this is an empty lazy list
295+
// - `tail: LazyList[A]` otherwise
286296
@volatile private var _head: Any /* Uninitialized | A */ =
287297
if (lazyState eq EmptyMarker) null else Uninitialized
288298

289-
// when `_head eq Uninitialized`
290-
// - `lazySate: () => LazyList[A]`
291-
// - MidEvaluation while evaluating lazyState
292-
// when `_head ne Uninitialized`
293-
// - `null` if this is an empty lazy list
294-
// - `tail: LazyList[A]` otherwise
295-
private var _tail: AnyRef | Null /* () => LazyList[A] | MidEvaluation.type | LazyList[A] | Null */ =
296-
if (lazyState eq EmptyMarker) null else lazyState
297-
298299
private def rawHead: Any = _head
299-
private def rawTail: AnyRef | Null = _tail
300300

301301
@inline private def isEvaluated: Boolean = _head.asInstanceOf[AnyRef] ne Uninitialized
302302

303-
private def initState(): Unit = synchronized {
304-
if (!isEvaluated) {
305-
// if it's already mid-evaluation, we're stuck in an infinite
306-
// self-referential loop (also it's empty)
307-
if (_tail eq MidEvaluation)
308-
throw new RuntimeException(
309-
"LazyList evaluation depends on its own result (self-reference); see docs for more info")
310-
311-
val fun = _tail.asInstanceOf[() => LazyList[A]]
312-
_tail = MidEvaluation
313-
val l =
314-
// `fun` returns a LazyList that represents the state (head/tail) of `this`. We call `l.evaluated` to ensure
315-
// `l` is initialized, to prevent races when reading `rawTail` / `rawHead` below.
316-
// Often, lazy lists are created with `newLL(eagerCons(...))` so `l` is already initialized, but `newLL` also
317-
// accepts non-evaluated lazy lists.
318-
try fun().evaluated
319-
// restore `fun` in finally so we can try again later if an exception was thrown (similar to lazy val)
320-
finally _tail = fun
321-
_tail = l.rawTail
322-
_head = l.rawHead
303+
private def initState(): Unit = {
304+
def selfRef(): Nothing =
305+
// if it's already mid-evaluation, we're stuck in an infinite self-referential loop (also it's empty)
306+
throw new RuntimeException(
307+
"LazyList evaluation depends on its own result (self-reference); see docs for more info")
308+
309+
while (!isEvaluated) {
310+
rawTail match {
311+
case t: Thread =>
312+
if (LazyListBase.isCurrentThread(t)) selfRef()
313+
val ir = InRace(t)
314+
if (_tailUpdater.compareAndSet(this, t, ir))
315+
ir.await()
316+
// loop on lost CAS
317+
318+
case ir: InRace =>
319+
if (LazyListBase.isCurrentThread(ir.owner)) selfRef()
320+
ir.await()
321+
322+
case fun: Function0[_] =>
323+
// use the current thread as marker that `fun` is being evaluated.
324+
// this way, there is no allocation in the common case where there's no race.
325+
// if multiple threads attempt to initialize a LazyList, an `InRace` instance is created to coordinate.
326+
if (_tailUpdater.compareAndSet(this, fun, Thread.currentThread)) {
327+
var ex: Throwable | Null = null
328+
// `fun` returns a LazyList that represents the state (head/tail) of `this`. We call `evaluated` to ensure
329+
// the result is initialized, to prevent races when reading `rawTail` / `rawHead` below.
330+
// Often, lazy lists are created with `newLL(eagerCons(...))` so `l` is already initialized, but `newLL`
331+
// also accepts non-evaluated lazy lists.
332+
val l = try fun().asInstanceOf[LazyList[A]].evaluated catch {
333+
case t: Throwable =>
334+
ex = t
335+
null
336+
}
337+
// update `_tail` before `_head`, because `_head` is used to test `isEvaluated`
338+
val newTail = if (ex == null) l.nn.rawTail else fun
339+
val sentinel = _tailUpdater.getAndSet(this, newTail)
340+
if (ex == null) _head = l.nn.rawHead
341+
sentinel match {
342+
case ir: InRace => ir.countDown()
343+
case _ =>
344+
}
345+
if (ex != null) throw ex.nn
346+
}
347+
// loop on lost CAS
348+
349+
case _ =>
350+
// loop when _tail is a LazyList but _head is still `Uninitialized`
351+
// could call `Thread.onSpinWait()` on JDK 9+
352+
}
323353
}
324354
}
325355

326356
@tailrec private def evaluated: LazyList[A] =
327357
if (isEvaluated) {
328-
if (_tail == null) Empty
358+
if (rawTail == null) Empty
329359
else this
330360
} else {
331361
initState()
@@ -1160,11 +1190,13 @@ object LazyList extends SeqFactory[LazyList] {
11601190
// def kount(): Unit = k += 1
11611191

11621192
private object Uninitialized extends Serializable
1163-
private object MidEvaluation
11641193
private object EmptyMarker
11651194

11661195
private val Empty: LazyList[Nothing] = new LazyList(EmptyMarker)
11671196

1197+
// lazy val to break cycle (Predef -> scala.package -> val LazyList -> makeTailUpdater -> Predef.classOf)
1198+
private lazy val _tailUpdater: LazyListBase.TailUpdater = Empty.makeTailUpdater
1199+
11681200
/** Creates a new LazyList.
11691201
*
11701202
* @tparam A the element type of the lazy list
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Scala (https://www.scala-lang.org)
3+
*
4+
* Copyright EPFL and Lightbend, Inc. dba Akka
5+
*
6+
* Licensed under Apache License 2.0
7+
* (http://www.apache.org/licenses/LICENSE-2.0).
8+
*
9+
* See the NOTICE file distributed with this work for
10+
* additional information regarding copyright ownership.
11+
*/
12+
13+
package scala.collection.immutable
14+
15+
import scala.language.`2.13`
16+
17+
import java.util.concurrent.CountDownLatch
18+
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater
19+
20+
/**
21+
* Base class for [[LazyList]] to split out code that uses concurrency utilities that are not available
22+
* on Scala.js. This way, Scala.js does not need to override all of LazyList.
23+
*
24+
* This class cannot be a trait because `AtomicReferenceFieldUpdater.newUpdater` checks if the caller
25+
* class has access to the corresponding field. So it needs to be called in the class where the field is
26+
* declared (fields are always private in Scala).
27+
*/
28+
abstract class LazyListBase[+A] private[immutable] (initialTail: AnyRef | Null) extends AbstractSeq[A] with Serializable {
29+
/** See [[LazyList._head]] for the possible states of this field. */
30+
@volatile private var _tail: AnyRef | Null /* () => LazyList[A] | Thread | InRace | LazyList[A] | Null */ = initialTail
31+
32+
private[immutable] def rawTail: AnyRef | Null = _tail
33+
34+
private[immutable] def setRawTail(value: AnyRef): Unit = _tail = value
35+
36+
@noinline private[immutable] def makeTailUpdater: LazyListBase.TailUpdater =
37+
new LazyListBase.TailUpdater(AtomicReferenceFieldUpdater.newUpdater(classOf[LazyListBase[?]], classOf[AnyRef], "_tail"))
38+
}
39+
40+
private[immutable] object LazyListBase {
41+
final class TailUpdater(u: AtomicReferenceFieldUpdater[LazyListBase[?], AnyRef]) {
42+
def compareAndSet(ll: LazyListBase[?], expected: AnyRef, value: AnyRef): Boolean = u.compareAndSet(ll, expected, value)
43+
def getAndSet(ll: LazyListBase[?], value: AnyRef | Null): AnyRef | Null = u.getAndSet(ll, value)
44+
}
45+
46+
// this utility is constant `true` on Scala.js -> enables DCE in LazyList
47+
def isCurrentThread(t: Thread): Boolean = t eq Thread.currentThread
48+
// also for Scala.js
49+
def InRace(t: Thread): InRace = new InRace(t)
50+
51+
final class InRace private[LazyListBase] (val owner: Thread) {
52+
private val done: CountDownLatch = new CountDownLatch(1)
53+
54+
def await(): Unit = {
55+
var interrupted = false
56+
while (done.getCount > 0) {
57+
try done.await() catch {
58+
case _: InterruptedException => interrupted = true
59+
}
60+
}
61+
if (interrupted) Thread.currentThread().interrupt()
62+
}
63+
64+
def countDown(): Unit = done.countDown()
65+
}
66+
}

library/test/scala/collection/immutable/LazyListTest.scala

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import scala.annotation.unused
88
import scala.collection.immutable.LazyListTest.sd
99
import scala.collection.mutable.{Builder, ListBuffer}
1010
import tools.AssertUtil
11+
12+
import java.io.NotSerializableException
13+
import java.util.concurrent.atomic.AtomicInteger
14+
import java.util.concurrent.{ConcurrentLinkedQueue, CountDownLatch}
1115
import scala.util.Try
1216

1317
class LazyListTest {
@@ -52,6 +56,57 @@ class LazyListTest {
5256
assertTrue(ll.toList == List(1, 2))
5357
}
5458

59+
@Test def racySerialization(): Unit = {
60+
import sd._
61+
val ll = 1 #:: { Thread.sleep(500); 2} #:: LazyList.empty
62+
new Thread(() => println(ll.toList)).start()
63+
Thread.sleep(200)
64+
AssertUtil.assertThrows[NotSerializableException](serialize(ll), msg => msg.contains("java.lang.Thread") || msg.contains("InRace"))
65+
}
66+
67+
@Test def forceSameCellManyThreads(): Unit = {
68+
val N = 8
69+
val trials = 5
70+
for (trial <- 1 to trials) {
71+
val counts = Array.fill(20)(new AtomicInteger(0))
72+
val ll = LazyList.tabulate(20) { i =>
73+
counts(i).incrementAndGet()
74+
i * 10
75+
}
76+
val go = new CountDownLatch(1)
77+
val done = new CountDownLatch(N)
78+
val results = new ConcurrentLinkedQueue[List[Int]]
79+
for (_ <- 1 to N)
80+
new Thread(() => {
81+
go.await()
82+
results.add(ll.toList)
83+
done.countDown()
84+
}).start()
85+
go.countDown()
86+
done.await()
87+
88+
val expected = (0 until 20).map(_ * 10).toList
89+
val it = results.iterator
90+
var seen = 0
91+
while (it.hasNext) { assertEquals(s"trial $trial", expected, it.next()); seen += 1 }
92+
assertEquals(s"trial $trial: result count", N, seen)
93+
for (i <- 0 until 20)
94+
assertEquals(s"trial $trial: element $i evaluated multiple times", 1, counts(i).get())
95+
}
96+
}
97+
98+
@Test def initStateExceptionRecovery(): Unit = {
99+
var n = 0
100+
val ll = 1 #:: {
101+
n += 1
102+
if (n == 1) throw new RuntimeException("first attempt throws") else 2
103+
} #:: LazyList.empty
104+
105+
AssertUtil.assertThrows[RuntimeException](ll.toList, _.contains("first attempt throws"))
106+
assertEquals(List(1, 2), ll.toList)
107+
assertEquals(2, n)
108+
}
109+
55110
@Test def storeNull(): Unit = {
56111
val l = "1" #:: null #:: "2" #:: LazyList.empty
57112
assert(l.toList == List("1", null, "2"))

project/MiMaFilters.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,15 @@ object MiMaFilters {
2424

2525
// Breaking changes since last reference version
2626
Build.mimaPreviousDottyVersion -> Seq(
27+
28+
// scala/scala3#26100
29+
ProblemFilters.exclude[MissingTypesProblem]("scala.collection.immutable.LazyList"),
30+
ProblemFilters.exclude[MissingTypesProblem]("scala.collection.immutable.LazyList$MidEvaluation$"),
31+
ProblemFilters.exclude[DirectMissingMethodProblem]("scala.collection.immutable.LazyList.<clinit>"),
32+
2733
ProblemFilters.exclude[DirectMissingMethodProblem]("scala.None.orNull"),
2834
ProblemFilters.exclude[MissingTypesProblem]("scala.util.control.NonLocalReturns$ReturnThrowable"),
35+
2936
// THIS IS FINE, IT SHOULD HAVE BEEN THIS WAY
3037
ProblemFilters.exclude[MissingTypesProblem]("scala.Function1$"),
3138
ProblemFilters.exclude[MissingTypesProblem]("scala.Function1$UnliftOps$"),

0 commit comments

Comments
 (0)