スレッド・ローカル変数

スレッド・ローカル変数は、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.USERfinal 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::testConnectionTLDBConn.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メソッドを安全に呼び出すことができる明確なポイントがなく、長期メモリー・リークが発生する可能性があります。

高コストの継承

親スレッドのスレッド・ローカル変数は子スレッドによって継承できるため、大量のスレッドを使用している場合はスレッド・ローカル変数のオーバーヘッドが悪化することがあります。