LoginSignup
101
84

More than 3 years have passed since last update.

Javaのsynchronizedの解説と典型的誤り集(記事の誤りや意見等あれば是非教えてください)

Last updated at Posted at 2020-01-10

synchronizedの基本

プログラムにてスレッドを分けて処理しているけれど、複数スレッドに同時に処理を行わせてはいけない箇所(クリティカルセクション)があり、そこでは処理実行可能なスレッドを制限したいときなどに使う。
synchronizedと書けばいつも内部がシングルスレッド処理になるというものではないので、何のインスタンスを使って領域を守っているかをしっかり意識すること。

synchronizedメソッド

synchronizedメソッドは、そのインスタンスにおいてsynchronized記述されているメソッドを実行できるスレッドを1つだけに制限するためのもの。
以下のクラスにおいて、someMethod()およびanotherMethod()はsynchronizedメソッドであり、このsomeClassインスタンスに対して複数のスレッドがsomeMethod()anotherMethod()を呼び出しても、一度に処理を行えるのは1スレッドだけである。

class SomeClass {
  synchronized void someMethod() {
    doSomething(); // ここに来れるのは1スレッドだけ
  }

  synchronized void anotherMethod() {
    doAnotherthing(); // ここに来れるのは1スレッドだけ
  }
  ...
}

synchronizedブロック

synchronizedブロックは、特定のコードブロックの実行を1スレッドに制限するためのもの。以下のような形式で記載する。

synchronized(instance) {
  ... // ここの処理が行えるのは、上記instanceをロックとして取得できた1スレッドのみ
}

複数の箇所で同じ同期インスタンスに対してsynchronizedブロックが記述されている場合は、複数のコードブロックにまたがって1スレッド制限がかかる。

void someMethod() {
  synchronized(instance) {
    ... // ここの処理を行っているスレッドがあると
  }
}

void anotherMethod() {
  synchronized(instance) {
    ... // ここの処理を他スレッドが行うことはできない(実行を待たされる)。
  }
}

synchronizedメソッドは、メソッド内部の処理全体をsynchronzedブロックで囲って、thisを使って同期しているのと等価と考えてよい1
つまり、以下のメソッドは、

synchronized void someMethod() {
  doSomething();
}

以下のメソッドと等価である1

void someMethod() {
  synchronized (this) {
    doSomething();
  }
}

synchronizedブロックで使用する同期インスタンスと典型的誤り

synchronizedブロックでよく用いられる同期インスタンスと典型的誤りを記す。

this

以下のコードで、doSomething()doAnotherthing()は、ともにthisを使ったsynchronizedブロックで囲まれているため、このSomeClassインスタンスに対してdoSomething()もしくはdoAnotherthing()を実行できるスレッドは一つだけになる。

class SomeClass() {
  void someMethod() {
    synchronized (this) {
      doSomething(); // ある時点でdoSomething()もしくはdoAnotherthing()を実行できるスレッドは一つだけ
    }
  }

  void anotherMethod() {
    synchronized (this) {
      doAnotherthing(); // ある時点でdoSomething()もしくはdoAnotherthing()を実行できるスレッドは一つだけ
    }
  }

  private void doSomething() {
    ...
  }

  private void doAnotherthing() {
    ...
  }

}

典型的誤り:他のSomeClassインスタンスに対する同期

同期インスタンスはthisのため、new SomeClass()が複数回行われるなどして複数のインスタンスが生成された場合、ぞれぞれのインスタンスにてthisは別物である。ゆえに、SomeClassインスタンス間でのスレッドの制限は行われない

// 以外のコードはスレッド制御かからない
(new SomeClass()).someMethod();
(new SomeClass()).anotherMethod();

これ以降の内容はnew SomeClass()が一度だけしか行われていない前提で記載する。

mLock等のプライベート変数

以下では、mLock1mLock2の二つの同期インスタンスを生成している。
mLock1にて守られているので、doSomething()を実行できるスレッドは一つだけ。同様に、mLock2にて守られているので、doAnotherthing()を実行できるスレッドも一つだけ。doSomething()doAnotherthing()の間では排他は行われない。

class SomeClass() {
  private Object mLock1 = new Object();
  private Object mLock2 = new Object();

  void someMethod() {
    synchronized (mLock1) {
      doSomething();  // ある時点でdoSomething()を実行できるスレッドは一つだけ
    }
  }

  void anotherMethod() {
    synchronized (mLock2) {
      doAnotherthing(); // ある時点でdoAnotherthing()を実行できるスレッドは一つだけ
    }
  }

  private void doSomething() {
    ...
  }

  private void doAnotherthing() {
    ...
  }
}

典型的誤り:無意味なロックインスタンス生成

以下では、mLockというインスタンスを生成して同期インスタンスに使用している。
基本的には、mLockを定義せず、thisを使ってロックすれば良いという場面が多い。例外についてはサンプルコードの下の記述を参照のこと。

class SomeClass() {
  private Object mLock = new Object(); // 定義しなくてもthisで良い

  void someMethod() {
    synchronized (mLock) {
      doSomething();
    }
  }

  void anotherMethod() {
    synchronized (mLock) {
      doAnotherthing();
    }
  }
  ...
}
コメント踏まえて加筆

@sdkeiさんに教えてもらったが、thisは他クラスからも参照可能なので、無作法な利用者にロックに利用される可能性がある。それを防ぐためにprivateなmLockを定義して使用する場合がある。synchronizedメソッドにも同様の危険性がある。
95%くらいはthisを使えば良いと気づいていないケースだと思うのでこの誤りパターンは消さないが、周辺のメソッドやクラスにfinal修飾子が記載されているようなしっかりプロジェクトでは、明確な意思のもとでmLockを定義していると場合が多いだろう。

