关于Java的ThreadLocal和线程安全问题

首先

在使用Struts开发系统时,遇到了Action中存储的值未能反映到JSP的问题。调查结果表明,问题的原因是在Action的execute方法中将参数存储到了Action的实例变量中并使用。以下将解释导致问题的源代码和解决方法。示例代码如下。

多线程和线程安全

线程安全指的是应用程序能够在多线程中运行(多个线程同时并行执行)而没有问题。如果不是线程安全的话,可能会出现在一个线程中对共享数据所做的更改被其他线程覆盖的情况。用于服务器的软件,如Web服务器和数据库,通常是以多线程(多进程)方式运行的,因此在开发服务器应用程序时,最好能够通过多线程来实现。

在Java中,仅限于局部变量的情况下是线程安全的。

在Java的内存空间中,主要分为两种:栈区和堆区。栈区是为每个线程准备的,栈区中的数据不会受到其他线程的影响。相反,堆区中的数据有可能被多个线程共享,根据访问顺序可能会产生意料之外的结果。局部变量由栈区管理,因此线程安全。而类变量和实例变量由堆区管理,所以不是线程安全的。

変数内容メモリ領域スレッドセーフクラス変数クラスで共用される変数ヒープ領域スレッドセーフでないインスタンス変数クラスから生成されたオブジェクトヒープ領域スレッドセーフでないローカル変数メソッドやブロック内で使われる変数スタック領域スレッドセーフ

不是线程安全的Action

我尝试实现了下面的类图,以再现在Web应用程序中正在发生的事情。

ThreadSafe.png

有以下这样的ActionForm。

public class ActionForm {

    private String message;

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

CustomAction类将ActionFrom对象从局部变量(堆栈区域)存储到实例变量(堆区域)中。

public class CustomAction extends Action {

    protected ActionForm unsafeActionForm;

    @Override
    public void execute(ActionForm safeActionForm) {
        // インスタンス変数はスレッドセーフでない。
        this.unsafeActionForm = safeActionForm;

        // 処理
        executeLogic();
    }

    public void executeLogic() {
    }
}

ThreadUnsafeAction类覆盖了unsafeActionForm对象(实例变量)的消息。在源代码中,在条件分支处返回false,不输出错误消息,但执行示例代码时,错误消息会被输出到控制台。

public class ThreadUnsafeAction extends CustomAction {

    public static final String MESSAGE = "メッセージを上書しました";

    @Override
    public void executeLogic() {
        unsafeActionForm.setMessage(MESSAGE);

        String message = unsafeActionForm.getMessage();
        if (!MESSAGE.equals(message)) {
            long id = Thread.currentThread().getId();
            System.out.println(String.format(
                    "エラー: %4d, %s, %s, 上書きに失敗しました",
                    id, message, MESSAGE
            ));
        }
    }
}

RequestProcessor类生成ActionForm对象,就像Web应用程序一样,为每个请求。

public class RequestProcessor implements Runnable {

    private CountDownLatch startLatch;
    private Action action;
    private int numberOfRequests;

    public RequestProcessor(CountDownLatch startLatch, Action action, int numberOfRequests) {
        this.startLatch = startLatch;
        this.action = action;
        this.numberOfRequests = numberOfRequests;
    }

