线程安全的排序链表

时间:2011-05-14 15:14:58

标签: java multithreading linked-list

我正在尝试编写一个线程安全的已排序单个链表。我写了两个版本:粗粒度同步和细粒度同步。以下是两种实现:

细粒度:

public void add(T t) {                                                         
  Node curr = head;
  curr.lock.lock();

  while (curr.next != null) {
    // Invariant: curr is locked                                               
    // Invariant: curr.data < t                                                
    curr.next.lock.lock();                                                     

    if (t.compareTo(curr.next.data) <= 0) {                                    
      break;                                                                   
    }                                                                          

    Node tmp = curr.next;                                                      
    curr.lock.unlock();                                                        
    curr = tmp;                                                                
  }                                                                            

  // curr is acquired                                                          
  curr.next = new Node(curr.next, t);                                          
  if (curr.next.next != null) {  // old curr's next is acquired                
    curr.next.next.lock.unlock();                                              
  }                                                                            
  curr.lock.unlock();                                                          
}                                                                              

粗粒度:

public void add(T t) {
  lock.lock();
  Node curr = head;
  while (curr.next != null) {
    if (t.compareTo(curr.next.data) <= 0) {
      break;
    }                                                                          
    curr = curr.next;                                                          
  }                                                                            
  curr.next = new Node(curr.next, t);                                          
  lock.unlock();                                                               
}

我将4个线程(在4个逻辑CPU核心上)的两个版本定时插入20000个整数。每个线程的时间显示CPU时间(即它不包括等待时间)。

Fine grained:
Worked 1 spent 1080 ms
Worked 2 spent 1230 ms
Worked 0 spent 1250 ms
Worked 3 spent 1260 ms
wall time: 1620 ms

Coarse grained:
Worked 1 spent 190 ms
Worked 2 spent 270 ms
Worked 3 spent 410 ms
Worked 0 spent 280 ms
wall time: 1298 ms

我最初的想法是.lock().unlock()是问题所在,但我分析了实施情况,他们一起只消耗了30%的时间。我的第二个猜测是细粒度的解决方案有更多的缓存未命中,但我对此表示怀疑,因为与数组不同,单个链表本身就容易出现缓存未命中。

知道为什么我没有得到预期的并行化吗?

5 个答案:

答案 0 :(得分:1)

是的,这可能是由于缓存未命中。包含锁的缓存行在CPU之间不断反弹。

另外,请注意你已经获得了很多相似之处:

Fine grained:
Worked 1 spent 1080 ms
Worked 2 spent 1230 ms
Worked 0 spent 1250 ms
Worked 3 spent 1260 ms
wall time: 1620 ms

Coarse grained:
Worked 1 spent 190 ms
Worked 2 spent 270 ms
Worked 3 spent 410 ms
Worked 0 spent 280 ms
wall time: 1298 ms

尽管由于缓存未命中(以及增加的开销),每个单独的线程都会花费更多的时间,但整个过程只会稍慢。

答案 1 :(得分:1)

您可以在每线程粗粒度版本附近实现挂壁时间 通过首先走没有锁的列表,以找到差距,然后从 当前,这次使用锁,走列表,以确保没有干预 在当前和当前之间的其他线程插入 - >接下来。 (当然,我正在贬低“头”总是至高无上的事实:)

答案 2 :(得分:0)

确实存在性能问题。我认为你应该将性能与内置实现和单线程版本进行比较。

for (int r = 0; r < 5; r++) {
    long start = System.nanoTime();
    ConcurrentLinkedQueue<Integer> list = new ConcurrentLinkedQueue<Integer>();
    for (int i = 0; i < 500000; i++)
        list.add(i);
    long time = System.nanoTime() - start;
    System.out.printf("Adding 500K %,d took ms%n", time / 1000 / 1000);
}

打印

Adding 500K 192 took ms
Adding 500K 154 took ms
Adding 500K 95 took ms
Adding 500K 211 took ms
Adding 500K 424 took ms

答案 3 :(得分:0)

除了ninjalj的回答 - 精美的锁也

  1. 禁用现有代码中的某些编译器优化
  2. 禁用某些CPU优化 - 例如预取
  3. 强制内存在锁定时获取语义,并在解锁时释放语义 - 导致跨CPU同步和无效缓存 - 这不会直接显示为分析器中的lock()成本,但会增加后续数据访问的成本。

答案 4 :(得分:-1)

我错过了什么吗?我的代码中没有看到任何类型的缓存。此外,您应该重新考虑使用锁定的方式。您应该只锁定整个列表以限制锁定数量,并且还可以防止竞争条件,如下所示。

thread1: Read Element X
thread2: Removes X + 1
thread1: Read Element X + 1 and fails since the element is no long valid.
thread1: Is unable to finish going through the list since it has been removed.

您可以对列表进行分区,但是您必须处理正在读取分区中最后一个元素并删除下一个分区中第一个元素的情况。

您还可以根据正在进行的操作类型(即,它是读操作并且当前没有正在执行写操作)来使某些功能需要锁定/解锁。