SimpleDateFormat にまつわるぐだぐだ話。

職場では、SimpleDateFormat.parse() は問題を起こすと言うので、腫れ物に触るような扱い、というか、「使用禁止」である。んでどうしてるかというと、各自でパース機能を実装している。うーん。それはそれで微妙・・・。

と、書いたが、ウチの職場が(だけでなくおそらく日本中のそれなりの数の職場)こういうことになってしまった原因の大きな部分が、googleで、「SimpleDateFormat スレッドセーフ」というキーワードで検索したときに不動のトップで表示される
http://www.geocities.co.jp/Playtown/1245/java/unsafe_simple_date_format.html
にあると思う。(このページの作者様に何らかの「責任」があるといってるわけではない。何の責任もない。あくまで「原因」)


いつからあるのかよくわからないこのページ、おそらく参照されているバグレポートの最新のものが、1999年の4月である点や、文中紹介されている log4j のドキュメントが2000年の1月であること、同 Tomcat のものが 2000年11月であること、そして極めつけに、2000年の12月に、秋元さんという方が、jfriends-ml に、「Java スレッドプロ グラミングを読む会第 2 回議事録」という言うタイトルで、以下のような文章を前書きとして、このページの文章をそのまま(!)投稿されていることなどからして、2001年初頭には既に存在していたと考えていいだろう。

秋元です。はじめまして

今回、newsで案内を拝見して、初めて読書会に参加させていただきました。

日頃、どうもスレッドまわりをちゃんと理解してないし、どのように設計
するのが望ましいかという知識も足りないと思っていたのですが、独習す
る根気もなく、読書会という形式に期待してやってきました。

結果は大満足で、読んだ部分については身についたと思います。たぶん。
みなさんとの情報交換も為になりました。あまりお話できなかった方も
いますが、次回ぜひお話しましょう。

今後も可能な限り参加したいと思います。よろしくおねがいします。
〜中略〜
java.text.SimpleDateFormat はスレッドセーフではない

[現象]
複数のスレッドからformat()やparse()を使うと、おかしな結果になる
ことがある

SunのBug Paradeを、"+SimpleDateFormat +thread"で検索してもらうとすぐ
〜後略〜

[jfriends-ml 1494] Java スレッドプロ グラミングを読む会第 2 回議事録

秋元さんはこの時点でご自身の文章がこんなにも長い間参照されると予想されたのだろうか。予想されていなかったような気がする。綺麗にかかれているので、あまりそういう風には見えないが、この文章は「議事録」だったんである。

思うに、このページがこんなに長い間生き残った原因は、


・引用元を明示してそのまとめを簡潔に書いている
・対応策を明確に書いている
・その対応策を取らない場合のリスクについても明確に書いている。
・サンプルコードまで提示し、そのサンプルコードを実行すると瞬時に問題が再現する。
・有力なオープンソース製品がSimpleDateFormatの問題を知ってか知らずか、独自の DateFormat を使用している、と書いてあるように読めるところ。


ってなところではないだろうか。要するに説得力にあふれる名文なのだ。


そして、もっとも大きな原因。


・この文章がいつかかれたものなのか、ページのどこを見ても書いてないところ・・・。


で、この文章に書いてあることって、間違っているのか?というと結論からいって、間違ってはいないと思う。んだけど、なーんか誤解を誘う部分がある。(ちなみに、この文章がかかれた時点のJDKではガッチリ正解だったのかもしれない。さすがにそこまで調べる気力が無かった。ゴメンナサイ)

SunのBug Paradeを、"+SimpleDateFormat +thread"で検索してもらうとすぐわかるように、SimpleDateFormatクラスは、複数のインスタンスで同じオブジェクトを共有して持っており、スレッドセーフではありません。


という部分。確かに持ってる。持ってるんだけど、ほとんどは static final な定数値。普通に考えて、「複数のインスタンスで同じオブジェクトを共有」と言えそうなのはJDK1.6では以下の2変数だと思う。

    /**
     * Cache to hold the DateTimePatterns of a Locale.
     */
    private static Hashtable<String,String[]> cachedLocaleData
	= new Hashtable<String,String[]>(3);

    /**
     * Cache NumberFormat instances with Locale key.
     */
    private static Hashtable<Locale,NumberFormat> cachedNumberFormatData
	= new Hashtable<Locale,NumberFormat>(3);

それ以外は全部インスタンス内の変数。結構ある。Calendar やら、Date やら、配列やら・・・。それが SimpleDateFormat のインスタンス生成が重い理由にもなってくるわけだけど。


で、まあ、話を戻して、この2変数、問題あるんですかというと、Locale を指定されてインスタンスを生成されたときに参照されるだけのキャッシュなんで、問題無いですよね?コンストラクタでしか参照されないのだ。


ちょっと誤解されていたのか、筆がすべってうっかり書かれたのかわからないのだが、

複数のインスタンスで同じオブジェクトを共有して持っており、スレッドセーフではありません。

