蹲厕所的熊

benjaminwhx

为什么使用0x61c88647

2017-12-26 作者: 吴海旭


  1. Why 0x61c88647?
  2. 什么时候ThreadLocal的值可以被垃圾回收?
  3. 深入分析ThreadLocal
  4. 最佳实践

原文地址:Why 0x61c88647?

在Java1.4之前,ThreadLocals会导致线程之间发生竞争。在新的设计里,每一个线程都有他们自己的ThreadLocalMap,用来提高吞吐量,然而,我们仍然面临内存泄漏的可能性,因为长时间运行线程的ThreadLocalMap中的值不会被清除。

Why 0x61c88647?

本篇简报是从Ant Kutschera的一封邮件(ThreadLocal and Thread Pools)开始,阐述了使用ThreadLocals注意到的一个问题。

在我的一个Java专家硕士课程演示中,我注意到,使用ThreadLocals从内存中释放值时并不那么明显,这可能会导致在线程复用的应用中会发生内存泄漏。

然而,ThreadLocal的代码看起来有些复杂,有一些相当吓人的数字(0x61c88647),所以我向Joachim Ansorg求助来解决这个问题。

在Java的早期版本中,ThreadLocals在多个线程进行访问的时候存在竞争问题,使得它们在多核应用程序中几乎无用。在Java 1.4中,引入了一个新的设计,设计者把ThreadLocals直接存储在Thread中。当我们现在调用ThreadLocal的get方法时,将会返回一个当前线程里的实例ThreadLocalMap(ThreadLocal的一个内部类)

我通过实验发现,当一个线程退出时,它会删除它ThreadLocal里的所有值。这发生在exit()方法中,垃圾回收之前,如果我们在使用ThreadLocal后忘记调用remove()方法,那么当线程退出后值还会存在。

ThreadLocalMap包含了对ThreadLocal的弱引用以及值的强引用,但是,它并不会判断ReferenceQueue里面哪些弱引用的值已经被清除,因为Entry不可能立即从ThreadLocalMap中清除。

在深入研究代码并试图弄清楚ThreadLocalMap是如何工作之前,我想演示一个如何使用ThreadLocals的简单例子。例如我们有一个StupidInhouseFramework类,并且从构造函数中调用抽象方法:

public abstract class StupidInhouseFramework {
  private final String title;

  protected StupidInhouseFramework(String title) {
    this.title = title;
    draw();
  }

  public abstract void draw();

  public String toString() {
    return "StupidInhouseFramework " + title;
  }
}

你可能会认为没有人会从构造函数中调用抽象方法,但是你想错了。 我甚至在JDK的类中找到了这样的地方,尽管我不记得它们在哪里。下面是类PoorUser

public class PoorUser extends StupidInhouseFramework {
  private final Long density;

  public PoorUser(String title, long density) {
    super(title);
    this.density = density;
  }

  public void draw() {
    long density_fudge_value = density + 30 * 113;
    System.out.println("draw ... " + density_fudge_value);
  }

  public static void main(String[] args) {
    StupidInhouseFramework sif = new PoorUser("Poor Me", 33244L);
    sif.draw();
  }
}

当我们执行PoorUser,会报一个NullPointerException。该字段是Long类型的包装类。 draw()方法是从父类中调用的,此时PoorUser的构造函数尚未被调用。 因此它仍然是null,当它被拆箱时会导致NullPointerException。 我们可以使用ThreadLocal来解决这个问题,即使这不是典型的用例,但是看起来很有趣。

public class HappyUser extends StupidInhouseFramework {
  private final Long density;

  private static final ThreadLocal<Long> density_param =
      new ThreadLocal<Long>();

  private static String setParams(String title, long density) {
    density_param.set(density);
    return title;
  }

  private long getDensity() {
    Long param = density_param.get();
    if (param != null) {
      return param;
    }
    return density;
  }

  public HappyUser(String title, long density) {
    super(setParams(title, density));
    this.density = density;
    density_param.remove();
  }

  public void draw() {
    long density_fudge_value = getDensity() + 30 * 113;
    System.out.println("draw ... " + density_fudge_value);
  }

  public static void main(String[] args) {
    StupidInhouseFramework sif = new HappyUser("Poor Me", 33244L);
    sif.draw();
  }
}

什么时候ThreadLocal的值可以被垃圾回收?

我曾经说过,当拥有的线程退出时,它们可以被垃圾回收。 但是,如果线程属于一个线程池,则这些值可能会或可能不会被垃圾回收。

为了证明这一点,我使用finalize()方法创建了几个类用来说明对象何时销毁。

public class MyValue {
  private final int value;

  public MyValue(int value) {
    this.value = value;
  }

  protected void finalize() throws Throwable {
    System.out.println("MyValue.finalize " + value);
    ThreadLocalTest.setMyValueFinalized();
    super.finalize();
  }
}

