問題を解決するのThreadLocal

プロジェクトチーム暁明(反復グループ:常に道路の繰り返しでは)どこで、多くの場合、既存のインタフェースに基づいていくつかの小さな機能を開発し、前提は保証に基づき、既存のユーザーの影響を受けるの繰り返しではありません。関数の反復が、明るい1ワット実装は小さなコードレベルで(自慢)が近づいている、この暁明の使用方法を見とるのThreadLocalはエレガントにそれのこの繰り返しを完了します!

そのようなThreadLocalの<のStringBuilder>としてThreadLocalの支持パラダイムので、以降と、プレゼンテーションを容易に可変のThreadLocal自体の代表的な、しかしと実施例の特定のタイプの例(例えばStringBuidler)担当。

誤解

この記事を書く前に、暁明は、インターネットのブログ上でアプリケーションシナリオと問題解決のThreadLocalのについて多くを読んで、明確にさえ満ち、記載されていません。例えば、以下は、共通のThreadLocalのプレゼンテーション(ある間違いました

ThreadLocalのは、同時マルチスレッド・プログラムの問題を解決するための新しい方法を提供し、
ThreadLocalの目的は、リソースへのマルチスレッドの共有アクセスの問題を解決することです。

暁明は読書とハンズオンラボ多くの後に締結:ThreadLocalのは、前述のようにマルチスレッドを解決することではない共有変数の問題。

正しい理解

ThreadLoal変数、その基本原理は、(すなわちStringBuilderの型変数のThreadLocal <のStringBuilder>用語)を含ん同じThreadLocalのオブジェクトであり、別のスレッドで異なるコピー(実際には異なるインスタンス)があります。

  • そのため、スレッドの各インスタンスの独自のコピーで、スレッドおよびコピーは、現在だけを使用することができます。
  • 他のスレッドにアクセスできなくするので、複数のスレッド間で共有されるだろう問題はありません。

公式文書が記述されています。

localthreadofjdk

私が読んだ後、と結論

ThreadLocalのは、スレッドローカルインスタンスを提供します。これは、各スレッドがコピーの完全に独立したインスタンスの初期化変数を使用することは通常の変数とは異なります。ThreadLocalの変数は、通常されているprivate static修正します。スレッドの終了時に、それはコピーを使用して、すべての比較的ThreadLocalのインスタンスが回復されます。

したがってThreadLocalのウェル例えばAシナリオに適した:各スレッドは、この例のそれ自身の別のインスタンスを必要とし、複数の方法の使用を必要とします同じ効果を達成するために、他の方法はもちろん、この記事を読んだ後、あなたはThreadLocalを、よりエレガントに、シンプルな達成するでしょう!

ThreadLocalの使い方

サンプルコード

最初は、私たちは、次のコードを持っている例も、その後、我々は結論を出す現象を分析します:

public class ThreadLocalDemo {

    public static void main(String[] args) throws InterruptedException {
        int threadNum = 3;
        CountDownLatch countDownLatch = new CountDownLatch(threadNum);
        for (int i = 1; i <= threadNum; i++) {
            new Thread(() -> {
                for (int j = 0; j <= 2; j++) {
                    MyUtil.add(String.valueOf(j));
                    MyUtil.print();
                }
                MyUtil.set("hello world");
                countDownLatch.countDown();
            }, "thread - " + i).start();
        }
        countDownLatch.await();
    }

    private static class MyUtil {

        public static void add(String newStr) {
            StringBuilder str = StringBuilderUtil.stringBuilderThreadLocal.get();
            StringBuilderUtil.stringBuilderThreadLocal.set(str.append(newStr));
        }

        public static void print() {
            System.out.printf("Thread name:%s , ThreadLocal hashcode:%s, Instance hashcode:%s, Value:%s\n",
                    Thread.currentThread().getName(),
                    StringBuilderUtil.stringBuilderThreadLocal.hashCode(),
                    StringBuilderUtil.stringBuilderThreadLocal.get().hashCode(),
                    StringBuilderUtil.stringBuilderThreadLocal.get().toString());
        }

        public static void set(String words) {
            StringBuilderUtil.stringBuilderThreadLocal.set(new StringBuilder(words));
            System.out.printf("Set, Thread name:%s , ThreadLocal hashcode:%s,  Instance hashcode:%s, Value:%s\n",
                    Thread.currentThread().getName(),
                    StringBuilderUtil.stringBuilderThreadLocal.hashCode(),
                    StringBuilderUtil.stringBuilderThreadLocal.get().hashCode(),
                    StringBuilderUtil.stringBuilderThreadLocal.get().toString());
        }
    }

    private static class StringBuilderUtil {
        // ThreadLocal 变量通常被 private static 修饰
        private static ThreadLocal<StringBuilder> stringBuilderThreadLocal = ThreadLocal.withInitial(() -> new StringBuilder());
    }

}

ケーススタディ

ThreadLocalの自体はStringBuilderのThreadLocalの変数型を使用し、そのような例として、パラダイムをサポートします。利用可能な()メソッドはGETを介しStringBuidler ThreadLocalのインスタンスを読み取り、StringBuilderのは、セット(T T)の方法によって提供することができます。

ヒント:たCountDownLatchクラスは、機能的に同様のカウンターを使用して実施することができるjava.util.concurrentパッケージ、下にあります。例えば、シーンがあります。タスクA、それはあなたがたCountDownLatchを達成するために、この機能を使用することができ、4つのタスクを実行するために実行するために他の後に終了するまで待ち。次回は、私たちだけでは、この機能について話すことができます。

[実行]をクリックし、コンソール出力

123

我々は見つけることができます:

  • get()メソッドを介して、同じ変数のThreadLocal、ThreadLocalの各スレッドアクセスが異なるStringBuilderのインスタンスを取得します。
  • 虽然从代码上都是对 StringBuilderUtil 类的静态 stringBuilderThreadLocal 字段进行 get() 得到 StringBuilder 实例并追加字符串,但是这并不会将所有线程追加的字符串都放进同一个 StringBuilder 中,而是每个线程将字符串追加进各自的 StringBuidler 实例内
  • 使用 set(T t) 方法后,ThreadLocal 变量所指向的 StringBuilder 实例被替换

ThreadLocal原理

方案一

我们大胆猜想一下,既然每个访问 ThreadLocal 变量的线程都有自己的一个“本地”实例副本。一个可能的方案是 ThreadLocal 维护一个 Map,Key 是当前线程,Value是ThreadLocal在当前线程内的实例。这样,线程通过该 ThreadLocal 的 get() 方案获取实例时,只需要以线程为键,从 Map 中找出对应的实例即可。该方案如下图所示

VarMap

这个方案可以满足上文提到的每个线程内部都有一个ThreadLocal 实例备份的要求。每个新线程访问该 ThreadLocal 时,都会向 Map 中添加一个新的映射,而当每个线程结束时再清除该线程对应的映射。But,这样就存在两个问题:

  • 开启线程与结束线程时我们都需要及时更新 Map,因此必需保证 Map 的线程安全。
  • 当线程结束时,需要保证它所访问的所有 ThreadLocal 中对应的映射均删除,否则可能会引起内存泄漏。

线程安全问题是JDK 未采用该方案的一个主要原因。

方案二

上面这个方案,存在多线程访问同一个 Map时可能会出现的同步问题。如果该 Map 由 Thread 维护,从而使得每个 Thread 只访问自己的 Map,就不存在这个问题。该方案如下图所示。

ThreadMap

该方案虽然没有锁的问题,但是由于每个线程在访问ThreadLocal 变量后,都会在自己的 Map 内维护该 ThreadLocal 变量与具体实例的映射,如果不删除这些引用(映射),就有可能会造成内存泄漏的问题。我们一起来看一下Jdk8是如何解决这个问题的。

ThreadLocal 在 JDK 8 中的实现

ThreadLocalMap与内存泄漏

在该方案中,Map 由 ThreadLocal 类的静态内部类 ThreadLocalMap 提供。该类的实例维护某个 ThreadLocal 与具体实例的映射。与 HashMap 不同的是,ThreadLocalMap 的每个 Entry 都是一个对 Key 的弱引用,这一点我们可以从super(k)可看出。另外,每个 Entry 中都包含了一个对 Value 的强引用。

static class Entry extends WeakReference<ThreadLocal<?>> {
  /** The value associated with this ThreadLocal. */
  Object value;

  Entry(ThreadLocal<?> k, Object v) {
    super(k);
    value = v;
  }
}

之所以使用弱引用,是因为当没有强引用指向 ThreadLocal 变量时,这个变量就可以被回收,就避免ThreadLocal 因为不能被回收而造成的内存泄漏的问题。

但是,这里又可能出现另外一种内存泄漏的问题。ThreadLocalMap 维护 ThreadLocal 变量与具体实例的映射,当 ThreadLocal 变量被回收后,该映射的键变为 null,该 Entry 无法被移除。从而使得实例被该 Entry 引用而无法被回收造成内存泄漏。

注意:Entry是对 ThreadLocal 类型的弱引用,并不是具体实例的弱引用,因此还存在具体实例相关的内存泄漏的问题。

读取实例

我们来看一下ThreadLocal获取实例的方法

public T get() {
  Thread t = Thread.currentThread();
  ThreadLocalMap map = getMap(t);
  if (map != null) {
    ThreadLocalMap.Entry e = map.getEntry(this);
    if (e != null) {
      @SuppressWarnings("unchecked")
      T result = (T)e.value;
      return result;
    }
  }
  return setInitialValue();
}

当线程获取实例时,首先会通过getMap(t)方法获取自身的 ThreadLocalMap。从如下该方法的定义可见,该 ThreadLocalMap 的实例是 Thread 类的一个字段,即由 Thread 维护 ThreadLocal 对象与具体实例的映射,这一点与上文分析一致。

ThreadLocalMap getMap(Thread t) {
  return t.threadLocals;
}

获取到 ThreadLocalMap 后,通过map.getEntry(this)方法获取该 ThreadLocal 在当前线程的 ThreadLocalMap 中对应的 Entry。该方法中的 this 即当前访问的 ThreadLocal 对象。

如果获取到的 Entry 不为 null,从 Entry 中取出值即为所需访问的本线程对应的实例。如果获取到的 Entry 为 null,则通过setInitialValue()方法设置该 ThreadLocal 变量在该线程中对应的具体实例的初始值。

设置初始值

设置初始值方法如下

private T setInitialValue() {
  T value = initialValue();
  Thread t = Thread.currentThread();
  ThreadLocalMap map = getMap(t);
  if (map != null)
    map.set(this, value);
  else
    createMap(t, value);
  return value;
}

该方法为 private 方法,无法被重载。

首先,通过initialValue()方法获取初始值。该方法为 public 方法,且默认返回 null。所以典型用法中常常重载该方法。上例中即在内部匿名类中将其重载。

然后拿到该线程对应的 ThreadLocalMap 对象,若该对象不为 null,则直接将该 ThreadLocal 对象与对应实例初始值的映射添加进该线程的 ThreadLocalMap中。若为 null,则先创建该 ThreadLocalMap 对象再将映射添加其中。

这里并不需要考虑 ThreadLocalMap 的线程安全问题。因为每个线程有且只有一个 ThreadLocalMap 对象,并且只有该线程自己可以访问它,其它线程不会访问该 ThreadLocalMap,也即该对象不会在多个线程中共享,也就不存在线程安全的问题。

设置实例

除了通过initialValue()方法设置实例的初始值,还可通过 set 方法设置线程内实例的值,如下所示。

public void set(T value) {
  Thread t = Thread.currentThread();
  ThreadLocalMap map = getMap(t);
  if (map != null)
    map.set(this, value);
  else
    createMap(t, value);
}

该方法先获取该线程的 ThreadLocalMap 对象,然后直接将 ThreadLocal 对象(即代码中的 this)与目标实例的映射添加进 ThreadLocalMap 中。当然,如果映射已经存在,就直接覆盖。另外,如果获取到的 ThreadLocalMap 为 null,则先创建该 ThreadLocalMap 对象。

防止内存泄漏

对于已经不再被使用且已被回收的 ThreadLocal 对象,它在每个线程内对应的实例由于被线程的 ThreadLocalMap 的 Entry 强引用,无法被回收,可能会造成内存泄漏。

针对该问题,ThreadLocalMap 的 set 方法中,通过 replaceStaleEntry 方法将所有键为 null 的 Entry 的值设置为 null,从而使得该值可被回收。另外,会在 rehash 方法中通过 expungeStaleEntry 方法将键和值为 null 的 Entry 设置为 null 从而使得该 Entry 可被回收。

private void set(ThreadLocal<?> key, Object value) {
  Entry[] tab = table;
  int len = tab.length;
  int i = key.threadLocalHashCode & (len-1);

  for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
    ThreadLocal<?> k = e.get();
    if (k == key) {
      e.value = value;
      return;
    }
    if (k == null) {
      replaceStaleEntry(key, value, i);
      return;
    }
  }
  tab[i] = new Entry(key, value);
  int sz = ++size;
  if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();
}

