Math.randomはマルチスレッドで使用しない

Androidでマルチスレッド処理による画像処理アプリを作っているときに気が付いたこと。

各ピクセルの色成分を取得し、Math.random()で乱数を加味して処理、といったプログラムを作っていたのですが、「マルチスレッドにすると速度が落ちる」現象に悩まされました。処理自体は、ピクセル毎に完全に独立していて、どう考えても「並列処理で速度が上がるプログラムの典型」だったのですけどね。
シングルスレッドで実行すると7秒で終わる処理が、2スレッドで並列処理すると9秒以上かかるのです。ただ、奇妙なのは3、4とスレッド数を増やしても処理時間がほとんど変わらないこと(使用した端末はAndroid 4.2のNexus7)。

となると考えられるのはただ一つ。どこかに「大きなボトルネック」ができている、としか思えないですよね。で、コードの一部を無効化したりして速度を調べていると……各ピクセルの処理で2回呼び出すMath.random()をコメントアウトすると劇的に速度が向上することが判明。そう、Math.random()がコードの実行を止めていたわけです。

なんでこうなる、と思って調べてみると、Javaのリファレンスに答えがありました。

java.util.Random のインスタンスはスレッドセーフです。ただし、複数のスレッドで同じ java.util.Random インスタンスを並行して使用すると、競合が発生してパフォーマンスが低下する可能性があります。マルチスレッド設計では、代わりに ThreadLocalRandom を使用することを検討してください。
Random (Java Platform SE 7)

マルチスレッドでRandomを使うと速度が落ちるよ、と。試しにスレッド毎にRandomオブジェクトを生成して、そのRandomオブジェクトで乱数を生成すると、速度低下がなくなりマルチスレッドによる高速化が実現。

まさかRandomクラス(を使うと思われるMath.random)にこんな特性があるとは知らず、解決までにかなり試行錯誤してしまいました。スタティック関数では一つのオブジェクトを使いまわす実装が多いはずですから、注意が必要ですね。マルチスレッド処理のコードにスタティック関数やオブジェクトを組み込むなら、ライブラリのドキュメントでスレッドセーフであること、複数スレッドで呼び出したときに何が起こるか、確認するようにしなくては。