Page  1  2  3  4 

やってはいけないJavaプログラミング
日経BP 日経ソフトウエア 2002年3月号特集
「Javaプログラミング再点検」の原稿を元に再構成
(山本 啓二)

◆Singleton

「オブジェクト指向における再利用のためのデザインパターン(注11)」以来、ここ数年デザインパターンがかなりな流行の兆しを見せています。皆さんも雑誌や書籍などでその一端に触れたことがおありでしょう。デザインパターンは、長年多くのソフトウエア設計者が繰り返してきた設計上の有効なプラクティスに名前をつけたもので、開発者同士のコミュニケーションには、もはやなくてはならないものとなりつつあります。

しかし、デザインパターンに則っているからと言って、適用方法を誤れば悪い設計になってしまうので注意が必要です。筆者が見てきた中では、特にSingletonパターンがその危険が高いようです。非常に便利なパターンであること、かつ、GoF本の23パターンの中では平易なほうに含まれるものなのが原因でしょうか。

少し過激な言い方になるかもしれませんが、Singletonパターンは体のいいグローバル変数です。VM内のすべてのコード、すべてのスレッドから、同一の領域にアクセスができる、非常な危険も伴う諸刃の剣です。Singletonオブジェクト=グローバル変数はいつ誰(コード/スレッド)がアクセスしているかわかりません。Singletonの内部状態に依存したプログラミングはすべて潜在的なバグといえます。

リスト15-1

package example; 

public class SingletonCalc {

  private static SingletonCalc instance;

  private int val1, val2;

  private SingletonCalc() {
  }

  public synchronized SingletonCalc getInstance() {
    if(instance == null) {
     instance = new SingletonCalc();
    }
    return instance;
  }

  public void setFirstValue(int value) {
    val1 = value;
  }

  public void setSecondValue(int value) {
    val2 = value;
  }

  public int getResult() {
    return val1 + val2;
  }
}
/*
:
:
    SingletonCalc calc = SingletonCalc.getInstance();
    calc.setFirstValue(1);
    calc.setSecondValue(2);
    System.err.println(calc.getResult());
*/

SingletonCalcクラスは、ふたつのsetterメソッドで値を登録し、getResult()メソッドが登録された二つの値を加算した結果を返します。期待した値が返ってくることは僥倖でしかありません。自分のスレッドの処理がsetFirstValue()、setSecondValue()、getResult()と進むうちにも、他のスレッドがsetXxxxxValue()を呼び出していない保証はまったくないからです。これだけのメソッドであれば

リスト15-2

public int add(int val1, int val2) {
  return val1 + val2;
} 

のように書けばよいでしょうし、計算履歴を保存しておく必要があるならばSingletonにはできません。

◆スレッド

Javaでは非常に簡単にマルチスレッドプログラミングを行うことが可能ですが、正確なマルチスレッドプログラミングは非常に難しいものです。ここでは、Javaでのマルチスレッドプログラミングで陥りやすい失敗について触れてみます。

マルチスレッドプログラミングでは先に述べたとおり、複数のスレッドが同じオブジェクトに対して同時にアクセスできてしまう問題をいかに制御するかが肝要ですが、これには弊害がつきものです。最大の問題はデッドロックです。【図3】が典型的なデッドロックの状態を表しています。

Aに対するロックを取得しているスレッド1が続けてBに対するロックを取得しようとし、それと同時にBに対するロックを取得しているスレッド2がAに対するロックを取得しようとしています。この2つのスレッドはお互いに相手がロックを解放するのを待ちますので、この状態から抜け出ることは不可能です。 Javaでロックを取得するためには"synchronized"キーワードを利用します。【図3】をコードであらわすならば次のようになるでしょう。

【図3】

 

リスト16

package example; 

public class DeadLock {
  static byte[] a = {0x01, 0x02, 0x03};
  static byte[] b = {0x03, 0x02, 0x01};

  public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread1();
    Thread t2 = new Thread2();
    t1.start();
    t2.start();
    Thread.sleep(99999);
  }
}

class Thread1 extends Thread {
  public void run() {
    while(true) {
      synchronized (DeadLock.a) {
        System.err.println("スレッド1:ロックA取得");
        synchronized (DeadLock.b) {
          System.err.println("スレッド1:ロックB取得");
          for(int i = 0; i < DeadLock.a.length; i++) {
            DeadLock.a[i] -= DeadLock.b[i];
            DeadLock.b[i] -= DeadLock.a[i];
          }
          try {
           sleep(60);
          } catch(InterruptedException ex) {
           break;
          }
        }
      }
    }
  }
}

class Thread2 extends Thread {
  public void run() {
    while(true) {
      synchronized (DeadLock.b) {
        System.err.println("スレッド2:ロックB取得");
      synchronized (DeadLock.a) {
        System.err.println("スレッド2:ロックA取得");
        for(int i = 0; i > DeadLock.a.length; i++) { DeadLock.a[i] += DeadLock.b[i];
          DeadLock.b[i] += DeadLock.a[i];
        }
        try {
         sleep(50);
        } catch(InterruptedException ex) {
         break;
        }
      }
    }
  }
}
}

出力を見てみると以下のようになり、デッドロックが発生していることが確認できます(デッドロック発生までの行数や順序は環境によってことなることがあります)。

リスト17

C:\K2\tmp>java example.DeadLock
スレッド1:ロックA取得
スレッド1:ロックB取得
スレッド1:ロックA取得
スレッド2:ロックB取得 

この例のようにすぐ判明するデッドロックであれば問題は小さいのですが、実際には、ずっと問題なく動いていたアプリケーションが突然止まってしまった、といった形で後から発見されることが多いものです。このようなバグは再現性がタイミングに依存するために低く、デバッグは困難を極めます。

ロックの中で異なるオブジェクトへのロックを取りに行かなければ、デッドロックにはなりません。この場合であれば、a、bが共にクラス変数であるため、例えばaとbのロックを順番に獲得している個所を、synchronized (DeadLock.class) とすることでDeadLockクラスオブジェクトをロックします。

aとbがインスタンス変数であった場合は、専用のロックオブジェクト(たとえばbyte[] lockAB = new byte[0])を導入して、アクセスする際に直接aとbをロックするのではなくロックオブジェクトをロックするようにするか、あるいはプログラミングルールとして「aとbのロックを取得する際には必ずa→bの順序で取得する」と規定してもよいでしょう。

ただし、例のようにprivateでない変数へのアクセスについて、コード全体でそれらのルールが確実に守られていることを証明することは難しいです。ロックしなければならない変数へのアクセスについては、ロック取得の責任を限定することでバグを減らし、デバッグの労力を軽減するために、通常のクラス以上に隠蔽を徹底しなくてはなりません。

マルチスレッドプログラミングには、スレッドの停止方法や待ち合わせ、ロックのコストや操作の原子性など、デッドロックのほかにも考慮する必要のある点が多くあります。この分野に関しては、昨年出版された「Javaスレッドプログラミング」(Doug Lea 著、松野 良蔵 訳。翔泳社 ISBN: 4881359185)が良書としてお薦めできますので、是非一度目を通してみてください。

(注11)オブジェクト指向における再利用のためのデザインパターン Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides (著), 本位田 真一, 吉田 和樹 (翻訳) ソフトバンクパブリッシング ISBN : 4797311126。通称 GoF本とも。

Page  1  2  3  4 

このサイトについてプライバシーポリシーサイトマップ

お問合わせ

Copyright (C) 2000-2011 UL Systems, Inc. All Rights Reserved.