案例

对于 Java Web 应用而言,Session 保存了很多信息。很多时候需要通过 Session 获取信息,有些时候又需要修改 Session 的信息。一方面,需要保证每个线程有自己单独的 Session 实例。另一方面,由于很多地方都需要操作 Session,存在多方法共享 Session 的需求。如果不使用 ThreadLocal,可以在每个线程内构建一个 Session实例,并将该实例在多个方法间传递,如下所示。

public class SessionHandler {

  @Data
  public static class Session {
    private String id;
    private String user;
    private String status;
  }

  public Session createSession() {
    return new Session();
  }

  public String getUser(Session session) {
    return session.getUser();
  }

  public String getStatus(Session session) {
    return session.getStatus();
  }

  public void setStatus(Session session, String status) {
    session.setStatus(status);
  }

  public static void main(String[] args) {
    new Thread(() -> {
      SessionHandler handler = new SessionHandler();
      Session session = handler.createSession();
      handler.getStatus(session);
      handler.getUser(session);
      handler.setStatus(session, "close");
      handler.getStatus(session);
    }).start();
  }
}

该方法是可以实现需求的。但是每个需要使用 Session 的地方,都需要显式传递 Session 对象,方法间耦合度较高,给人的感觉并不优雅。

这里使用 ThreadLocal 重新实现该功能如下所示。