典型的誤り:デッドロック

以下において、someMethod()およびanotherMethod()が異なるスレッドで呼ばれるとデッドロックする可能性がある。特にdoSomething()の処理時間が長い場合はすぐに発生する。

class SomeClass() {
  private Object mLock1 = new Object();
  private Object mLock2 = new Object();

  void someMethod() {
    synchronized(mLock1) { // mLock1を取得する
      doSomething();
      synchronized(mLock2) { //  mLock2の取得をしようとする
        doAnotherthing();
      }
    }
  }

  void anotherMethod() {
    synchronized(mLock2) { // mLock2を取得する
      doSomething();
      synchronized(mLock1) { // mLock1を取得しようとする *デッドロック
        doAnotherthing();
      }
    }
  }
  ...
}

典型的誤り:不十分な保護

以下において、anotherMethod()呼び出しによりmLockインスタンスが書き換わると、doSomething()が複数スレッドから実行可能になる。

class SomeClass() {
  private Object mLock = new Object();

  void someMethod() {
    synchronized(mLock) {
      doSomething();
    }
  }

  void anotherMethod() {
    mLock = new Object(); // mLockが他インスタンスになる
  }
  ...
}

典型的誤り:無意味な保護

以下において、最初のsynchronizedブロックでmLockをすでに取得しているので、二つ目のsynchronizedブロックには意味がない。たぶん開発者の意図した動作になっていないと思うので、このコードを見つけた時には真意を聞いたほうがいい。

class SomeClass() {
  private Object mLock = new Object();

  void method() {
    synchronized(mLock) { // mLockを取得する
      someMethod();
      synchronized(mLock) { // mLockを取得済みなので意味がない
        otherMethod();
      }
    }
  }
}

CLASS_NAME.class

以下では、SomeClass.classというインスタンスを使って同期を行っている。VM上でSomeClassだけに作られた唯一のインスタンス(Class<SomeClass>インスタンス)を参照するため、VM上で単一であることが保証される。
かなり強いロックになるので、ロック取得と放棄のコストを考えるとシングルスレッド実行のほうが処理速度早いなんてことにもなる。

class SomeClass() {
  void someMethod() {
    synchronized (SomeClass.class) {
      doSomething();
    }
  }

  void anotherMethod() {
    synchronized (SomeClass.class) {
      doAnotherthing();
    }
  }
  ...
}

その他の典型的誤り

synchronizedではないが、同じ同期カテゴリということで以下のような部分にも誤りが多いので記載しておく。

誤り:プリミティブ型変数への代入はアトミック操作

longとdouble以外のプリミティブ型変数への値の代入はアトミックなので誤解しがちだが、longとdoubleへ値の代入はアトミックではない

以下はスレッドセーフだが、

class SomeClass {
  private int mValue = 0;

  void assign(int value) {
    mValue = value;
  }
}

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

class SomeClass {
  private long mValue = 0;

  void assign(long value) {
    mValue = value;
  }
}

誤り:volatile修飾子をつけた変数への操作はアトミック操作

変数xに対してスレッドAで書き込みを行った後にスレッドBで読み込みを行った際、xの最新の値が返ってくるとは限らない。変数定義の際にvolatileを付けておくと、最新の値が返ってくることが保証される。

volatileを付けておくとindex++等の処理がアトミックになると記載しているサイトがあるが、これは誤りである。TECHSCOREの記載も、volatileを付けていてもa=0, b=1となることはあるので誤りである。

代替案

synchronizedを使わなくても、もっと軽量にスレッドセーフにすることができる場合があるので紹介する。

値の加算や除算をスレッドセーフに行う

java.util.concurrent.atomicに定義されたクラスを使う。
例えば、AtomicIntegerにはincrementAndGet()addAndGet(int delta)などのメソッドがあり、スレッドセーフにインクリメントや加算・除算を行うことが出来る。

書き込み中の読み書きはロックしたいが、そうでない場合は読み込みを許可したい

以下のような場合は、

  • 値の書き込みを行っているスレッドがある場合、他スレッドの書き込みと読み込みをブロックしたい
  • 書き込みが行われていないときは読みこみをブロックしたくない

以下のように、値のget()add()もsynchronizedで囲ってしまうのではなく、

class DataStorage() {
  private List<Integer> mStorage = new ArrayList<>();

  synchronized Integer get(int index) {
    return mStorage.get(index);
  }

  synchronized boolean add(Integer value) {
    return mStorage.add(value);
  }

以下のようにReentrantReadWriteLocksを使うことで、get()同士はブロックしないがadd()を処理中にはadd()get()をブロックするというようなことができる。

class DataStorage() {
  private List<Integer> mStorage = new ArrayList<>();
  private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
  private Lock r = rwl.readLock();
  private Lock w = rwl.writeLock();

  synchronized Integer get(int index) {
    r.lock(); // read lock
    try {
      return mStorage.get(index);
    } finally {
      r.unlock(); // read unlock
    }
  }

  synchronized boolean add(Integer value) {
    w.lock(); // write lock
    try {
      return mStorage.add(value);
    } finally {
      w.unlock(); // write unlock
    }
  }

この例だとCopyOnWriteArrayListを使ったほうがいいような気もするが。

ConcurrentModificationExceptionを防ぎたい場合

CopyOnWriteArrayList等のjava.util.concurrentパッケージに定義されたクラスを使う。

その他のロック

SemaphoreCountDownLatch等あり、便利なので以下を参照のこと。
Javaの排他制御(ロック)に関係するクラスまとめ


  1. VMの実装によって実行時間は異なる可能性がある。 

101
84
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
101
84