JsonSlurperはスレッドセーフじゃない。(Groovy)

JsonSlurperとは

Groovyで便利なのが、JSON文字列を渡すと、
オブジェクトマップに変換して簡単にJSONの値にアクセスする事が出来ます。

そこで活躍してるのが、JsonSlurperというやつです。
Slurper はスルーパーと読みます。
スルスルと吸い込んで処理していくという感じです。

import groovy.json.JsonSlurper

String json="""
{
  "hoge":123,
  "piyo":456
}
"""

def map = new JsonSlurper().parseText(json)

println map
//==> [hoge:123, piyo:456]

println map.hoge
//==> 123

こんな感じで、Mapオブジェクトに変換されており、
値へのアクセスは、プロパティのようにアクセスする事ができます。

JsonSlurperはスレッドセーフではない。

シングルスレッドで使用する場合は、特に問題ありませんが。
並列処理にする場合に、予期せぬ競合エラーが発生してしまう事があります。

マルチスレッドで書き込みしている場合はもちろんなのですが、
参照して値を取り出しているだけでも競合が発生します。

そのため、スレッドセーフではないだけでなく、
そもそも、マルチスレッドでは利用する事すら出来ないもののようです。

競合が発生する

試しにJsonSlurperで作成したオブジェクトに並列にアクセスするようなコードを書いてみました。

import groovy.json.JsonSlurper
import io.reactivex.Flowable
import io.reactivex.schedulers.Schedulers


String json = """
{
  "hoge":123,
  "piyo":345,
}
"""

def map = new JsonSlurper().parseText(json)

// Flowable::rangeで 0~9のストリームを生成する
def stream = Flowable.range(0, 10).flatMap({ i ->

    // 別スレッドで動作する処理を記述
    Flowable.generate({ emitter ->
        // 処理開始
        println "${Thread.currentThread().name}: $i start"

        // mapオブジェクトにアクセス (ここでエラーが発生する場合がある)
        println "${Thread.currentThread().name}: $i ${map.hoge}"

        // スレッド切り替えのタイミングを与える
        Thread.sleep(0)

        // 処理おわり
        println "${Thread.currentThread().name}: $i end"
        emitter.onComplete()
    }).subscribeOn(Schedulers.newThread())
})

// サブスクライブ開始
stream.blockingSubscribe()

このコードを実行すると、以下のようなエラーか、
もしくはConcurrentModificationExceptionなどが発生するかと思います。
運良く発生しない場合もありますが、何回か実行すると発生することでしょう。

java.lang.NullPointerException
    at groovy.json.internal.LazyMap.buildIfNeeded(LazyMap.java:124)
    at groovy.json.internal.LazyMap.get(LazyMap.java:110)
    at groovy.lang.MetaClassImpl$6.getProperty(MetaClassImpl.java:1890)
    at org.codehaus.groovy.runtime.callsite.GetEffectivePojoPropertySite.getProperty(GetEffectivePojoPropertySite.java:64)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callGetProperty(AbstractCallSite.java:296)

LazyMapで動的に内部変数が変わってる

なぜ、並列で生成済みのMapにアクセスしているだけでこんな事が起こるのかというと。

jsonファイルをmap化する、GroovyのクラスJsonSlurperというのが

内部で生成するのがLazyMapという特殊な遅延Mapになっており、
アクセス時に内部Mapに変更を加えている為、イテレーターアクセス中に競合が発生する。

また内部操作中にアクセスが発生して、ヌルポが発生しているようです。

JsonSlurperClassicを使おう

並列で使用するのであれば、代わりにJsonSlurperClassicを使いましょう。
LazyMapが動的に内部で変更されるためにマルチスレッドでは、競合が発生する。

通常のHashMapを使う、JsonSlurperClassicというクラスを変わりに使用すれば正常に動作するようになります。

先程のサンプルは、正常に動作するようになります。

def map = new JsonSlurperClassic().parseText(json)
RxNewThreadScheduler-3: 2 start
RxNewThreadScheduler-2: 1 start
RxNewThreadScheduler-7: 6 start
RxNewThreadScheduler-4: 3 start
RxNewThreadScheduler-9: 8 start
RxNewThreadScheduler-5: 4 start
RxNewThreadScheduler-6: 5 start
RxNewThreadScheduler-1: 0 start
RxNewThreadScheduler-10: 9 start
RxNewThreadScheduler-8: 7 start
RxNewThreadScheduler-9: 8 123
RxNewThreadScheduler-1: 0 123
RxNewThreadScheduler-3: 2 123
RxNewThreadScheduler-4: 3 123
RxNewThreadScheduler-6: 5 123
RxNewThreadScheduler-2: 1 123
RxNewThreadScheduler-7: 6 123
RxNewThreadScheduler-3: 2 end
RxNewThreadScheduler-1: 0 end
RxNewThreadScheduler-9: 8 end
RxNewThreadScheduler-8: 7 123
RxNewThreadScheduler-5: 4 123
RxNewThreadScheduler-10: 9 123
RxNewThreadScheduler-4: 3 end
RxNewThreadScheduler-6: 5 end
RxNewThreadScheduler-2: 1 end
RxNewThreadScheduler-8: 7 end
RxNewThreadScheduler-7: 6 end
RxNewThreadScheduler-5: 4 end
RxNewThreadScheduler-10: 9 end

マルチスレッドでJsonSlurperの恩恵を受けたい時は、JsonSlurperClassicを利用するようにしましょう。

参考

StackOverflow にこんな記載を発見。
JsonSlurperは、内部変数にHashMapから、LazyMapを使うようになったようで、
もともとのHashMapを使う方が、JsonSlurperClassicとして残されているようです。

https://stackoverflow.com/questions/37864542/jenkins-pipeline-notserializableexception-groovy-json-internal-lazymap/37897833

Use JsonSlurperClassic instead.
Since Groovy 2.3 (note: Jenkins 2.7.1 uses Groovy 2.4.7) JsonSlurper returns LazyMap instead of HashMap.
This makes new implementation of JsonSlurper not thread safe and not serializable.

コメントを残す