public class SessionHandler {

  public static ThreadLocal<Session> session = ThreadLocal.<Session>withInitial(() -> new Session());

  @Data
  public static class Session {
    private String id;
    private String user;
    private String status;
  }

  public String getUser() {
    return session.get().getUser();
  }

  public String getStatus() {
    return session.get().getStatus();
  }

  public void setStatus(String status) {
    session.get().setStatus(status);
  }

  public static void main(String[] args) {
    new Thread(() -> {
      SessionHandler handler = new SessionHandler();
      handler.getStatus();
      handler.getUser();
      handler.setStatus("close");
      handler.getStatus();
    }).start();
  }
}

あなたは、変換後のコードは、各スレッドは、独自の個別のインスタンスを持つことができることを保証するために楽に様々な方法との間でSessionオブジェクトの前後にを渡すことはもはや必要性を見ることができます。そのうちのいくつかは、まだ少し、多くのオプションを見ている間。たとえば、あなたはまた、各スレッドは、独自のインスタンス変数が静的変数のメソッドの間で共有することができる持っていることを確認するために、スレッド内のローカル変数を作成することができます。あなたは、スレッド間のアイソレーションとメソッドの間で共有変数を満たすために必要がある場合でも、ThreadLocalのは非常に適切。

概要

  • ThreadLocalのは、スレッド間でデータを共有する問題を解決しません
  • 暗黙的にスレッド安全性の問題の例を避けるために、異なるスレッド内のインスタンスのコピーを個別に作成することによってThreadLocalの
  • 各スレッドは保持し、維持地図は、特定の例でThreadLocalのオブジェクトのマッピング、地図だけでそのスレッドへのアクセスを保持することによるものであるので、何もスレッドセーフとロックの問題はありません
  • エントリのThreadLocalMapが弱参照のThreadLocalへの参照で、ThreadLocalのオブジェクトを回収することができないという問題を回避することです
  • ThreadLocalMap replaceStaleEntry法(すなわち、特定の一例)を呼び出すことによってNULL値エントリオブジェクトにキーセットを回収し、オブジェクト自体エントリメモリリークを防ぐために
  • スレッド間の変数の分離に適用され、シーンの方法の間で共有ThreadLocalの
    より多くのリソースのために、マイクロチャネル公共番号「プログラマ暁明」に歓迎の注意。
    プログラマー暁明

おすすめ

転載: www.cnblogs.com/coderxx/p/12043764.html
おすすめ