通过MyThreadLocal来重写了ThreadLocal的finalize方法,让它调用之前打印一些信息。

public class MyThreadLocal<T> extends ThreadLocal<T> {
  protected void finalize() throws Throwable {
    System.out.println("MyThreadLocal.finalize");
    ThreadLocalTest.setMyThreadLocalFinalized();
    super.finalize();
  }
}

ThreadLocalUser是一个封装了ThreadLocal的类。 当它变得不可达时,我们希望它里面的ThreadLocal被回收。Note:在JavaDoc中:ThreadLocal通常是一个希望把状态和线程关联起来的private static实例(例如:一个User ID或者Transaction ID)。 通过构建大量的ThreadLocals实例,我们以更戏剧性的方式展示了这个问题。

public class ThreadLocalUser {
  private final int num;
  private MyThreadLocal<MyValue> value =
    new MyThreadLocal<MyValue>();

  public ThreadLocalUser() {
    this(0);
  }

  public ThreadLocalUser(int num) {
    this.num = num;
  }

  protected void finalize() throws Throwable {
    System.out.println("ThreadLocalUser.finalize " + num);
    ThreadLocalTest.setThreadLocalUserFinalized();
    super.finalize();
  }

  public void setThreadLocal(MyValue myValue) {
    value.set(myValue);
  }

  public void clear() {
    value.remove();
  }
}

最后一个类是MyThread,用来显示线程是何时被回收的。

public class MyThread extends Thread {
  public MyThread(Runnable target) {
    super(target);
  }
  protected void finalize() throws Throwable {
    System.out.println("MyThread.finalize");
    ThreadLocalTest.setMyThreadFinalized();
    super.finalize();
  }
}

前两个测试用例说明了当使用remove()方法清理thread local以及垃圾回收器回收时会发生的情况。 booleans用于帮助我们编写单元测试。

import junit.framework.TestCase;

import java.util.concurrent.*;

public class ThreadLocalTest extends TestCase {
  private static boolean myValueFinalized;
  private static boolean threadLocalUserFinalized;
  private static boolean myThreadLocalFinalized;
  private static boolean myThreadFinalized;

  public void setUp() {
    myValueFinalized = false;
    threadLocalUserFinalized = false;
    myThreadLocalFinalized = false;
    myThreadFinalized = false;
  }

  public static void setMyValueFinalized() {
    myValueFinalized = true;
  }

  public static void setThreadLocalUserFinalized() {
    threadLocalUserFinalized = true;
  }

  public static void setMyThreadLocalFinalized() {
    myThreadLocalFinalized = true;
  }

  public static void setMyThreadFinalized() {
    myThreadFinalized = true;
  }

  private void collectGarbage() {
    for (int i = 0; i < 10; i++) {
      System.gc();
      try {
        Thread.sleep(50);
      } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        break;
      }
    }
  }

  public void test1() {
    ThreadLocalUser user = new ThreadLocalUser();
    MyValue value = new MyValue(1);
    user.setThreadLocal(value);
    user.clear();
    value = null;
    collectGarbage();
    assertTrue(myValueFinalized);
    assertFalse(threadLocalUserFinalized);
    assertFalse(myThreadLocalFinalized);
  }

  // weird case
  public void test2() {
    ThreadLocalUser user = new ThreadLocalUser();
    MyValue value = new MyValue(1);
    user.setThreadLocal(value);
    value = null;
    user = null;
    collectGarbage();
    assertFalse(myValueFinalized);
    assertTrue(threadLocalUserFinalized);
    assertTrue(myThreadLocalFinalized);
  }
}

在test3()中,我们演示一个线程关闭如何释放它的ThreadLocal值:

public void test3() throws InterruptedException {
  Thread t = new MyThread(new Runnable() {
    public void run() {
      ThreadLocalUser user = new ThreadLocalUser();
      MyValue value = new MyValue(1);
      user.setThreadLocal(value);
    }
  });
  t.start();
  t.join();
  collectGarbage();
  assertTrue(myValueFinalized);
  assertTrue(threadLocalUserFinalized);
  assertTrue(myThreadLocalFinalized);
  assertFalse(myThreadFinalized);
}

在下面一个测试中,我们使用了线程池

public void test4() throws InterruptedException {
  Executor singlePool = Executors.newSingleThreadExecutor();
  singlePool.execute(new Runnable() {
    public void run() {
      ThreadLocalUser user = new ThreadLocalUser();
      MyValue value = new MyValue(1);
      user.setThreadLocal(value);
    }
  });
  Thread.sleep(100);
  collectGarbage();
  assertFalse(myValueFinalized);
  assertTrue(threadLocalUserFinalized);
  assertTrue(myThreadLocalFinalized);
}

到目前为止,我们还没有看到任何事情发生。 接下来我们看一个有趣的测试用例。我们构建了一百个ThreadLocals并且在最后进行垃圾回收,但是没有一个MyValue对象被垃圾回收。

