[Java] 24. Javaの同期化(Synchronized)とデッドロック(Deadlock)


Study / Java    作成日付 : 2019/09/11 23:06:09   修正日付 : 2021/03/02 20:21:28

こんにちは。明月です。


この投稿はJavaの同期化(Synchronized)とデッドロック(Deadlock)に関する説明です。


以前にスレッドに関して説明したことがあります。

link - [Java] 22.スレッド(Thread)を使う方法

link - [Java] 23. スレッドプール(Threadpool)を使う方法


スレッドというのは簡単に説明するとメインプロセスから独立な処理領域を生成して並列処理することという意味です。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Example {
  // スレッドsleep関数(sleep関数のException除き用)
  private static void sleep() {
    try {
      // スレッドの1秒待機
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
  // 実行関数
  public static void main(String[] args) {
    // スレッドプール(最大に2こ生成)
    ExecutorService service = Executors.newFixedThreadPool(2);
    // スレッドで使うラムダ式
    Runnable func = () -> {
      // 0から9までの繰り返し
      for (int i = 0; i < 10; i++) {
        // コンソール出力
        System.out.println(Thread.currentThread().getName() + " " + i);
        // スレッド1秒待機
        sleep();
      }
    };
    try {
      // スレッド実行
      Future<?> f1 = service.submit(func);
      Future<?> f2 = service.submit(func);
      // スレッド終了まで待機
      f1.get();
      f2.get();
    } catch (Throwable e) {
      e.printStackTrace();
    }
    // スレッドプールの中のスレッドがすべて正常終了ならスレッドプール終了
    service.shutdown();
  }
}


上の例はスレッドの各領域から0から9までのコンソール出力の例です。別に問題があることではありません。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
// テストクラス
class Node {
  // メンバー変数
  public int data = 0;
  // データを格納する関数
  public void setData(int data) {
    // メンバー変数にデータ格納
    this.data = data;
  }
  // 変数の値を取得する関数
  public int getData() {
    // 変数の値をリターン
    return this.data;
  }
}
public class Example {
  // スレッドsleep関数(sleep関数のException除き用)
  private static void sleep() {
    try {
      // スレッドの1秒待機
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
  // 実行関数
  public static void main(String[] args) {
    // スレッドプール(最大に2こ生成)
    ExecutorService service = Executors.newFixedThreadPool(2);
    // クロージャのためのテストクラス
    final Node node = new Node();
    // スレッドで使うラムダ式
    Runnable func = () -> {
      // 0から9まで繰り返し
      for (int i = 0; i < 10; i++) {
        // 値を取得して
        int data = node.getData();
        // iを足す。
        node.setData(data + i);
        // スレッド1秒待機
        sleep();
      }
    };
    try {
      // スレッド実行
      Future<?> f1 = service.submit(func);
      Future<?> f2 = service.submit(func);
      // スレッド終了まで待機
      f1.get();
      f2.get();
    } catch (Throwable e) {
      e.printStackTrace();
    }
    // スレッドプールの中のスレッドがすべて正常ならスレッドプール終了。
    service.shutdown();
    // 0から9まで2回に足す値を出力
    System.out.println("Node data - " + node.getData());
  }
}


ここで0から9まで足すと45の値になります。それをスレッドで2回に実行したから予想値は90になると思いますが、69という値が出力しました。理由は並列処理せいで発生して上の現象みたいになります。

つまり、NodeインスタンスからgetDataでデータを取得してiの値を足します。始めのスレッドでgetDataで取得してデータが0でiの値が1といえばsetDataする時にNodeのデータは1になります。

でも二つ目のスレッドには始めのスレッドのgetDataしてsetDataするまで待てないです。同時にgetDataして各スレッドでsetDataで1を格納すれば1足す計算は2回しましたが、結果は1になります。


スレッドの中でコンソール出力して確認しましょう。


iが1の時に始めのスレッドのgetDataから1の値を取得して二つ目のスレッドのgetDataからは2の値を取得します。でもiが2の時には始めと二つ目のスレッドのgetDataからは4のデータを取得します。

つまり、二つのスレッドで同期化になってないのでこの問題が発生します。


同期化すれば問題がなくなります。Nodeインスタンスに関してlockを設定しましょう。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
// テストクラス
class Node {
  // メンバー変数
  public int data = 0;
  // データを格納する関数
  public void setData(int data) {
    // メンバー変数にデータ格納
    this.data = data;
  }
  // 変数の値を取得する関数
  public int getData() {
    // 変数の値をリターン
    return this.data;
  }
}
public class Example {
  // スレッドsleep関数(sleep関数のException除き用)
  private static void sleep() {
    try {
      // スレッドの1秒待機
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
  // 実行関数
  public static void main(String[] args) {
    // スレッドプール(最大に2こ生成)
    ExecutorService service = Executors.newFixedThreadPool(2);
    // クロージャのためのテストクラス
    final Node node = new Node();
    // スレッドで使うラムダ式
    Runnable func = () -> {
      // 0から9まで繰り返し
      for (int i = 0; i < 10; i++) {
        // nodeインスタンスにロックを掛ける
        synchronized (node) {
          // 値を取得する。
          int data = node.getData();
          // iを足す
          node.setData(data + i);
          // コンソール出力
          System.out.println(Thread.currentThread().getName() + " i = " + i + " node.getData() = " + node.getData());
        }
        // スレッド1秒待機
        sleep();
      }
    };
    try {
      // スレッド実行
      Future<?> f1 = service.submit(func);
      Future<?> f2 = service.submit(func);
      // スレッド終了まで待機
      f1.get();
      f2.get();
    } catch (Throwable e) {
      e.printStackTrace();
    }
    // スレッドプールの中のスレッドがすべて正常ならスレッドプール終了。
    service.shutdown();
    // 0から9まで2回に足す値を出力
    System.out.println("Node data - " + node.getData());
  }
}


Runnableのラムダー式の中でsynchronizedのキーワードを使いました。

synchronizedは並列処理するスレッドで特定なObjectに関して同期化するキーワードです。つまり、Nodeインスタンスで設定したsynchronizedスタック領域に進入すると他のNodeインスタンスで設定したsynchronizedにはsynchronizedが終わるまで待って進入したsynchronized領域処理が終われば進入します。

つまり、synchronizedの領域の中のgetDataとsetData、コンソール出力まではスレッド同期化になって直列処理になります。

なので上みたいに90の値を取得します。


この同期化(synchronized)はマルチスレッド環境で値を同期してデータを一貫的にできますが、プログラム設計が間違ってすればロックによってお互いにリソースを待機する状況のデッドロックになる可能性があります。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
// テストクラス
class Node {
  // メンバー変数
  public int data = 0;
  // データを格納する関数
  public void setData(int data) {
    // メンバー変数にデータ格納
    this.data = data;
  }
  // 変数の値を取得する関数
  public int getData() {
    // 変数の値をリターン
    return this.data;
  }
}
public class Example {
  // スレッドsleep関数(sleep関数のException除き用)
  private static void sleep() {
    try {
      // スレッドの1秒待機
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
  // 合算関数
  private static void sum(Node n1, Node n2) {
    // 0から9まで繰り返す
    for (int i = 0; i < 10; i++) {
      // n1にロックする。
      synchronized (n1) {
        // n2にロックする。
        synchronized (n2) {
          // 値を取得して
          int data = n1.getData();
          // iを足す。
          n1.setData(data + i);
          // 値を取得して
          data = n2.getData();
          // iを足す。
          n2.setData(data + i);
        }
      }
      // コンソール出力
      System.out.println(Thread.currentThread().getName() + " i = " + i);
      // スレッド1秒待機
      sleep();
    }
  }
  // 実行関数
  public static void main(String[] args) {
    // スレッドプール(最大に2こ生成)
    ExecutorService service = Executors.newFixedThreadPool(2);
    // クロージャのためのテストクラス
    final Node node1 = new Node();
    final Node node2 = new Node();
    try {
      // スレッド実行
      Future<?> f1 = service.submit(() -> {
        // データ合算
        sum(node1,node2);
      });
      // スレッド実行
      Future<?> f2 = service.submit(() -> {
        // データ合算
        sum(node2,node1);
      });
      // スレッド終了まで待機
      f1.get();
      f2.get()
    } catch (Throwable e) {
      e.printStackTrace();
    }
    // スレッドプールの中のスレッドがすべて正常ならスレッドプール終了。
    service.shutdown();
    // 0から9まで2回に足す値を出力
    System.out.println("Node data - " + node1.getData());
    System.out.println("Node data - " + node2.getData());
  }
}


上の例はiが3の時にデッドロックに掛けて処理が止まりました。

始めのスレッドはsum関数でnode1とnode2を入れました。二つ目のスレッドにはnode2とnode1を入れました。

そうすると始まのスレッドにはsynchronized(node1)にロックを掛けて同時に二つ目のスレッドではsynchronized(node2)にロックを掛けます。

次に始めのスレッドでsynchronized(node2)に進入しようと思えば二つ目のスレッドのロックを待つことにするし、二つ目のスレッドにはsynchronized(node1)に進入しようと思えば始めのスレッドを待つ状況になります。

つまり、二つのスレッドでお互いに待つ状況になってデッドロック(Deadlock)になります。


このデッドロックにならないためにはルールがありますが、同期化(synchronized)の中で同期化(synchronized)に作成しなければ可能です。

でも、この状況が簡単にできることではないです。上の例は私が理解しやすくために簡単に作成しましたが、関数の関数で作成すれば同期化(synchronized)の中で同期化(synchronized)になったかを確認しにくいです。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
// テストクラス
class Node {
  // メンバー変数
  public int data = 0;
  // データを格納する関数
  public void setData(int data) {
    // メンバー変数にデータ格納
    this.data = data;
  }
  // 変数の値を取得する関数
  public int getData() {
    // 変数の値をリターン
    return this.data;
  }
}
public class Example {
  // スレッドsleep関数(sleep関数のException除き用)
  private static void sleep() {
    try {
      // スレッドの1秒待機
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
  // 足す関数
  private static void add(Node n1, Node n2, int i) {
    // n2にロックする。
    synchronized (n2) {
      // 値を取得して
      int data = n1.getData();
      // iを足す。
      n1.setData(data + i);
      // 値を取得して
      data = n2.getData();
      // iを足す。
      n2.setData(data + i);
    }
  }
  // 合算関数
  private static void sum(Node n1, Node n2) {
    // 0から9まで繰り返す
    for (int i = 0; i < 10; i++) {
      // n1にロックする。
      synchronized (n1) {
        // 足す関数を呼び出す。
        add(n1, n2, i);
      }
      // コンソール出力
      System.out.println(Thread.currentThread().getName() + " i = " + i);
      // スレッド1秒待機
      sleep();
    }
  }
  // 実行関数
  public static void main(String[] args) {
    // スレッドプール(最大に2こ生成)
    ExecutorService service = Executors.newFixedThreadPool(2);
    // クロージャのためのテストクラス
    final Node node1 = new Node();
    final Node node2 = new Node();
    try {
      // スレッド実行
      Future<?> f1 = service.submit(() -> {
        // データ合算
        sum(node1,node2);
      });
      // スレッド実行
      Future<?> f2 = service.submit(() -> {
        // データ合算
        sum(node2,node1);
      });
      // スレッド終了まで待機
      f1.get();
      f2.get()
    } catch (Throwable e) {
      e.printStackTrace();
    }
    // スレッドプールの中のスレッドがすべて正常ならスレッドプール終了。
    service.shutdown();
    // 0から9まで2回に足す値を出力
    System.out.println("Node data - " + node1.getData());
    System.out.println("Node data - " + node2.getData());
  }
}

上の例はsum関数の中でadd関数を分離しました。そうすると同期化(synchronized)の中で同期化(synchronized)がよく見えないです。

どうでも同期化(synchronized)の中で同期化(synchronized)で作成することになったら同期化(synchronized)のインスタンスを一つで統一する方法もあります。

つまり、static変数を一つ宣言してlock用なインスタンスで使う方法もあります。この方法はパフォーマンスが悪くなる可能性がありますが、デッドロックを確実に避けることができます。


同期化(synchronized)のスタック領域を広く設定することでパフォーマンスが遅くなることではありませんが、同期化(synchronized)の中の処理が遅いなら全般的にパフォーマンスが遅くなります。

なので、確実に同期化が必要な領域だけ同期化(synchronized)のスタック領域設定すればデッドロックを確実に避ける方法です。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
// テストクラス
class Node {
  // メンバー変数
  public int data = 0;
  // データを格納する関数
  public void setData(int data) {
    // メンバー変数にデータ格納
    this.data = data;
  }
  // 変数の値を取得する関数
  public int getData() {
    // 変数の値をリターン
    return this.data;
  }
}
public class Example {
  // スレッドsleep関数(sleep関数のException除き用)
  private static void sleep() {
    try {
      // スレッドの1秒待機
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
  // インスタンスではなく関数による同期化(synchronized)設定、関数の中で他の同期化(synchronized)がないように注意
  private static synchronized void add(Node n1, Node n2, int i) {
    // 値を取得して
    int data = n1.getData();
    // iを足す。
    n1.setData(data + i);
    // 値を取得して
    data = n2.getData();
    // iを足す。
    n2.setData(data + i);
  }
  // 合算関数
  private static void sum(Node n1, Node n2) {
    // 0から9まで繰り返す
    for (int i = 0; i < 10; i++) {
      // 足す関数を呼び出す。
      add(n1, n2, i);
      // コンソール出力
      System.out.println(Thread.currentThread().getName() + " i = " + i);
      // スレッド1秒待機
      sleep();
    }
  }
  // 実行関数
  public static void main(String[] args) {
    // スレッドプール(最大に2こ生成)
    ExecutorService service = Executors.newFixedThreadPool(2);
    // クロージャのためのテストクラス
    final Node node1 = new Node();
    final Node node2 = new Node();
    try {
      // スレッド実行
      Future<?> f1 = service.submit(() -> {
        // データ合算
        sum(node1,node2);
      });
      // スレッド実行
      Future<?> f2 = service.submit(() -> {
        // データ合算
        sum(node2,node1);
      });
      // スレッド終了まで待機
      f1.get();
      f2.get()
    } catch (Throwable e) {
      e.printStackTrace();
    }
    // スレッドプールの中のスレッドがすべて正常ならスレッドプール終了。
    service.shutdown();
    // 0から9まで2回に足す値を出力
    System.out.println("Node data - " + node1.getData());
    System.out.println("Node data - " + node2.getData());
  }
}


add関数に同期化(synchronized)を宣言してnode1とnode2を同期化しました。注意点は関数の中で他の同期化(synchronized)がないようにすることです。

そうするとデッドロックを避けることができます。


ここまでJavaの同期化(Synchronized)とデッドロック(Deadlock)に関する説明でした。


ご不明なところや間違いところがあればコメントしてください。

最新投稿