Javaマルチスレッドプログラミングロックの最適化について深く勉強します。


本文
同時環境でプログラミングする場合は、複数スレッド間の動作を同期させるためにロック機構を使用し、共有リソースの相互反発訪問を保証する必要があります。ロックをかけると性能的なダメージを与えることはよく知られているようです。しかし、ロック自体は多くの性能消耗をもたらさず、パフォーマンスは主にスレッドでロックを取得するプロセスである。もし1つのスレッドだけが競合している場合、マルチスレッド競合が存在しない場合、JVMは最適化され、ロックによる性能消費はほぼ無視できる。したがって、ロックの操作を規範化し、ロックの使用方法を最適化し、不要なスレッド競争を回避することで、プログラムの性能を向上させるだけでなく、規範化されていないロックがスレッドのデッドロック問題を引き起こし、プログラムの堅牢性を向上させることができる。以下にいくつかのロックの最適化の考え方を述べます。
一、できるだけ鍵をかけないでください。
通常のメンバー関数にロックをかけると、スレッドが得られます。この時は対象全体がロックされます。これは、この対象が提供する複数の同期方法が異なる業務に対して提供される場合、対象全体がロックされているため、1つの業務が処理される際、他の関連しないサービススレッドもwaitでなければならないという意味である。以下の例は、このような状況を示している。
LockMethod類は二つの同期方法を含み、それぞれ二つの業務処理で呼び出される:

public class LockMethod {
 public synchronized void busiA() {
  for (int i = 0; i < 10000; i++) {
   System.out.println(Thread.currentThread().getName() + "deal with bussiness A:"+i);
  }
 }
 public synchronized void busiB() {
  for (int i = 0; i < 10000; i++) {
   System.out.println(Thread.currentThread().getName() + "deal with bussiness B:"+i);
  }
 }
}
BUSSSAはスレッド類で、A業務を処理するために使われています。呼び出しはLockMethodのbusiAです。

public class BUSSB extends Thread {
 LockMethod lockMethod;
 void deal(LockMethod lockMethod){
  this.lockMethod = lockMethod;
 }

 @Override
 public void run() {
  super.run();
  lockMethod.busiB();
 }
}
TestLockMethod類は、スレッドBUSASとBUSSSBを使って業務処理を行います。

public class TestLockMethod extends Thread {
 public static void main(String[] args) {
  LockMethod lockMethod = new LockMethod();
  BUSSA bussa = new BUSSA();
  BUSSB bussb = new BUSSB();
  bussa.deal(lockMethod);
  bussb.deal(lockMethod);
  bussa.start();
  bussb.start();
 }
}
プログラムを実行すると、スレッドbussaの実行中に、bussbは関数busiBに入ることができません。このとき、ロックMethodのオブジェクトロックはスレッドbussaによって取得されました。
二、同期コードブロックを縮小し、データのみロックする。
プログラムを容易にするために、synchnoizedの大きなブロックコードがあります。このコードブロックのいくつかの操作が共有リソースに関連していない場合、それらを同期ブロックの外部に置いて、長い間のホールドロックを避けるべきです。他のスレッドが待ち続けている状態になります。特に一部の循環操作、同期I/O操作。コードの行数範囲上で同期ブロックを縮小するだけでなく、実行ロジック上で、同期ブロックを縮小するべきであり、例えば、条件に合致する再同期は、同期後に条件判断を行うのではなく、必要でない同期ブロックに入る論理をできるだけ減らすべきである。
三、鍵の中には鍵が含まれないようにしてください。
この場合、スレッドは、Aロックを取得した後、同期方法ブロックに別のオブジェクトの同期方法を呼び出して、第二のロックを獲得しています。これにより、一つの呼び出しスタックに複数のロックが要求され、マルチスレッドの場合、複雑で解析が困難な異常が発生し、デッドロックが発生する可能性があります。以下のコードはこのような状況を示しています。

synchronized(A){
 synchronized(B){
  } 
}
または同期ブロックで同期方法を呼び出しました。

synchronized(A){
 B b = objArrayList.get(0);
 b.method(); //        
}
解決策はジャンプして鍵をかけることです。ロックを含まないでください。

{
  B b = null;
 synchronized(A){
 b = objArrayList.get(0);
 }
 b.method();
}
四、ロックを私有化し、内部でロックを管理する
ロックをプライベートの対象として、外部はこのオブジェクトを入手できません。もっと安全です。オブジェクトは他のスレッドによって直接ロックされることがあります。この場合、スレッドはそのオブジェクトのロックを持っています。例えば、以下のような場合:

class A {
 public void method1() {
 }
}
class B {
 public void method1() {
  A a = new A();
  synchronized (a) { //            a.method1();
  }
 }
}
このような使い方では、対象aのオブジェクトは外部にロックされています。これを外部の複数の場所にロックして使用させるのは危険です。コードの論理フローを読むのも迷惑です。より良い方法は、クラスの内部で自分でロックを管理し、外部で同期案が必要な場合でも、インターフェース方式で同期操作を提供することです。

class A {
 private Object lock = new Object();
 public void method1() {
  synchronized (lock){
   
  }
 }
}
class B {
 public void method1() {
  A a = new A();
  a.method1();
 }
}
五、適切なロック分解を行う
次の手順を考えます。

public class GameServer {
 public Map<String, List<Player>> tables = new HashMap<String, List<Player>>();
 public void join(Player player, Table table) {
 if (player.getAccountBalance() > table.getLimit()) {
  synchronized (tables) {
  List<Player> tablePlayers = tables.get(table.getId());
  if (tablePlayers.size() < 9) {
   tablePlayers.add(player);
  }
  }
 }
 }
 public void leave(Player player, Table table) {/*  */} 
 public void createTable() {/*  */} 
 public void destroyTable(Table table) {/*  */}
}
この例では、ジョインメソッドは、一つの同期ロックのみを使用して、テーブル内のList<Player>オブジェクトを取得し、その後、プレイヤー数が9以下であるかどうかを判断し、もしそうであれば、1人のプレイヤーを追加する。何千何万というList<Player>がtablesに存在する場合、tablesロックに対する競争は非常に激しくなります。ここでは、ロックの分解を考慮することができます。迅速にデータを取り出した後、List<Player>オブジェクトをロックし、他のスレッドが素早く競合して、tablesオブジェクトのロックを獲得することができます。

public class GameServer {
    public Map < String,
    List < Player >> tables = new HashMap < String,
    List < Player >> ();
    public void join(Player player, Table table) {
        if (player.getAccountBalance() > table.getLimit()) {
            List < Player > tablePlayers = null;
            synchronized(tables) {
                tablePlayers = tables.get(table.getId());
            }
            synchronized(tablePlayers) {
                if (tablePlayers.size() < 9) {
                    tablePlayers.add(player);
                }
            }
        }
    }
    public void leave(Player player, Table table) {
        /* */
    }
    public void createTable() {
        /* */
    }
    public void destroyTable(Table table) {
        /* */
    }
}