HHeLiBeXの日記 正道編

日々の記憶の記録とメモ‥

java.lang.ThreadLocal クラス

Java 2 SE 5.0 から、ThreadLocalクラスにremove()メソッドが追加された。


Removes the value for this ThreadLocal. This may help reduce the storage requirements of ThreadLocals. If this ThreadLocal is accessed again, it will by default have its initialValue.
Java 2 SE 1.4 の場合、ThreadLocalクラスの使用例はこんな感じ。

ThreadLocal var = new ThreadLocal();

System.out.println("1: " + var.get());
var.set("hello");
System.out.println("2: " + var.get());
var.set(null);
System.out.println("3: " + var.get());

一方、Java 2 SE 5.0 の場合は、こんな感じ。

ThreadLocal<String> var = new ThreadLocal<String>();

System.out.println("1: " + var.get());
var.set("hello");
System.out.println("2: " + var.get());
var.remove();
System.out.println("3: " + var.get());

で、いずれも次のような出力を得る。

1: null
2: hello
3: null

しかし、「set(null)」と「remove()」は必ずしも等価ではない。
remove()メソッドJavaDocにも書いてあるように、remove()呼び出し後に再度アクセスした場合はinitialValueを持つ、とある。
例えばこんな感じである。

ThreadLocal<String> var = new ThreadLocal<String>() {
    protected String initialValue() {
        return "world";
    }
};

System.out.println("1: " + var.get());
var.set("hello");
System.out.println("2: " + var.get());
var.set(null);
System.out.println("3: " + var.get());
var.remove();
System.out.println("4: " + var.get());

この場合、結果は次のようになる。

1: world
2: hello
3: null
4: world


また、JavaDocにある、必要な記憶領域の削減、というのは、ThreadLocalクラスのソースを見ると分かるが、「set(null)」ではnullを値とするエントリが保持されるが、「remove()」を呼ぶとそのエントリ自体が削除される。
これを、ThreadLocalクラスのソースを参考に作ったプログラムで確認してみる。

ThreadLocal<String> var = new ThreadLocal<String>() {
    protected String initialValue() {
        return "world";
    }
};

Thread t = Thread.currentThread();
// java.lang.reflect.Field
Field fMap = t.getClass().getDeclaredField("threadLocals");
fMap.setAccessible(true);
Object map = fMap.get(t); // 実体は ThreadLocal.ThreadLocalMap オブジェクト
Field fSize = map.getClass().getDeclaredField("size");
fSize.setAccessible(true);

System.out.println("1: " + fSize.getInt(map));
var.set("hello");
System.out.println("2: " + fSize.getInt(map));
var.set(null);
System.out.println("3: " + fSize.getInt(map));
var.remove();
System.out.println("4: " + fSize.getInt(map));
var.get();
System.out.println("5: " + fSize.getInt(map));

fSize.setAccessible(false);
fMap.setAccessible(false);

これを実行すると、

1: 4
2: 5
3: 5
4: 4
5: 5

で、確かに一旦は保持されたエントリが、remove()メソッドの呼び出しによって削除されていることが確認できる。さらに、get()メソッドを呼び出すと、初期値が保持される分だけエントリ数が増えている。


ここまで書いておいてなんだが、実は、そもそも「set(null)」を呼び出すという記述自体がおかしくて、本来ならば「set(初期値)」であるべきなのである。initialValue()メソッドをオーバーライドしない場合はこの「初期値」がnullであるから区別がつかないだけで。