【问题标题】:java - stealing bits from referencesjava - 从引用中窃取位
【发布时间】:2013-12-12 07:00:47
【问题描述】:

如何从一个地址中窃取 2 个 MSB 来执行原子操作?我正在尝试做一个单词的CAS

一个例子

public class Node
{
    long key;
    long value;
    Node lchild; // format is flag1,flag2,address
    Node rchild; // format is flag1,flag2,address
}

public void createNode()
{
    Node n1 = new Node(); //this should create a node with format 0,0,address1
}

public void setFlag1(Node n1)
{
    Now the new address should be in format 1,0,address1
}

public void setFlag2(Node n1)
{
    Now the new address should be in format 0,1,address1
}

如果我只需要一个额外的标志,可以使用AtomicReferenceAtomicStampedReference 可以使用,但效率不高,因为它会创建一个包含时间戳和引用的额外框。

在 C 中讨论了一个类似的问题 stealing bits from a pointer

【问题讨论】:

  • 如果指针是字对齐的,你想窃取两个 LSB 而不是两个 MSB。大多数指针都是字对齐的。不过,我不知道如何在 Java 中获得指针的位表示。
  • 为什么java.util.concurrent.AtomicReference 不适合你?
  • 当我调用Node n1 = new Node() 时,操作系统是否总是会分配一个字对齐的空格?我试图窃取 MSB 以避免 compareAndSwap 可能发生的 ABA 问题。您能否解释一下为什么从 LSB 中窃取效率更高?
  • @JimGarrison:我希望我可以使用 AtomicReference。但我需要偷两个位。我认为 AtomicReference 只使用了一个额外的位。如果我错了,请纠正我
  • 除非您编写一些 native 代码,否则无法访问引用的位。

标签: java c java.util.concurrent concurrent-programming compare-and-swap


【解决方案1】:

您或许可以使用 sun.misc.Unsafe 来实现这一点

除此之外,它还有许多compareAndSwap 方法可以处理任何对象中的任意二进制数据。

话虽如此,如果您要实现二叉搜索树,我建议您查看不可变的persistent data structures。这些优势包括:

  • 由于不可变性,它们在定义上是线程安全的
  • 它们不需要锁
  • 一旦您开始做更复杂的事情(例如子树的快照),进行结构共享的能力将大大提高性能 - 基本上您无需进行防御性副本。

【讨论】:

  • 这个unsafe包和java中标准的concurrent包有什么区别?为什么你认为使用 unsafe 包中的 CAS 可以解决我的问题?
  • 基本上,Unsafe 允许您在不遵循 Java 通常的类型安全规则的情况下操作对象中的数据。这是有风险的,你很容易导致一些非常讨厌的错误——但另一方面你可以获得一些很好的性能。通常我会建议不要这样做。
【解决方案2】:

如果不实现您自己的支持此类操作并正确处理标志位的 JVM,这是不可能的,例如进行 GC 时(此时需要识别所有引用,移动收集器需要更改它们)。即使这样,这也违反了 Java 的设计原则,Java 的设计原则不包括显式取消引用或指针算法(我会计算引用中的更改位并将它们屏蔽以进行取消引用)。

相反,我建议您创建一个新的混合 Edge 类型的标志和节点:

public class Node {
    long key;
    long value;
    Child lchild; // child = node reference + flags
    Child rchild;
}

// this is the edge type which should also be used to reference the root node with flags
public class Child {
    Node child;
    BitSet flags;

    public Child(Node node) {
        this.child = node;
        this.flags = new BitSet(2); // we initialize the flags with 00
    }
    public void setFlag(int index) {
        this.flags.set(index); // index would be 0 or 1 here for the two flags
    }
    public boolean isFlagSet(int index) {
        return this.flags.get(index); // index would be 0 or 1 here for the two flags
    }
}

【讨论】:

  • 复合边类型是什么意思。你能详细说明一下吗?
  • @arunmoezhi 我添加了一个代码示例来说明这个建议。
  • SchmeiBer 感谢您的示例。所以当我用这种结构在二叉搜索树中创建一个新节点时,我应该使用new Node()还是new Child()
  • @arunmoezhi new Child(theNode) 是正确的,theNode 是应该集成在树中的节点 - 类的命名可以在这里改进,但我想接近这个问题有了它
【解决方案3】:

复制 Maurice Herlihy 和 Nir ​​Shavit 所著的“多处理器编程的艺术”第 215 页一书中的摘录

如 Pragma 9.8.1 中详细描述的,AtomicMarkableReference object 封装了对 T 类型对象的引用和 布尔标记。这些字段可以一起自动更新 或单独。我们使每个节点的下一个字段 原子标记参考。线程 A 逻辑上删除 currA 在节点的下一个字段中设置标记位,并共享物理 与执行 add() 或 remove() 的其他线程一起删除:作为每个 线程遍历列表,它通过物理清理列表 删除(使用 compareAndSet())它遇到的任何标记节点。在 换句话说,执行 add() 和 remove() 的线程不会遍历 标记的节点,他们在继续之前将其删除。包含() 方法和 LazyList 算法一样,遍历所有 节点是否被标记,并测试项目是否在 根据其键和标记列出。

【讨论】:

    【解决方案4】:

    对于在 Java 中工作,我能给你的最好建议是在数组索引而不是地址上做你的比特,因为 Java 不公开地址。

    【讨论】:

      【解决方案5】:

      要从可用于对象引用变量的那些中提取位,需要创建您自己的 JVM。

      您首先必须确保这些位未被实际使用(例如,当 JVM 总是在 16 字节边界上对齐对象时,通过获取引用中的低位位)。但有些 JVM 使用 32 位引用的所有位。

      接下来,您必须在每次加载引用时注入代码,以在访问关联对象之前清除这些位。

      那么你必须对垃圾收集器做同样的事情。

      【讨论】:

        【解决方案6】:

        您不能从引用中窃取位,但有一种解决方法: 您可以使用 AtomicStampedReference,它允许您进行比较和交换以原子更新引用和整数。您可以将整数用作状态,也可以根据需要从整数的 MSB 中窃取位。你可以在java中对整数进行位运算,这很酷:

        https://docs.oracle.com/javase/tutorial/java/nutsandbolts/op3.html

        我最终编写了一个 java 程序,我从 AtomicStampedReference 的整数中窃取了 2 位并将它们用作状态位,并将整数的剩余 30 位用作计数器以防止 ABA 问题。

        【讨论】:

          猜你喜欢
          • 2013-11-29
          • 2010-11-04
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-05-02
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多