遍历具有多个线程的二叉树

时间:2009-12-03 19:55:19

标签: java multithreading optimization binary-tree

所以我正在开展Java速度竞赛。我有(处理器数量)线程正在工作,他们都需要添加到二叉树。最初我只是使用了一个同步的add方法,但是我想这样做,这样线程就可以通过树相互跟随(每个线程只对它正在访问的对象有锁)。不幸的是,即使对于一个非常大的文件(48,000行),我的新二叉树比旧的更慢。我想这是因为我每次搬进树时都会得到并释放一把锁。这是最好的方法吗?还是有更好的方法?

每个节点都有一个名为lock的ReentrantLock,而getLock()和releaseLock()只调用lock.lock()和lock.unlock();

我的代码:

public void add(String sortedWord, String word) {

    synchronized(this){
        if (head == null) {
            head = new TreeNode(sortedWord, word);
            return;
        }
        head.getLock();
    }

    TreeNode current = head, previous = null;
    while (current != null) {

        // If this is an anagram of another word in the list..
        if (current.getSortedWord().equals(sortedWord)) {
            current.add(word);
            current.releaseLock();
            return;
        }
        // New word is less than current word
        else if (current.compareTo(sortedWord) > 0) {
            previous = current;
            current = current.getLeft();
            if(current != null){
                current.getLock();
                previous.releaseLock();
            }
        }
        // New word greater than current word
        else {
            previous = current;
            current = current.getRight();
            if(current != null){
                current.getLock();
                previous.releaseLock();
            }
        }
    }

    if (previous.compareTo(sortedWord) > 0) {
        previous.setLeft(sortedWord, word);
    }
    else {
        previous.setRight(sortedWord, word);
    }
    previous.releaseLock();
}
编辑:为了澄清,我的代码结构如下:主线程从文件中读取输入并将单词添加到队列中,每个工作线程从队列中提取单词并执行一些操作(包括对它们进行排序和添加)他们到二叉树)。

8 个答案:

答案 0 :(得分:4)

另一件事。在性能关键代码中绝对没有二叉树的位置。缓存行为将终止所有性能。它应该有一个更大的扇出(一个缓存行)[编辑]使用二叉树你可以访问太多非连续的内存。看看Judy树上的材料。

你可能想在开始使用树之前先用至少一个字符的基数开始。

首先使用int键而不是字符串进行比较。

也许看看尝试

摆脱所有线程和同步。只是尝试使问题内存访问绑定

[编辑] 我会这样做有点不同。我会为字符串的每个第一个字符使用一个线程,并给它们自己的BTree(或者也许是Trie)。我在每个线程中放置一个非阻塞工作队列,并根据字符串的第一个字符填充它们。通过预先分配添加队列并对BTree进行合并排序,可以获得更高的性能。在BTree中,我使用表示前4个字符的int键,仅引用叶页中的字符串。

在速度竞赛中,您希望受到内存访问限制,因此对线程没有用处。如果没有,你仍然在为每个字符串做太多的处理。

答案 1 :(得分:3)

我实际上会开始考虑compare()equals()的使用,看看是否可以改进某些内容。您可以将String对象包装在另一个具有不同的类中,并针对您的用例compare()方法进行优化。例如,请考虑使用hashCode()代替equals()。哈希码被缓存,因此将来的调用会更快。 考虑实习字符串。我不知道vm是否会接受那么多字符串,但值得一试。

(这将是对答案的评论但是太过于罗嗦了。)

在读取节点时,您需要在到达时为每个节点获取读锁定。如果您读取整个树,那么您什么也得不到。 到达要修改的节点后,释放该节点的读锁定并尝试获取写锁定。代码类似于:

TreeNode当前; //为每个节点添加一个ReentrantReadWriteLock。

