为什么是0x61c88647?

为什么是0x61c88647?
摘要:在Java1.4之前,ThreadLocals会导致线程争用,使它们对高性能代码毫无用处。
在新的设计中,每个线程都包含自己的线程本地映射,从而提高了吞吐量。然而,由于长时间运行的线程没有从线程本地映射中清除值,我们仍然面临内存泄漏的可能性。

在Java 1.4中,引入了一种新的设计,线程本地变量直接存储在线程中。现在,当我们在线程本地调用get()时,会回调线程,这将返回线程本地映射(ThreadLocalMap)的一个实例,这是线程本地的一个内部类。

我通过实验发现,当一个线程退出时,它也会移除所有的线程本地值。这发生在它被垃圾收集之前,在其exit()方法中。如果我们因此忘记在线程本地上调用remove(),那么当线程退出时,该值仍然可以进行垃圾收集。
线程本地映射包含对线程本地的WeakReferences和对值的普通强引用。然而,它不会查询引用队列来发现哪些弱引用已经被清除,因此条目可能不会立即从线程本地映射中清除(或者根本不会,正如我们将看到的。(
在我们深入研究代码并试图弄清楚线程本地映射是如何工作的之前,我想演示一个如何使用线程本地映射的简单例子。假设我们有一个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找到了这样做的地方,尽管我不记得它们在哪里。下面是这个可怜的用户构建的类:

    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();
      }
    }
当我们运行这个时,我们得到一个NullPointerException。该字段的类型为包装类Long。从超类调用draw()方法,此时还没有调用PoorUser的构造函数。因此,它仍然被设置为空,这将在取消装箱时导致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();
  }
}

只是一个警告,JDK 6中线程本地的JavaDocs内部的例子有一个明显的错别字,但幸运的是他们在JDK 7中修复了它。看看能不能看出区别。  线程本地值何时可以被垃圾收集? 我们已经说过,当拥有线程退出时,它们可以被垃圾收集。然而,如果线程属于一个线程池,就像我们在一些应用程序服务器中发现的那样,这些值可能被垃圾收集,也可能不被垃圾收集。  为了演示这一点,我用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,并在完成时打印出一条消息:

    public class MyThreadLocal<T> extends ThreadLocal<T> {
      protected void finalize() throws Throwable {
        System.out.println("MyThreadLocal.finalize");
        ThreadLocalTest.setMyThreadLocalFinalized();
        super.finalize();
      }
    }
      
线程本地用户是一个封装线程本地的类。当它不再可达时,我们期望它的ThreadLocal也被收集。请注意,在JavaDocs中,我们看到:ThreadLocal实例通常是私有静态类中希望将状态与线程相关联的字段(例如,用户标识或事务标识)。通过构建许多线程本地的实例,我们以一种更戏剧化的方式展示了这个问题。

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()方法清除线程本地时,以及当让垃圾收集器处理线程本地时,会发生什么情况。布尔用来帮助我们编写单元测试。
 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()中,我们演示了线程关闭是如何释放其线程本地值的:

    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);
}
  到目前为止,我们还没有看到任何重大的惊喜。现在我们来看有趣的测试用例。在下一个例子中,我们构建了一百个线程本地,然后在最后对它们进行垃圾收集。请注意,没有一个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);
}
  
您可以看到我的值集合在输出中是如何落后的。到程序结束时,我的值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正盯着我。这是HASH _ INDENCE。每次创建新的线程本地时,它都会通过将0x61c88647添加到先前的值中来获得一个唯一的哈希号。昨天大部分时间我都在想为什么工程师们选择了这个特定的数字。如果你搜索61c88647,你会发现一些中文文章和一些与加密有关的文章。除此之外,没什么别的了。

我的朋友约翰·格林想把这个数字变成十进制,然后重复搜索。数字1640531527有更多有用的点击。然而,在我们所看到的上下文中,它被用来在散列中相乘散列值,而不是相加它们。此外,在我们找到的所有上下文中,实际数字是-1640531527。进一步挖掘发现,这个数字是无符号数字2654435769的32位有符号版本。

这个数字代表黄金比例(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++数据结构的书。为了完整起见,我还查阅了唐纳德·克努特(Donald Knuth)的《计算机编程艺术》(The Art of Computer Programming)中对黄金比例的引用。唐纳德·克努特属于每一个严肃的计算机程序员,就像默克手册装饰你的健康从业者的书架一样(在线提供)。只是不要指望理解内容...

因此,我们利用黄金比例,确定HASH _ INDENTATION与斐波那契散列法有关。如果我们仔细观察在ThreadLocalMap中散列的方式,我们就会明白为什么这是必要的。标准的java.util.HashMap使用链表来解决冲突。ThreadLocalMap只是寻找下一个可用空间,并在那里插入元素。它通过位屏蔽找到第一个空间,因此只有低几个位是有效的。如果第一个空间已满,它只需将元素放在下一个可用空间中。HASH _ INDENTATION在sparce哈希表中将键隔开,这样就减少了在我们的哈希表旁边找到一个值的可能性。

当线程本地被垃圾收集时,线程本地映射中的WeakReference键被清除。我们接下来需要解决的问题是何时从ThreadLocalMap中删除它。确实如此不当我们打电话的时候就可以离开了get()地图上的另一个条目。java.util.WeakHashMap删除地图上所有过时的条目get()。get()在ThreadLocalMap中会快一点,但可能会留下过时的条目,从而导致内存泄漏。

当线程本地设置为()时,它可以分为三类:

首先,我们可以找到条目并简单地设置它。在这种情况下,过时的条目根本不会被删除。
其次,我们可能会发现我们之前的一个条目已经过时,在这种情况下,我们会在运行中删除所有过时的条目(也就是说,在两个之间空值)。如果我们找到了我们的钥匙,它将与旧的条目交换。
第三,我们的运行可能没有足够的空间来扩展,在这种情况下,条目被放在运行的最后一个空值中,一些过时的条目被清除。这个阶段最初使用0(log2n)算法,但是如果它不能低于填充因子,则在0(n)中执行完全重新散列。
最后,如果删除了一个关键字,那么该条目将与该运行中的任何其他条目一起被删除。


最佳实践
如果必须使用ThreadLocal,请确保在完成后立即移除该值,最好是在将线程返回线程池之前。最佳实践是remove()而不是设置set(null),因为这将导致WeakReference立即与值一起被移除。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值