技術メモ

技術メモ

ラフなメモ

Javaでsyncronizedを試す

スレッドまわりの勉強のために以下の本を読んでいます。(第14章:スレッド)

プログラミング言語 Java 第4版

プログラミング言語 Java 第4版

syncronized についてスレッドの排他制御の挙動を確認してみようと思います。

syncronized

syncronized排他制御する方法として大きく以下の2つの方法があります。

  • Synchronizedメソッド
  • Synchronized文

それぞれのsyncronizedの挙動を確認します。

syncronizedメソッド

1つのスレッドがオブジェクトに対してsyncronizedメソッドを呼び出すと、オブジェクトのロックを取得してメソッドを実行します。
メソッド終了時にロックを開放します。
ロックはスレッド単位になります。
同じオブジェクトに対して、syncronizedメソッドを呼び出した他のスレッドは、ロックを取得できるまで待機します。

確認してみる

実験用メソッドのコード

package sample;

public class MyClass {

    public void myInstanceMethod() {
        System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getStackTrace()[1].getMethodName() + " start");
        sleep();
        System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getStackTrace()[1].getMethodName() + " end");
    }

    public synchronized void syncInstanceMethodA() {
        System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getStackTrace()[1].getMethodName() + " start");
        sleep();
        System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getStackTrace()[1].getMethodName() + " end");
    }

    public synchronized void syncInstanceMethodB() {
        System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getStackTrace()[1].getMethodName() + " start");
        sleep();
        System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getStackTrace()[1].getMethodName() + " end");
    }

    public static void myStaticMethod() {
        System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getStackTrace()[1].getMethodName() + " start");
        sleep();
        System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getStackTrace()[1].getMethodName() + " end");
    }

    public static synchronized void syncStaticMethodA() {
        System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getStackTrace()[1].getMethodName() + " start");
        sleep();
        System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getStackTrace()[1].getMethodName() + " end");
    }

    public static synchronized void syncStaticMethodB() {
        System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getStackTrace()[1].getMethodName() + " start");
        sleep();
        System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getStackTrace()[1].getMethodName() + " end");
    }

    private static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static void sleep() {
        try {
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

結果サマリ

No インスタンスが同じかどうか インスタンスメソッド syncronizedインスタンスメソッド クラスメソッド syncronizedクラスメソッド 結果
1 同一インスタンス 可能
2 同一インスタンス 可能
3 同一インスタンス 可能
4 同一インスタンス 可能
5 同一インスタンス 不可
6 同一インスタンス 可能
7 同一インスタンス 可能
8 同一インスタンス 可能
9 同一インスタンス 可能
10 同一インスタンス 不可
11 インスタンス
12 インスタンス
13 インスタンス
14 インスタンス
15 インスタンス 可能
16 インスタンス
17 インスタンス
18 インスタンス
19 インスタンス
20 インスタンス 不可

1.インスタンスメソッドとインスタンスメソッド

同時実行可能

    @Test
    public void 同時実行可_インスタンスメソッド() throws InterruptedException {
        MyClass clazz = new MyClass();
        Runnable task = clazz::myInstanceMethod;

        new Thread(task).start();
        new Thread(task).start();
        Thread.sleep(10000);
    }

結果

Thread-1: myInstanceMethod start
Thread-2: myInstanceMethod start
Thread-1: myInstanceMethod end
Thread-2: myInstanceMethod end

2.インスタンスメソッドとsyncronizedインスタンスメソッド

同時実行可能。

   @Test
    public void 同時実行可_インスタンスメソッドとsyncインスタンスメソッド() throws InterruptedException {
        MyClass clazz = new MyClass();
        Runnable taskNonSync = clazz::myInstanceMethod;
        Runnable taskSync = clazz::syncInstanceMethodA;

        new Thread(taskNonSync).start();
        new Thread(taskSync).start();
        Thread.sleep(5000);
    }

結果

Thread-1: myInstanceMethod start
Thread-2: syncInstanceMethodA start
Thread-1: myInstanceMethod end
Thread-2: syncInstanceMethodA end

3.インスタンスメソッドとクラスメソッド

同時実行可能

   @Test
    public void 同時実行可_インスタンスメソッドとクラスメソッド() throws InterruptedException {
        MyClass clazz = new MyClass();
        Runnable taskInstance = clazz::myInstanceMethod;
        Runnable taskStatic = MyClass::myStaticMethod;

        new Thread(taskInstance).start();
        new Thread(taskStatic).start();
        Thread.sleep(10000);
    }
Thread-2: myStaticMethod start
Thread-1: myInstanceMethod start
Thread-1: myInstanceMethod end
Thread-2: myStaticMethod end

4.インスタンスメソッドとsyncronizedクラスメソッド

同時実行可能

   @Test
    public void 同時実行可_syncインスタンスメソッドとクラスメソッド() throws InterruptedException {
        MyClass clazz = new MyClass();
        Runnable taskInstance = clazz::syncInstanceMethodA;
        Runnable taskStatic = MyClass::myStaticMethod;

        new Thread(taskInstance).start();
        new Thread(taskStatic).start();
        Thread.sleep(10000);
    }

結果

Thread-2: myStaticMethod start
Thread-1: syncInstanceMethodA start
Thread-2: myStaticMethod end
Thread-1: syncInstanceMethodA end

5.syncインスタンスメソッドとsyncインスタンスメソッド

同時実行は不可です。この結果からもわかるように syncronized がメソッド修飾子として指定されているオブジェクトのロックを取得するときは、メソッドのロックをしているわけではなく、参照先インスタンスのオブジェクトのロックを取得していることになります。

   @Test
    public void 同時実行不可_syncインスタンスメソッドとsyncインスタンスメソッド() throws InterruptedException {
        MyClass clazz = new MyClass();
        Runnable taskSyncA = clazz::syncInstanceMethodA;
        Runnable taskSyncB = clazz::syncInstanceMethodB;

        new Thread(taskSyncA).start();
        new Thread(taskSyncB).start();
        Thread.sleep(10000);
    }

結果

Thread-1: syncInstanceMethodA start
Thread-1: syncInstanceMethodA end
Thread-2: syncInstanceMethodB start
Thread-2: syncInstanceMethodB end

6.syncインスタンスメソッドとクラスメソッド

同時実行可能

   @Test
    public void 同時実行可_syncインスタンスメソッドとクラスメソッド() throws InterruptedException {
        MyClass clazz = new MyClass();
        Runnable taskSync = clazz::syncInstanceMethodA;
        Runnable taskStatic = MyClass::myStaticMethod;

        new Thread(taskSync).start();
        new Thread(taskStatic).start();
        Thread.sleep(10000);
    }

結果

Thread-1: syncInstanceMethodA start
Thread-2: myStaticMethod start
Thread-1: syncInstanceMethodA end
Thread-2: myStaticMethod end

7.syncインスタンスメソッドとsyncクラスメソッド

同時実行可能。インスタンスのロックとクラスのロックは異なるため、同時に実行できます。書籍にも以下のようにあります。

staticの同期されたメソッドでClassオブジェクトのロックを獲得することで、そのクラスのオブジェクトに何らかの影響を与えたりはしません。他のスレッドがstaticの同期されたメソッドでClassオブジェクトのロックを保持していても、そのクラスのオブジェクトに対する同期されたメソッドの呼び出しができます。

   @Test
    public void 同時実行可_syncインスタンスメソッドとstaticメソッド() throws InterruptedException {
        MyClass clazz = new MyClass();
        Runnable taskSync = clazz::syncInstanceMethodA;
        Runnable taskSyncStatic = MyClass::syncStaticMethodA;

        new Thread(taskSync).start();
        new Thread(taskSyncStatic).start();
        Thread.sleep(10000);
    }

結果

Thread-2: syncStaticMethodA start
Thread-1: syncInstanceMethodA start
Thread-2: syncStaticMethodA end
Thread-1: syncInstanceMethodA end

8.クラスメソッドとクラスメソッド

同時実行可能

   @Test
    public void 同時実行可_クラスメソッドとクラスメソッド() throws InterruptedException {
        Runnable taskStaticA = MyClass::myStaticMethod;
        Runnable taskStaticB = MyClass::myStaticMethod;

        new Thread(taskStaticA).start();
        new Thread(taskStaticB).start();
        Thread.sleep(10000);
    }

結果

Thread-1: myStaticMethod start
Thread-2: myStaticMethod start
Thread-1: myStaticMethod end
Thread-2: myStaticMethod end

9.クラスメソッドとsyncクラスメソッド

   @Test
    public void 同時実行可_syncインスタンスメソッドとstaticメソッド() throws InterruptedException {
        Runnable taskStatic = MyClass::myStaticMethod;
        Runnable taskSyncStatic = MyClass::syncStaticMethodA;

        new Thread(taskStatic).start();
        new Thread(taskSyncStatic).start();
        Thread.sleep(10000);
    }

結果

Thread-2: syncStaticMethodA start
Thread-1: myStaticMethod start
Thread-1: myStaticMethod end
Thread-2: syncStaticMethodA end

10.syncクラスメソッドとsyncクラスメソッド

同時実行不可。

   @Test
    public void 同時実行不可_syncクラスメソッドとsyncクラスメソッド() throws InterruptedException {
        Runnable taskSyncStatic = MyClass::syncStaticMethodA;

        new Thread(taskSyncStatic).start();
        new Thread(taskSyncStatic).start();
        Thread.sleep(10000);
    }

結果

Thread-1: syncStaticMethodA start
Thread-1: syncStaticMethodA end
Thread-2: syncStaticMethodA start
Thread-2: syncStaticMethodA end

15.異なるインスンタンスでsyncインスタンスメソッドとsyncインスタンスメソッド

同時実行可能。synchronizedはそのインスタンスのオブジェクトに対するロックを取得するため、インスタンスが異なればロックも異なるため同時に実行できます。

   @Test
    public void 同時実行可_異なるインスタンスの異なるsyncメソッド() throws InterruptedException {
        MyClass clazz1 = new MyClass();
        MyClass clazz2 = new MyClass();
        Runnable taskClazz1 = clazz1::syncInstanceMethodA;
        Runnable taskClazz2 = clazz2::syncInstanceMethodB;

        new Thread(taskClazz1).start();
        new Thread(taskClazz2).start();
        Thread.sleep(10000);
    }

結果

Thread-1: syncInstanceMethodA start
Thread-2: syncInstanceMethodB start
Thread-1: syncInstanceMethodA end
Thread-2: syncInstanceMethodB end

20.異なるsyncクラスメソッド

同時実行不可。異なるsynchronizedクラスメソッドでも、同じクラスのロックを取得するため、同時実行できません。

   @Test
    public void 同時実行不可_異なるsyncクラスメソッド() throws InterruptedException {
        Runnable taskStaticSyncA = MyClass::syncStaticMethodA;
        Runnable taskStaticSyncB = MyClass::syncStaticMethodB;

        new Thread(taskStaticSyncA).start();
        new Thread(taskStaticSyncB).start();
        Thread.sleep(10000);
    }

結果

Thread-1: syncStaticMethodA start
Thread-1: syncStaticMethodA end
Thread-2: syncStaticMethodB start
Thread-2: syncStaticMethodB end

syncronized文

synchronizedメソッドよりも短い領域/期間だけ同期することができます。これはパフォーマンスの観点から長い領域に対するロックよりも有効です。

synchronized文は以下のような形になります。exprにはオブジェクト参照の結果が入ります。ロックを取得するとブロック内のstatementが実行されます。

synchronized (expr) {
    statement
}

ロック対象という意味では以下のコードは同じです。コンパイル後のバイトコードは異なります。

synchronizedメソッド

   public synchronized void syncInstanceMethodA() {
        System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getStackTrace()[1].getMethodName() + " start");
        sleep();
        System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getStackTrace()[1].getMethodName() + " end");
    }

