在他的一个视频中(关于Scala的懒惰评估,即lazy
关键字),Martin Odersky展示了用于构建cons
的{{1}}操作的以下实现:
Stream
因此使用该语言的惰性评估功能简明扼要地编写def cons[T](hd: T, tl: => Stream[T]) = new Stream[T] {
def head = hd
lazy val tail = tl
...
}
操作。
但实际上(在Scala 2.11.7中),tail
的实现有点不那么优雅了:
tail
双重检查锁定和两个易失性字段:这大致是如何在Java中实现线程安全的惰性计算。
所以问题是:
@volatile private[this] var tlVal: Stream[A] = _
@volatile private[this] var tlGen = tl _
def tailDefined: Boolean = tlGen eq null
override def tail: Stream[A] = {
if (!tailDefined)
synchronized {
if (!tailDefined) {
tlVal = tlGen()
tlGen = null
}
}
tlVal
}
关键字在多线程案例中是否提供任何“评估的最大一次”保证?lazy
实现中使用的模式是否是在Scala中进行线程安全延迟评估的惯用方法?答案 0 :(得分:4)
Scala的懒惰关键字不提供任何“已评估的最大值” 在多线程案例中保证?
是的,正如其他人所说的那样。
真正的尾部实现中使用的模式是否是惯用的方法 Scala中一个线程安全的懒惰评估?
我认为我有实际的回答为什么不是lazy val
。 Stream
具有公开的API方法,例如hasDefinitionSize
继承自TraversableOnce
。为了了解Stream
是否具有有限大小,我们需要一种检查的方法,而不会实现基础 Stream
尾部。由于lazy val
实际上没有公开底层位,我们不能这样做。
这由SI-1220
支持为了加强这一点,@ Jasper-M指出草丛中的新LazyList
api(Scala 2.13集合改造版)不再存在这个问题,因为整个集合层次结构已经重新设计并且不再存在这样的担忧。
我会说“这取决于”你在看这个问题的角度。从LOB的角度来看,我肯定会说明lazy val
的简洁性和清晰度。但是,如果从Scala集合库作者的角度来看它,事情开始变得不同。想象一下,你正在创建一个可能被许多人使用并在世界各地的许多机器上运行的库。这意味着你应该考虑每个结构的内存开销,特别是如果你自己创建这样一个基本的数据结构。
我说这是因为当你使用lazy val
时,你会设计一个额外的Boolean
字段来标记该值是否已经初始化,我假设这是图书馆作者的目标。避免。 JVM上Boolean
的大小当然取决于VM,甚至需要考虑一个字节,特别是当人们生成大Stream
个数据时。同样,这绝对是不我通常会考虑的东西,绝对是对内存使用的微优化。
我认为性能是关键点之一的原因是SI-7266修复了Stream中的内存泄漏。请注意跟踪字节代码以确保在生成的类中没有保留额外值的重要性。
实现的不同之处在于正在初始化的tail
的定义是检查生成器的方法实现:
def tailDefined: Boolean = tlGen eq null
而不是班上的一个字段。
答案 1 :(得分:2)
Scala lazy
值仅在多线程情况下评估一次。这是因为lazy
成员的评估实际上包含在生成的代码中的同步块中。
让我们来看看简单的claas,
class LazyTest {
lazy val x = 5
}
现在,让我们用scalac编译它,
scalac -Xprint:all LazyTest.scala
这将导致
package <empty> {
class LazyTest extends Object {
final <synthetic> lazy private[this] var x: Int = _;
@volatile private[this] var bitmap$0: Boolean = _;
private def x$lzycompute(): Int = {
LazyTest.this.synchronized(if (LazyTest.this.bitmap$0.unary_!())
{
LazyTest.this.x = (5: Int);
LazyTest.this.bitmap$0 = true
});
LazyTest.this.x
};
<stable> <accessor> lazy def x(): Int = if (LazyTest.this.bitmap$0.unary_!())
LazyTest.this.x$lzycompute()
else
LazyTest.this.x;
def <init>(): LazyTest = {
LazyTest.super.<init>();
()
}
}
}
你应该能够看到......懒惰的评估是线程安全的。而且你也会看到一些相似之处&#34;不太优雅&#34;在Scala 2.11.7中实现
您还可以尝试类似以下的测试,
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
case class A(i: Int) {
lazy val j = {
println("calculating j")
i + 1
}
}
def checkLazyInMultiThread(): Unit = {
val a = A(6)
val futuresList = Range(1, 20).toList.map(i => Future{
println(s"Future $i :: ${a.j}")
})
Future.sequence(futuresList).onComplete(_ => println("completed"))
}
checkLazyInMultiThread()
现在,标准库中的实现避免使用lazy
,因为它们能够提供比此通用lazy
转换更有效的解决方案。
答案 2 :(得分:1)
lazy val
使用锁定来防止双重评估,当两个线程同时访问时。此外,未来的发展将在没有锁定的情况下提供相同的保证。var
s while
{{1}来自命令式编程的循环和已建立的模式(正如您在问题中强调的那样)。