Javaでのsynchronized
ブロックのパフォーマンスについて、小さな論争がありました。これは理論上の問題であり、実際のアプリケーションには影響しません。
ロックを使用してセクションを同期するシングルスレッドアプリケーションを検討してください。このコードは、セクションを同期しない同じコードよりも遅く動作しますか?もしそうなら、なぜですか?同時実行性については説明しません。これは単一スレッドアプリケーションのみであるためです。
更新
おもしろい benchmark テストしてみた。しかし、それは2001年からです。最新バージョンのJDKでは、状況が劇的に変化した可能性があります。
HotSpotには3種類のロックがあります
デフォルトでは、JVMはthinロックを使用します。後でJVMが競合がないと判断した場合、シンロックはbiasedロックに変換されます。ロックのタイプを変更する操作はかなりコストがかかるため、JVMはこの最適化をすぐには適用しません。特別なJVMオプション-XX:BiasedLockingStartupDelay = delayがあり、この種の最適化をいつ適用するかをJVMに伝えます。
バイアスがかかると、そのスレッドはその後、高価なアトミック命令に頼ることなくオブジェクトをロックおよびロック解除できます。
質問への回答:状況により異なります。ただし、バイアスをかけると、ロックありとロックなしのシングルスレッドコードの平均パフォーマンスは同じになります。
synchronized
ブロックを使用すると、シングルスレッドコードの実行速度が低下します。明らかに、他のスレッドが終了するのを待つ間、他のスレッドが停止することはありませんが、同期の他の影響、つまりキャッシュの一貫性に対処する必要があります。
同期ブロックは、concurrencyだけでなく、visibilityにも使用されます。同期されたすべてのブロックはメモリバリアです。JVMは、メインメモリーの代わりに、レジスター内の変数を自由に操作できます。ただし、複数のスレッドがその変数にアクセスしないことを前提としています。同期ブロックがない場合、このデータはCPUのキャッシュに格納され、異なるCPUの異なるスレッドは同じデータを認識しません。同期ブロックを使用することにより、JVMにこのデータをメインメモリに書き込み、他のスレッドから見えるようにします。
したがって、ロックの競合から解放されていても、JVMはデータをメインメモリにフラッシュする際にハウスキーピングを実行する必要があります。
さらに、これには最適化の制約があります。 JVMは、最適化を提供するために命令を自由に並べ替えることができます。簡単な例を考えてみます。
foo++;
bar++;
対:
foo++;
synchronized(obj)
{
bar++;
}
最初の例では、コンパイラはfoo
とbar
を同時に自由にロードしてから、両方をインクリメントしてから、両方を保存します。 2番目の例では、コンパイラーmustがfoo
に対してロード/追加/保存を実行し、次にロード/追加/保存を実行します。 bar
。したがって、同期は、JREが命令を最適化する機能に影響を与える可能性があります。
(Javaメモリモデルはブライアンゲッツの Java Concurrency In Practice に関する優れた本です。)
競合しないロックの取得にはある程度のオーバーヘッドがありますが、最新のJVMでは非常に小さいです。
このケースに関連する主要な実行時最適化は「バイアスロック」と呼ばれ、 Java SE 6パフォーマンスホワイトペーパー で説明されています。
JVMとハードウェアに関連するいくつかのパフォーマンス値が必要な場合は、このオーバーヘッドを試して測定するためのマイクロベンチマークを構築できます。
必要のないときにロックを使用すると、アプリケーションの速度が低下します。小さすぎて測定できない場合や、驚くほど高い場合があります。
IMHO多くの場合、最善のアプローチは、ロックされていないコードをシングルスレッドプログラムで使用して、このコードがスレッド間で共有されることを意図していないことを明確にすることです。これは、パフォーマンスの問題よりもメンテナンスにとって重要です。
public static void main(String... args) throws IOException {
for (int i = 0; i < 3; i++) {
perfTest(new Vector<Integer>());
perfTest(new ArrayList<Integer>());
}
}
private static void perfTest(List<Integer> objects) {
long start = System.nanoTime();
final int runs = 100000000;
for (int i = 0; i < runs; i += 20) {
// add items.
for (int j = 0; j < 20; j+=2)
objects.add(i);
// remove from the end.
while (!objects.isEmpty())
objects.remove(objects.size() - 1);
}
long time = System.nanoTime() - start;
System.out.printf("%s each add/remove took an average of %.1f ns%n", objects.getClass().getSimpleName(), (double) time/runs);
}
プリント
Vector each add/remove took an average of 38.9 ns
ArrayList each add/remove took an average of 6.4 ns
Vector each add/remove took an average of 10.5 ns
ArrayList each add/remove took an average of 6.2 ns
Vector each add/remove took an average of 10.4 ns
ArrayList each add/remove took an average of 5.7 ns
パフォーマンスの観点から、4 nsが重要な場合は、非同期バージョンを使用する必要があります。
ユースケースの99%では、コードの明確さはパフォーマンスよりも重要です。明確で単純なコードも、かなり適切に機能します。
ところで、私は4.6 GHz i7 2600をOracleで使用していますJava 7u1。
比較のために、perfTest1、2、3が同じである次の場合を実行します。
perfTest1(new ArrayList<Integer>());
perfTest2(new Vector<Integer>());
perfTest3(Collections.synchronizedList(new ArrayList<Integer>()));
私は得る
ArrayList each add/remove took an average of 2.6 ns
Vector each add/remove took an average of 7.5 ns
SynchronizedRandomAccessList each add/remove took an average of 8.9 ns
共通のperfTest
メソッドを使用すると、コードを最適にインライン化できず、すべてが遅くなります
ArrayList each add/remove took an average of 9.3 ns
Vector each add/remove took an average of 12.4 ns
SynchronizedRandomAccessList each add/remove took an average of 13.9 ns
テストの順序を入れ替える
ArrayList each add/remove took an average of 3.0 ns
Vector each add/remove took an average of 39.7 ns
ArrayList each add/remove took an average of 2.0 ns
Vector each add/remove took an average of 4.6 ns
ArrayList each add/remove took an average of 2.3 ns
Vector each add/remove took an average of 4.5 ns
ArrayList each add/remove took an average of 2.3 ns
Vector each add/remove took an average of 4.4 ns
ArrayList each add/remove took an average of 2.4 ns
Vector each add/remove took an average of 4.6 ns
一つずつ
ArrayList each add/remove took an average of 3.0 ns
ArrayList each add/remove took an average of 3.0 ns
ArrayList each add/remove took an average of 2.3 ns
ArrayList each add/remove took an average of 2.2 ns
ArrayList each add/remove took an average of 2.4 ns
そして
Vector each add/remove took an average of 28.4 ns
Vector each add/remove took an average of 37.4 ns
Vector each add/remove took an average of 7.6 ns
Vector each add/remove took an average of 7.6 ns
Vector each add/remove took an average of 7.6 ns
HotSpot VMを使用している場合、JVMはsynchronized
ブロック内のリソースに競合がないことを認識し、「通常の」コードとして扱うことができると思います。
このサンプルコード(100スレッドでそれぞれ1,000,000回の反復を行う)は、同期ブロックを回避することと回避しないことのパフォーマンスの違いを示しています。
出力:
Total time(Avoid Sync Block): 630ms
Total time(NOT Avoid Sync Block): 6360ms
Total time(Avoid Sync Block): 427ms
Total time(NOT Avoid Sync Block): 6636ms
Total time(Avoid Sync Block): 481ms
Total time(NOT Avoid Sync Block): 5882ms
コード:
import org.Apache.commons.lang.time.StopWatch;
public class App {
public static int countTheads = 100;
public static int loopsPerThead = 1000000;
public static int sleepOfFirst = 10;
public static int runningCount = 0;
public static Boolean flagSync = null;
public static void main( String[] args )
{
for (int j = 0; j < 3; j++) {
App.startAll(new App.AvoidSyncBlockRunner(), "(Avoid Sync Block)");
App.startAll(new App.NotAvoidSyncBlockRunner(), "(NOT Avoid Sync Block)");
}
}
public static void startAll(Runnable runnable, String description) {
App.runningCount = 0;
App.flagSync = null;
Thread[] threads = new Thread[App.countTheads];
StopWatch sw = new StopWatch();
sw.start();
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(runnable);
}
for (int i = 0; i < threads.length; i++) {
threads[i].start();
}
do {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
} while (runningCount != 0);
System.out.println("Total time"+description+": " + (sw.getTime() - App.sleepOfFirst) + "ms");
}
public static void commonBlock() {
String a = "foo";
a += "Baa";
}
public static synchronized void incrementCountRunning(int inc) {
runningCount = runningCount + inc;
}
public static class NotAvoidSyncBlockRunner implements Runnable {
public void run() {
App.incrementCountRunning(1);
for (int i = 0; i < App.loopsPerThead; i++) {
synchronized (App.class) {
if (App.flagSync == null) {
try {
Thread.sleep(App.sleepOfFirst);
} catch (InterruptedException e) {
e.printStackTrace();
}
App.flagSync = true;
}
}
App.commonBlock();
}
App.incrementCountRunning(-1);
}
}
public static class AvoidSyncBlockRunner implements Runnable {
public void run() {
App.incrementCountRunning(1);
for (int i = 0; i < App.loopsPerThead; i++) {
// THIS "IF" MAY SEEM POINTLESS, BUT IT AVOIDS THE NEXT
//ITERATION OF ENTERING INTO THE SYNCHRONIZED BLOCK
if (App.flagSync == null) {
synchronized (App.class) {
if (App.flagSync == null) {
try {
Thread.sleep(App.sleepOfFirst);
} catch (InterruptedException e) {
e.printStackTrace();
}
App.flagSync = true;
}
}
}
App.commonBlock();
}
App.incrementCountRunning(-1);
}
}
}