synchronized文

   public void syncInstanceMethodA() {
        synchronized (this) {
        System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getStackTrace()[1].getMethodName() + " start");
        sleep();
        System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getStackTrace()[1].getMethodName() + " end");
        }
    }

またコンストラクタにsynchronized文を組み込むことができます。thisに対して同期をとると、各呼び出しで異なるオブジェクトが使用されるため、排他制御が担保できません。

   public MyClassStatement(int valA, int valB) {
        synchronized (MyClassStatement.class) {
            this.valA = valA;
            this.valB = valB;
        }
    }

確認してみる

実験用メソッドのコード

package sample_14_3;

public class MyClassStatement {

    int valA = 0, valB = 0;
    Object lockA = new Object();
    Object lockB = new Object();

    public int syncGetA() {
        System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getStackTrace()[1].getMethodName() + " start");
        synchronized (lockA) {
            System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getStackTrace()[1].getMethodName() + " locked.");
            sleep();
            System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getStackTrace()[1].getMethodName() + " end");
            return valA;
        }
    }

    public void syncIncA() {
        System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getStackTrace()[1].getMethodName() + " start");
        synchronized (lockA) {
            System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getStackTrace()[1].getMethodName() + " locked.");
            sleep();
            valA++;
        }
        System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getStackTrace()[1].getMethodName() + " end");
    }

    public int syncGetB() {
        System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getStackTrace()[1].getMethodName() + " start");
        synchronized (lockB) {
            System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getStackTrace()[1].getMethodName() + " locked.");
            sleep();
            System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getStackTrace()[1].getMethodName() + " end");
            return valB;
        }
    }

    public void syncIncB() {
        System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getStackTrace()[1].getMethodName() + " start");
        synchronized (lockB) {
            System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getStackTrace()[1].getMethodName() + " locked.");
            sleep();
            valB++;
        }
        System.out.println(Thread.currentThread().getName() + ": " + Thread.currentThread().getStackTrace()[1].getMethodName() + " end");
    }

    private static void sleep() {
        try {
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

ObjectAに対するロック

同時実行不可です。ObjectAのオブジェクトのロック待ちになるためです。

   @Test
    public void 同時実行不可_objectAのロック取得待ち() throws InterruptedException {
        MyClassStatement clazz1 = new MyClassStatement();
        Runnable taskSyncStaticA1 = clazz1::syncGetA;
        Runnable taskSyncStaticA2 = clazz1::syncIncA;

        new Thread(taskSyncStaticA1).start();
        new Thread(taskSyncStaticA2).start();
        Thread.sleep(10000);
    }

結果

Thread-2: syncIncA start
Thread-1: syncGetA start
Thread-2: syncIncA locked.
Thread-1: syncGetA locked.
Thread-2: syncIncA end
Thread-1: syncGetA end

ObjectAとObjectBに対するロック

同時実行可能です。ロックするオブジェクトが異なるためです。

 public void 同時実行可_objectAとObjectBのロック() throws InterruptedException {
        MyClassStatement clazz1 = new MyClassStatement();
        Runnable taskSyncStaticA1 = clazz1::syncGetA;
        Runnable taskSyncStaticB1 = clazz1::syncIncB;

        new Thread(taskSyncStaticA1).start();
        new Thread(taskSyncStaticB1).start();
        Thread.sleep(10000);
    }

結果

Thread-2: syncIncB start
Thread-1: syncGetA start
Thread-2: syncIncB locked.
Thread-1: syncGetA locked.
Thread-2: syncIncB end
Thread-1: syncGetA end

参考