    @Override
    public void run() {
        try {
            startLatch.await();

            for (int i = 1; i <= numberOfRequests; i++) {
                ActionForm actionForm = new ActionForm();
                actionForm.setMessage("メッセージがあります");
                action.execute(actionForm);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Main#testThreadSafe方法在多个线程中共享一个ThreadUnsafeAction对象。

public class Main {

    // スレッド数
    private static final int NUMBER_OF_THREADS = 100;
    // リクエスト数
    private static final int NUMBER_OF_REQUESTS = 10;

    public static void main(String[] args) {
        testThreadSafe(new ThreadUnsafeAction());
    }

    public static void testThreadSafe(Action action) {
        String className = action.getClass().getSimpleName();
        System.out.println(className + " start.");

        CountDownLatch startLatch = new CountDownLatch(1);
        Collection<Thread> threads = new HashSet<>();

        // 複数のスレッドでアクションを共有する。
        for (int i = 0; i < NUMBER_OF_THREADS; i++) {
            Thread thread = new Thread(new RequestProcessor(startLatch, action, NUMBER_OF_REQUESTS));
            thread.start();
            threads.add(thread);
        }

        // 同時にスレッドを実行する。
        startLatch.countDown();
        try {
            // スレッドが終了するのを待つ。
            for (Thread thread : threads) {
                thread.join();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(className + " end.");
    }
}

当执行Main#main时,最终会在控制台上输出多个错误消息。如果没有显示任何错误消息,则可以增加线程数或请求数,或者多次执行以输出错误消息。

ThreadUnsafeAction start.
エラー:   36, メッセージがあります, メッセージを上書しました, 上書きに失敗しました
エラー:   96, メッセージがあります, メッセージを上書しました, 上書きに失敗しました
エラー:  105, メッセージがあります, メッセージを上書しました, 上書きに失敗しました
エラー:   44, メッセージがあります, メッセージを上書しました, 上書きに失敗しました
エラー:   31, メッセージがあります, メッセージを上書しました, 上書きに失敗しました
エラー:   68, メッセージがあります, メッセージを上書しました, 上書きに失敗しました
エラー:   52, メッセージがあります, メッセージを上書しました, 上書きに失敗しました
エラー:   27, メッセージがあります, メッセージを上書しました, 上書きに失敗しました
エラー:   22, メッセージがあります, メッセージを上書しました, 上書きに失敗しました
エラー:  100, メッセージがあります, メッセージを上書しました, 上書きに失敗しました
エラー:   81, メッセージがあります, メッセージを上書しました, 上書きに失敗しました
エラー:   47, メッセージがあります, メッセージを上書しました, 上書きに失敗しました
エラー:   45, メッセージがあります, メッセージを上書しました, 上書きに失敗しました
エラー:  102, メッセージがあります, メッセージを上書しました, 上書きに失敗しました
ThreadUnsafeAction end.

线程安全的操作

在访问堆区数据时,会利用缓存区域。当一个线程更新了堆区数据时,实际上只是更新了缓存区域,其他线程可能仍在引用更新前的旧数据。在某个时机,缓存区域会与堆区进行同步,以达到最新状态。由于存在缓存区域,共享数据在线程间无法保证安全性。

为了使不是线程安全的操作变得线程安全,可以使用ThreadLocal类。ThreadLocal类可以保存每个线程的不同数据。由于其他线程无法访问某个线程所持有的数据,因此可以实现线程安全。

ActionFormContext类会为每个线程保存一个ActionForm对象。

public class ActionFormContext {

    private static ThreadLocal<ActionForm> actionFormThreadLocal = new ThreadLocal<>();

    public static ActionForm getActionFrom() {
        return actionFormThreadLocal.get();
    }

    public static void setActionFrom(ActionForm actionForm) {
        actionFormThreadLocal.set(actionForm);
    }

    public static void removeActionFrom() {
        actionFormThreadLocal.remove();
    }
}

CustomAction类将ActionForm对象存储在ActionFormContext类中,并在调用ActionFormContext#removeActionForm方法之前一直保持该对象。

public class CustomAction extends Action {

    @Override
    public void execute(ActionForm safeActionForm) {
        try {
            // スレッドローカルを使用することで、スレッドセーフを実現。
            ActionFormContext.setActionForm(safeActionForm);

            // 処理
            executeLogic();

        } finally {
            // スレッドローカルで確保したオブジェクトはスレッド終了時に破棄されるが、
            // Web アプリケーションのようにスレッドを共有する場合は、
            // メモリリークが起きないように明示的に開放する必要がある。
            ActionFormContext.removeActionForm();
        }
    }

    public void executeLogic() {
    }
}

ThreadSafeAction类从ActionFormContext类中获取ActionForm对象。ActionFormContext类会为每个线程返回不同的ActionForm对象。

public class ThreadSafeAction extends CustomAction {

    public static final String MESSAGE = "メッセージを上書しました";

    @Override
    public void executeLogic() {
        ActionForm safeActionForm = ActionFormContext.getActionFrom();
        safeActionForm.setMessage(MESSAGE);

        String message = safeActionForm.getMessage();
        if (!MESSAGE.equals(message)) {
            long id = Thread.currentThread().getId();
            System.out.println(String.format(
                    "エラー: %4d, %s, %s, 上書きに失敗しました",
                    id, message, MESSAGE
            ));
        }
    }
}

将Main类中的ThreadUnsafeAction类替换为ThreadSafe类,并执行Main#main,此时控制台将不再输出错误。

ThreadSafeAction start.
ThreadSafeAction end.

总结

在多个线程中共享的对象的实例变量可以同时访问,因此可能会导致意想不到的行为。因此,我们发现在多个线程之间共享的对象的实例变量是非线程安全的。我们发现通过使用ThreadLocal来管理每个线程的对象可以实现线程安全,而不是使用实例变量。

bannerAds