跨不同线程访问ThreadLocal的值

13

考虑到ThreadLocal变量对于不同的线程持有不同的值,是否可能从另一个线程中访问一个ThreadLocal变量的值呢?

也就是说,在下面的示例代码中,是否可以在中从读取TLocWrapper.tlint的值?

public class Example
{
  public static void main (String[] args)
  {
    Tex t1 = new Tex("t1"), t2 = new Tex("t2");
    new Thread(t1).start();
    try
    {
      Thread.sleep(100);
    }
    catch (InterruptedException e)
    {}
    new Thread(t2).start();
    try
    {
      Thread.sleep(1000);
    }
    catch (InterruptedException e)
    {}
    t1.kill = true;
    t2.kill = true;
  }

  private static class Tex implements Runnable
  {
    final String name;
    Tex (String name)
    {
      this.name = name;
    }
    public boolean kill = false;
    public void run ()
    {
      TLocWrapper.get().tlint.set(System.currentTimeMillis());
      while (!kill)
      {
        // read value of tlint from TLocWrapper
        System.out.println(name + ": " + TLocWrapper.get().tlint.get());
      }
    }
  }
}
class TLocWrapper
{
  public ThreadLocal<Long> tlint = new ThreadLocal<Long>();
  static final TLocWrapper self = new TLocWrapper();
  static TLocWrapper get ()
  {
    return self;
  }
  private TLocWrapper () {}
}

1
使用适当的读/写锁定的常规变量用于在线程之间共享数据。ThreadLocal专门用于在不希望在线程之间共享数据的情况下使用。这使我相信这要么是一个纯粹的假设性问题,要么你正在尝试使用ThreadLocal来做一些它特别不适合使用的事情。 - cthulhu
1
@Cthulhu:是的,我正在尝试使用ThreadLocal做一些“邪恶”的事情——但我的意图是好的 : )。我只是想解决一个问题。 - JHollanti
顺便提一下:你必须将kill field设置为volatile,这是一个非常微妙的错误。 - sleeplessnerd
5个回答

16

正如Peter所说,这是不可能的。如果您需要这种功能,则在概念上您真正需要的只是标准的Map<Thread, Long> - 其中大多数操作将使用Thread.currentThread()键完成,但您也可以传递其他线程。

然而,这可能并不是一个好主意。首先,持有对垂死线程的引用会混乱GC,因此您必须额外地进行一圈处理,使键类型变为WeakReference<Thread>。我不确定Thread是否是一个很好的Map键。

因此,一旦您超越了内置的ThreadLocal的方便性,也许值得质疑使用Thread对象作为键是否是最佳选项?最好为每个线程提供唯一的ID(如果它们没有更合理的自然键,则为字符串或整数),并简单地使用这些来将地图键掉。我意识到您的示例是人为的,但您可以使用Map<String, Long>做同样的事情,并使用键"t1""t2"

这也可能更清晰,因为Map表示您实际使用数据结构的方式; ThreadLocal更像是带有一点访问控制魔法的标量变量,而不是集合,因此即使可以按您想要的方式使用它们,对于查看您代码的其他人来说,这可能会更加混乱。


为什么线程不适合作为Map键?使用Map<String,V>是否更好?当线程终止时,您将会在Map中泄漏数据。即使使用Map<WeakReference<Thread>,V>,您也不会泄漏线程,但是您将会泄漏线程局部数据。我建议创建一个持有者对象来持有V,然后将该持有者两次存储:一次存储在正常的ThreadLocal中,另一次存储在由线程索引的同步或并发Map中以供其他线程访问。然后创建一个清理守护线程,定期从Map中删除已死亡的线程。 - Simon Kissane

12

根据Andrzej Doyle的答案,这里是一个完整工作解决方案:

ThreadLocal<String> threadLocal = new ThreadLocal<String>();
threadLocal.set("Test"); // do this in otherThread

Thread otherThread = Thread.currentThread(); // get a reference to the otherThread somehow (this is just for demo)

Field field = Thread.class.getDeclaredField("threadLocals");
field.setAccessible(true);
Object map = field.get(otherThread);

Method method = Class.forName("java.lang.ThreadLocal$ThreadLocalMap").getDeclaredMethod("getEntry", ThreadLocal.class);
method.setAccessible(true);
WeakReference entry = (WeakReference) method.invoke(map, threadLocal);

Field valueField = Class.forName("java.lang.ThreadLocal$ThreadLocalMap$Entry").getDeclaredField("value");
valueField.setAccessible(true);
Object value = valueField.get(entry);

System.out.println("value: " + value); // prints: "value: Test"

当然,所有之前的评论仍然适用 - 这是不安全的

但出于调试的目的,它可能正是你所需要的 - 我就是这样使用的。


7

我想查看ThreadLocal存储中的内容,因此我扩展了上面的示例以显示给我。这也对调试很有用。

            Field field = Thread.class.getDeclaredField("threadLocals");
            field.setAccessible(true);
            Object map = field.get(Thread.currentThread());
            Field table = Class.forName("java.lang.ThreadLocal$ThreadLocalMap").getDeclaredField("table");
            table.setAccessible(true);
            Object tbl = table.get(map);
            int length = Array.getLength(tbl);
            for(int i = 0; i < length; i++) {                   
                Object entry = Array.get(tbl, i);
                Object value = null;
                String valueClass = null;
                if(entry != null) { 
                    Field valueField = Class.forName("java.lang.ThreadLocal$ThreadLocalMap$Entry").getDeclaredField("value");
                    valueField.setAccessible(true);
                    value = valueField.get(entry);
                    if(value != null) {
                        valueClass = value.getClass().getName();
                    }
                    Logger.getRootLogger().info("[" + i + "] type[" + valueClass + "] " + value);
                }
            }

很好,特别适用于调试。 - Ean V

6
ThreadLocalMap 可以通过反射和 Thread.class.getDeclaredField("threadLocals") setAccssible(true) 等方式进行访问。
但是不要这样做。该映射表应仅由拥有线程访问,并且访问任何 ThreadLocal 的值都可能导致数据竞争。
然而,如果您可以接受上述的数据竞争或者避免它们(更好的方法),那么这里有一个最简单的解决方案:扩展 Thread 并在其中定义所需内容即可。
ThreadX extends Thread{
  int extraField1;
  String blah2; //and so on
}

这是一个不依赖于WeakReferences的不错解决方案,但需要您创建线程。您可以像这样设置:((ThreadX)Thread.currentThread()).extraField1=22

确保在访问字段时不会出现数据竞争。因此,您可能需要使用volatile、synchronized等。

总体而言,Map是一个糟糕的想法,永远不要保留对您没有明确管理/拥有的对象的引用,特别是涉及Thread、ThreadGroup、Class、ClassLoader等对象时。 WeakHashMap<Thread, Object>稍微好一些,但您需要在锁定下独占访问它(即在高度多线程环境中可能会影响性能)。 WeakHashMap不是世界上最快的东西。

ConcurrentMap, Object>可能更好,但您需要具有equalshashCode的WeakRef...


5

只有在一个不是ThreadLocal的字段中放置相同的值并访问该字段才可能实现。按照定义,ThreadLocal仅局限于该线程。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接