Scalaの怠惰なvalへのガイド
1. 概要
Scalaは、変数の初期化を延期する lazyvalと呼ばれる優れた言語機能を提供します。 遅延初期化パターンは、Javaプログラムでは一般的です。
魅力的なように見えますが、lazy valの具体的な実装にはいくつかの微妙な問題があります。このクイックチュートリアルでは、基礎となるバイトコードを分析して lazyval機能を調べます。
2. lazy val はどのように機能しますか?
valをlazyとして指定するには、変数宣言の前にlazyキーワードを追加するだけです。
lazy val foo = {
println("Initialized")
1
}
foo: Int = <lazy>
コンパイラは、 lazyvalのバインドされた式をすぐには評価しません。 最初のアクセスでのみ変数を評価します。最初のアクセス時に、コンパイラは式を評価し、結果を lazyvalに格納します。 後の段階でこの値にアクセスするたびに、実行は行われず、コンパイラは結果を返します。
このプログラムを実行したときに得られる出力を見てみましょう。
Initialized
res0: Int = 1
3. lazy valのデコード
次に、 lazyvalの内部で何が起こっているのかを調べてみましょう。
まず、Personクラス内で単一のlazyvalを宣言します。
class Person {
lazy val age = 27
}
Person.scalaファイルをコンパイルすると、Person.classファイルが取得されます。 このクラスファイルは、任意のJavaデコンパイラを使用して逆コンパイルできます。 このクラスファイルを逆コンパイルすると、すべてのレイジーvalに対して生成される同等のJavaコードを取得します。
public class Person {
private int age;
private volatile boolean bitmap$0;
private int age$lzycompute() {
synchronized (this) {
if (!this.bitmap$0) {
this.age = 27;
this.bitmap$0 = true;
}
}
return this.age;
}
public int age() {
return this.bitmap$0 ? this.age : this.age$lzycompute();
}
}
私たちが何から始めたかを考えると、これはかなり多くのコードです!
7行目で、コンパイラは同期された(これ){…}モニターを導入します。 これにより、変数が1回だけ初期化されることが保証されます。 コンパイラはブールフラグを導入しますbitmap$0は初期化ステータスを追跡するためのものです。 この変数は、コンパイラが lazyvalに初めてアクセスするときに変更されます。
4. lazy valsのパフォーマンスのボトルネック
4.1. lazy valsにアクセスする際の潜在的なデッドロック
複数のスレッドからインスタンス内の複数の値にアクセスすると、常にデッドロックが発生する可能性があります。
object FirstObj {
lazy val initialState = 42
lazy val start = SecondObj.initialState
}
object SecondObj {
lazy val initialState = FirstObj.initialState
}
object Deadlock extends App {
def run = {
val result = Future.sequence(Seq(
Future {
FirstObj.start
},
Future {
SecondObj.initialState
}
))
Await.result(result, 10.second)
}
run
}
上記のコードを実行すると、スタックstraceが取得されます。
Exception in thread "main" java.util.concurrent.TimeoutException: Future timed out after [10 seconds]
at scala.concurrent.impl.Promise$DefaultPromise.tryAwait0(Promise.scala:212)
at scala.concurrent.impl.Promise$DefaultPromise.result(Promise.scala:225)
at scala.concurrent.Await$.$anonfun$result$1(package.scala:201)
at scala.concurrent.BlockContext$DefaultBlockContext$.blockOn(BlockContext.scala:62)
18行目で、FutureはFirstObjを初期化し、FirstObjのインスタンスは内部でSecondObjを初期化しようとします。 また、21行目の Future は、SecondObjを初期化しようとします。 これにより、デッドロック状態が発生する可能性があります。
4.2. オブジェクト内のlazy valsの順次評価
オブジェクト内でlazyvals を宣言し、それらに同時にアクセスしてみましょう。 このような場合、遅延値は順番に実行されます。
object LazyValStore {
lazy val squareOf5 = println(square(5))
lazy val squareOf6 = println(square(6))
def square(n: Int): Int = n * n
}
object SequentialLazyVals extends App {
def run = {
val result = Future.sequence(Seq(
Future {
LazyValStore.squareOf5
},
Future {
LazyValStore.squareOf6
}
))
Await.result(result, 15.second)
}
run
}
コンパイラーは、すべての lazyvalにモニターを導入します。 このため、コンパイラは初期化中にインスタンス全体をロックします。 複数のスレッドがインスタンスにアクセスしようとすると、スレッドはすべての lazyvalsが初期化されるまで待機する必要があります。
5. 結論
このチュートリアルでは、Scalaの lazyvalの落とし穴を探りました。 valをlazyval に置き換えることで、コードを最適化したいと考えています。 ただし、 lazyvalがテーブルにもたらす影響を完全に認識している必要があります。
いつものように、完全なコードはGitHubでから入手できます。