と書かれたために、これを読んだ子羊達は震え上がったわけである。まじっすかと。(ていうか私もびびった)SimpleDateFormat がスレッドセーフではないと言われる原因が「インスタンス間共有される変数(つまり static 変数)を内部で保持していて、format(), parse() 時にそれらを書き換えてるためなんだ!」って誤解しそうじゃありません?この文章。(くどいようだけど、この文章がかかれた時点ではそうだったのかもしれない。が、JDK1.6 では違うと思う)


上記のように誤解してしまうとこれはもう非常にキツイ。使用する都度新しいインスタンスを生成したって無駄だということになってしまう。確かに シングルトンなオブジェクトに一個だけ SimpleDateFormat を保持して、applyPattern() + ( parse() | format())をアトミックに使用できるようにガッサリと synchronized でくくるしかなくなってしまう。


SimpleDateFormat がスレッドセーフではないのは、parse(), format() 時に「インスタンス変数を」書き換えているからだ。これに関してはもう間違いなく書き換えている。

    public StringBuffer format(Date date, StringBuffer toAppendTo,
                               FieldPosition pos)
    {
        pos.beginIndex = pos.endIndex = 0;
        return format(date, toAppendTo, pos.getFieldDelegate());
    }

    // Called from Format after creating a FieldDelegate
    private StringBuffer format(Date date, StringBuffer toAppendTo,
                                FieldDelegate delegate) {
        // Convert input date to time field list
        calendar.setTime(date);


清々しいくらいに書き換えている。上は format() のソース。

    public Date parse(String text, ParsePosition pos)
    {
    
        checkNegativeNumberExpression();
    
        int start = pos.index;
        int oldStart = start;
        int textLength = text.length();

        calendar.clear(); // Clears all the time fields

上が、parse()。これはバグでもなんでもなく、「そういう仕様」なのだ。なので、将来においてもこれが「修正」されることは無いし、その必要もない。


でも大事なことは、


(1)「インスタンス変数を書き換えているからスレッドセーフではない」
(2)「static変数を書き換えているからスレッドセーフではない」


これは全然違うということ。秋元さんのページに参照されているバグレポートを見ても、それを混同している人とちゃんと区別している人がいる。「format(), parse() って名前だったら、インスタンス変数書き換えると思わないのが普通でしょ?おかしくね?」というようなもっとな投稿もある。


バグレポートを見ても、確かに、「synchronized で囲めばいい。」というワークアラウンドを示されているので、余計不安になるが、これ、2000年だったからそういうワークアラウンドになったんであって、今だったらそうはならなかったと思う。


他にも、秋元さんが「実験方法」として上げられたソースには、余計な部分や意図が不明瞭な部分が含まれていて、見る人は余計疑心暗鬼にかられたということもあるんじゃなかろうか。これは想像するに、秋元さん自身が、「バグるパターン」、「バグらないパターン」を試行錯誤しつつ検証されていて、バグる状態にしたものをとりあえず掲載されたのでは無いかと思う。(不要な部分をちょこっと書き換えるとバグらないパターンにできたりする)


このページの情報を受けて、いろいろ議論された跡なんかも他のページで追えたりして、結局何が結論なのかよーわからんようになる。多分結論がよーわからんってのが一番問題で、時系列のまとめサイトでも作らない限り、この辺が後から見た人にすっきり理解されることは無いだろう。まああれやこれやで、そろそろ指が痛くなってきたのでこれくらいにするが、その結果として、うちの職場があるのだと思う。


そのような状況も、

Java日付処理メモ(Hishidama's Java Date Memo)

このページのおかげでなくなっていくのかな、なんて思い、このほぼ足掛け10年に渡る出来事を感慨深く感じた次第。


最後に、このページでも上げられている、ThreadLocal を使用して、この問題に対処する方法についてだが、Provider クラスを作成して、スレッド毎の SimpleDateFormat を渡すようにしてもいいのだが、利用者が SimpleDateFormat をおかしくしたり?消しちゃったりすると微妙なのかなーと。それを考えると、不格好ではあるが、やはり、ラッパークラスを作成して、SimpleDateFormat をコンポジションするのがいいのかなと。


そのようなクラスをサンプル的に作成してみた。秋元さんの提示されたサンプルで動かしてもバグらないことはJDK1.6にて確認済み。(というか、秋元さんのサンプルで使用されているメソッドしか実装してない)コメントも無いが別に特別なことはしていない、と思う。

import java.text.ParseException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

final public class SafeDateFormat {
	private static final ThreadLocal<SimpleDateFormat> formatter 
		= new ThreadLocal<SimpleDateFormat>() {
		@Override
		protected SimpleDateFormat initialValue() {
			return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		}
	};

	private SafeDateFormat() {}
	
	public static final Calendar getCalendar() {
		return formatter.get().getCalendar();
	}

	public static final void applyPattern(String pattern) {
		formatter.get().applyPattern(pattern);
	}

	public static final String format(Date date) {
		return formatter.get().format(date);
	}

	public static final Date parse(final String source, final ParsePosition pos) {
		return formatter.get().parse(source, pos);
	}

	public static final Date parse(String source) throws ParseException {
		return formatter.get().parse(source);
	}
}