//输入当前节点:
  。current.getLock()readLock()锁();
    if(isTheRightPlace(当前){
        current.getLock()readLock()解锁();
        。current.getLock()writeLock()锁(); // NB:getLock返回ConcurrentRWLock
        //做东西然后释放锁定         。current.getLock()writeLock()解锁();
    其他{
        current.getLock()readLock()解锁();
    }

答案 2 :(得分:2)

锁定和解锁是开销,您执行的越多,程序就越慢。

另一方面,分解任务并并行运行部分将使您的程序更快完成。

“收支平衡”点的位置高度依赖于程序中特定锁的争用量,以及运行程序的系统体系结构。如果没有争用(如本程序中似乎存在)和许多处理器,这可能是一个很好的方法。但是,随着线程数量的减少,开销将占主导地位,并发程序将变慢。您必须在目标平台上分析您的程序以确定这一点。

另一个需要考虑的选择是使用不可变结构的非锁定方法。例如,您可以将旧的(链接的)列表附加到新节点,然后在compareAndSet上使用AtomicReference操作,而不是修改列表,确保您赢得数据竞争以设置当前树节点中的words集合。如果没有,请再试一次。您也可以在树节点中使用AtomicReferences作为左右子项。无论是否更快,都必须在您的目标平台上进行测试。

答案 3 :(得分:2)

您可以尝试使用可升级的读/写锁(可能称为可升级的共享锁等,我不知道Java提供了什么):对整个树使用单个RWLock。在遍历B-Tree之前,您获取了读取(共享)锁定,并在完成后释放它(在add方法中获取一个并释放一个,而不是更多)。

在你必须修改 B-Tree的时候,你获得了写(独占)锁(或从读到锁的“升级”),插入节点并降级到读取(共享)锁。

使用这种技术,也可以删除检查和插入头节点的同步!

看起来应该是这样的:

public void add(String sortedWord, String word) {

    lock.read();

    if (head == null) {
        lock.upgrade();
        head = new TreeNode(sortedWord, word);
        lock.downgrade();
        lock.unlock();
        return;
    }

    TreeNode current = head, previous = null;
    while (current != null) {

            if (current.getSortedWord().equals(sortedWord)) {
                    lock.upgrade();
                    current.add(word);
                    lock.downgrade();
                    lock.unlock();
                    return;
            }

            .. more tree traversal, do not touch the lock here ..
            ...

    }

    if (previous.compareTo(sortedWord) > 0) {
        lock.upgrade();
        previous.setLeft(sortedWord, word);
        lock.downgrade();
    }
    else {
        lock.upgrade();
        previous.setRight(sortedWord, word);
        lock.downgrade();
    }

    lock.unlock();
}

不幸的是,经过一些谷歌搜索后,我找不到适合Java的“ugradeable”rwlock。 “类ReentrantReadWriteLock”不可升级,但是,您可以解锁读取,然后锁定写入,并且(非常重要):重新检查导致这些行的条件(例如{{ 1}})。这很重要,因为另一个线程可能在读取解锁和写入锁定之间发生了变化。

for details check this question and its answers

最后,B树的遍历将并行运行。只有在找到目标节点时,该线程才会获得一个独占锁(其他线程只会在插入时阻塞)。

答案 4 :(得分:1)

考虑到每行一个数据集,48k行并不是那么多,您只能对操作系统和虚拟机如何破坏文件IO以尽可能快地进行猜测。

尝试使用生产者/消费者范例在这里可能会有问题,因为您必须仔细平衡锁的开销与实际的IO数量。如果您只是尝试改进文件IO的方式(考虑mmap()之类的内容),您可能会获得更好的性能。

答案 5 :(得分:1)

我会说,这样做是的方式,甚至不考虑同步性能问题。

此实现比原始完全同步版本慢的事实可能是一个问题,但更大的问题是此实现中的锁定根本不健全。

想象一下,例如,您将null传递给sortedWord;这将导致抛出NullPointerException,这意味着您最终会保留当前线程中的锁,从而使您的数据结构处于不一致状态。另一方面,如果你只是synchronize这种方法,你不必担心这些事情。考虑到同步版本也更快,它是一个简单的选择。

答案 6 :(得分:1)

您似乎已经实现了二叉搜索树,而不是B树。

无论如何,你考虑过使用ConcurrentSkipListMap吗?这是一个有序的数据结构(在Java 6中引入),它应该具有良好的并发性。

答案 7 :(得分:0)

我有一个愚蠢的问题:因为你正在阅读和修改文件,所以你将完全受限于读/写头可以移动的速度以及磁盘可以旋转的速度。那么使用线程和处理器有什么用呢?光盘不能同时做两件事。

或者这一切都在RAM中?

补充:好的,我不清楚这里有多少并行可以帮助你(有些可能),但无论如何,我建议的是从每个线程中挤出每个循环你可以。 This is what I'm talking about.例如,我想知道那些看起来像“get”和“compare”方法的看起来无辜的睡眠代码是否比你预期的花费了更多的时间。如果是这样的话,你可以做一次而不是两次或三次 - 那种事情。