public void test5() throws Exception {
  for (int i = 0; i < 100; i++) {
    ThreadLocalUser user = new ThreadLocalUser(i);
    MyValue value = new MyValue(i);
    user.setThreadLocal(value);
    value = null;
    user = null;
  }
  collectGarbage();

  assertFalse(myValueFinalized);
  assertTrue(threadLocalUserFinalized);
  assertTrue(myThreadLocalFinalized);
}

在test6()中,我们可以看到由于强制垃圾收集,一些值在被回收,但是它们落后于ThreadLocalUser集合。

public void test6() throws Exception {
  for (int i = 0; i < 100; i++) {
    ThreadLocalUser user = new ThreadLocalUser(i);
    MyValue value = new MyValue(i);
    user.setThreadLocal(value);
    value = null;
    user = null;
    collectGarbage();
  }

  assertTrue(myValueFinalized);
  assertTrue(threadLocalUserFinalized);
  assertTrue(myThreadLocalFinalized);
}

你可以观察到MyValues输出如何落后的,直到程序结束,MyValues的98和99还没有被输出。

ThreadLocalUser.finalize 96
MyValue.finalize 94
ThreadLocalUser.finalize 97
MyThreadLocal.finalize
MyValue.finalize 96
MyValue.finalize 95
MyThreadLocal.finalize
ThreadLocalUser.finalize 98
ThreadLocalUser.finalize 99
MyThreadLocal.finalize
MyValue.finalize 97

深入分析ThreadLocal

当我研究ThreadLocal的时候,第一个让我注意的就是这个数字0x61c88647,每次创建一个新的ThreadLocal时,都会通过将0x61c88647添加到以前的值来获取唯一的哈希数。我昨天大部分时间都在想弄明白为什么工程师选择了这个特定的号码。如果你google搜索61c88647,你会发现一些中文的文章,还有一些与加密相关的文章。除此之外,其他的不多。

我的朋友 John Green想把它转成十进制(1640531527),但是我们发现,实际的数字是-1640531527。 更多的资料显示,这个数字是32位有符号数的无符号数字2654435769。

这个数字代表黄金比例(sqrt(5)-1)乘以2的31次方。结果是一个黄金数字,可以是2654435769或-1640531527。下面是这个计算方式:

public class ThreadHashTest {
  public static void main(String[] args) {
    long l1 = (long) ((1L << 31) * (Math.sqrt(5) - 1));
    System.out.println("as 32 bit unsigned: " + l1);
    int i1 = (int) l1;
    System.out.println("as 32 bit signed:   " + i1);
    System.out.println("MAGIC = " + 0x61c88647);
  }
}

有关黄金比例的更多信息,请查看维基百科链接以及关于C ++数据结构的书籍。

如果你仔细看了ThreadLocalMap里的哈希代码,你就能知道为什么它要这么做。标准的java.util.HashMap使用链表来解决冲突。 ThreadLocalMap只是查找下一个可用空间并在其中插入元素。 它通过位掩码来找到第一个空间,因此只有较低的几位是显着的。 如果第一个空间已满,则只需将该元素放入下一个可用空间。 HASH_INCREMENT将key放在稀疏的哈希表中,这样可以减少在我们旁边找到值的可能性。

当ThreadLocal被垃圾回收时,ThreadLocalMap中的WeakReference键被清除。 那么我们需要解决的问题是什么时候将它从ThreadLocalMap中删除。 当我们在map中通过get()来调用其他Entry时,它不会被清除。 java.util.WeakHashMap即使在get()的时候也会从去删除所有过期的Entry。 因此get()方法在ThreadLocalMap中会稍微快一点,但是可能会在表中留下过期的Entry,从而导致内存泄漏。

当调用ThreadLocal的set()时,它可能是下面3种其中的一种:

  • 首先我们可以找到Entry并设置值,在这种情况下,过期的Entry根本不会被删除。
  • 其次,我们可能会发现,我们之前的一个Entry已经过时了,在这种情况下,我们会删除两个null Entry之间运行的所有过期Entry。一旦我们找到了key,它会和过期的Entry进行交换。
  • 第三,我们的运行可能没有足够的空间来扩展,在这种情况下,Entry被放置在运行的最后一个空值,并且一些过期的Entry被清除。 这个阶段最初使用O(log2n)算法,但是如果它不能低于填充因子,那么在O(n)中执行完整的重新哈希。

最佳实践

如果您必须使用ThreadLocal,请确保在您完成该操作后立即删除该值,并且最好在将线程返回到线程池之前。 最佳做法是使用remove()而不是set(null),因为这将导致WeakReference立即被删除,并与值一起被删除。

Kind regards

Heinz



坚持原创技术分享,您的支持将鼓励我继续创作!



分享

评论