スレッド・ローカル変数
スレッド・ローカル変数は、ThreadLocal型の変数です。スレッド・ローカル変数にアクセスする各スレッドには、独立して初期化される変数の独自のコピーがあります。スレッド・ローカル変数の値の書込みまたは読取りを行うには、それぞれset
メソッドとget
メソッドを呼び出します。通常、スレッド・ローカル変数は、多くのコンポーネントが簡単に到達できるように、final static
フィールドとして宣言されます。
次の例では、クラスTLDBConn
はデータベース接続を表します。TLBDBConn::open
メソッドは、文字列とユーザーの名前を出力します。クラスTLServer
は、データベース自体を表します。これには、ユーザーの名前を含む文字列を返す1つのメソッドTLServer::fetchOrder
が含まれます。クラスTLApplication
は複数のTLDBConn
オブジェクトを作成し、それぞれ異なるユーザーにより、それぞれ独自のスレッドで作成されます。TLApplication::testConnection
は、スレッドを同時に実行できるように、スレッドの継続時間をランダムに変化させます。
図14-6 User.java
public class User {
public String name;
public User(String n) {
name = n;
}
}
図14-7 TLDBConn.java
public class TLDBConn {
final static ThreadLocal<User> TLUSER = new ThreadLocal<>();
public static String open(String info) {
System.out.println(info + ": " + TLUSER.get().name);
return info + ": " + TLUSER.get().name;
}
}
図14-8 TLServer.java
public class TLServer {
public static String fetchOrder() {
return "Fetching order for " + TLDBConn.TLUSER.get().name;
}
}
図14-9 TLApplication.java
import java.util.*;
public class TLApplication {
public void testConnection(User u) {
Runnable r = () -> {
TLDBConn.TLUSER.set(u);
TLDBConn.open("Thread " + Thread.currentThread().getName() + ", testConnection");
System.out.println(TLServer.fetchOrder());
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
TLDBConn.TLUSER.set(new User(u.name + " renamed"));
TLDBConn.open("Thread " + Thread.currentThread().getName() + ", testConnection");
};
Thread t = new Thread(r, u.name);
t.start();
}
public static void main(String[] args) {
TLApplication myApp = new TLApplication();
for(int i=0 ; i<5; i++) {
myApp.testConnection(new User("user" + i));
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
TLApplication
の出力は次のようになります。
Thread user0, testConnection: user0
Fetching order for user0
Thread user1, testConnection: user1
Fetching order for user1
Thread user2, testConnection: user2
Fetching order for user2
Thread user0, testConnection: user0 renamed
Thread user1, testConnection: user1 renamed
Thread user2, testConnection: user2 renamed
Thread user3, testConnection: user3
Fetching order for user3
Thread user3, testConnection: user3 renamed
Thread user4, testConnection: user4
Fetching order for user4
Thread user4, testConnection: user4 renamed
メンバー変数TLDBConn.USER
がfinal static
として宣言されている場合でも、その値はTLApplication
によって作成されたスレッドごとに一意であることに注意してください。
また、TLServer::fetchOrder
メソッドにはパラメータがないことに注意してください。特に、User
パラメータを渡すためにTLApplication::testConnection
というコードは必要ありません。TLServer::fetchOrder
は、実行中のスレッドに対応するTLDBConn.USER
スレッド・ローカル変数に直接アクセスできます。
return "Fetching order for " + TLDBConn.TLUSER.get().name;
したがって、スレッド・ローカル変数を使用すると、メソッド引数を非表示にできます。
スレッド・ローカル変数の継承
親スレッドが子スレッドを開始する場合、親スレッドのスレッド・ローカル変数の値はいずれも子スレッドによって継承されません。ただし、子スレッドがその親のスレッド・ローカル変数の値を継承したい場合は、かわりにInheritableThreadLocalクラスを使用してスレッド・ローカル変数を作成します。
次の例では、TLUSER
という名前のThreadLocalに加えて、TLADMIN
という名前のInheritableThreadLocal変数が含まれています。
図14-10 TLDBConn.java
public class TLDBConn {
final static ThreadLocal<User> TLUSER = new ThreadLocal<>();
final static InheritableThreadLocal<User> TLADMIN = new InheritableThreadLocal<>();
public static String open(String info) {
System.out.println(info + ": " + TLUSER.get().name);
return info + ": " + TLUSER.get().name;
}
}
次のメソッドは、スレッド内でchildThread
という名前のスレッドを起動します。スレッドchildThread
は、TLADMIN
という名前のInheritableThreadLocal
変数の値を取得し、TLUSER
という名前のThreadLocal
変数の値を取得しようとします。
public void testConnectionWithInheritableTL(User u) {
Runnable r = () -> {
TLDBConn.TLUSER.set(u);
TLDBConn.TLADMIN.set(new User("Admin"));
TLDBConn.open("Thread " + Thread.currentThread().getName() + ", testConnection");
System.out.println(TLServer.fetchOrder());
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread childThread = new Thread(
() -> {
System.out.println("Child thread");
System.out.println("TLADMIN: " + TLDBConn.TLADMIN.get().name);
try {
System.out.println("TLUSER: " + TLDBConn.TLUSER.get().name);
} catch (NullPointerException e) {
System.out.println("NullPointerException: TLUSER hasn't beet set");
}
}
);
childThread.start();
TLDBConn.TLUSER.set(new User(u.name + " renamed"));
TLDBConn.open("Thread " + Thread.currentThread().getName() + ", testConnection");
};
Thread t = new Thread(r, u.name);
t.start();
}
このメソッドを呼び出すと、childThread
のインスタンス化の次の文はNullPointerExceptionをスローします。
System.out.println("TLUSER: " + TLDBConn.TLUSER.get().name);
ThreadLocal変数TLUSER
の値は、childThread
によって継承されていません。ただし、InheritableThreadLocal変数TLADMIN
の値は、childThread
によって継承されています。childThread
が起動すると、次のような内容が出力されます。
Child thread
TLADMIN: Admin
NullPointerException: TLUSER hasn't beet set
スレッド・ローカル変数の問題
残念ながら、スレッド・ローカル変数には設計上の欠陥がいくつかあります。
ノート:
スコープ値は、スレッド・ローカル変数のこれらの問題に対処できます。制約なしの変動性
すべてのスレッド・ローカル変数は可変です。これにより、共有状態を更新するコンポーネントとその更新順序をアプリケーションのコードで識別することが困難になる場合があります。「スレッド・ローカル変数」で説明されている例では、TLApplication::testConnection
はTLDBConn.TLUSER
を新しい値で再割当てします。
Runnable r = () -> {
TLDBConn.TLUSER.set(u);
TLDBConn.open("Thread " + Thread.currentThread().getName() + ", testConnection");
System.out.println(TLServer.fetchOrder());
// ...
TLDBConn.TLUSER.set(new User(u.name + " renamed"));
TLDBConn.open("Thread " + Thread.currentThread().getName() + ", testConnection");
};
制限なしの存続期間
Javaランタイムは、スレッドの存続期間中、またはスレッド内のコードがスレッド・ローカル変数のremove
メソッドを呼び出すまで、スレッド・ローカル変数のスレッドのインカネーションを保持します。このメソッドの呼出しを省略すると、Javaランタイムが必要以上に長時間スレッド・データを保持する可能性があります。スレッド・プールを使用している場合、あるタスクで設定されたスレッド・ローカル変数内の値が別のタスクにリークする可能性があります。スレッド・ローカル変数の値をスレッド内で複数回設定した場合、スレッドがremove
メソッドを安全に呼び出すことができる明確なポイントがなく、長期メモリー・リークが発生する可能性があります。
高コストの継承
親スレッドのスレッド・ローカル変数は子スレッドによって継承できるため、大量のスレッドを使用している場合はスレッド・ローカル変数のオーバーヘッドが悪化することがあります。