所以我正在开展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();
}
编辑:为了澄清,我的代码结构如下:主线程从文件中读取输入并将单词添加到队列中,每个工作线程从队列中提取单词并执行一些操作(包括对它们进行排序和添加)他们到二叉树)。
答案 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”方法的看起来无辜的睡眠代码是否比你预期的花费了更多的时间。如果是这样的话,你可以做一次而不是两次或三次 - 那种事情。