【问题标题】:Why using Java threads is not much faster?为什么使用 Java 线程并没有快多少?
【发布时间】:2011-07-21 15:54:24
【问题描述】:

我有以下程序从字符串向量中删除偶数,当向量大小变大时,可能需要很长时间,所以我想到了线程,但是使用10个线程并不比一个线程快,我的PC 6核12线程,为什么?

import java.util.*;

public class Test_Threads
{
  static boolean Use_Threads_To_Remove_Duplicates(Vector<String> Good_Email_Address_Vector,Vector<String> To_Be_Removed_Email_Address_Vector)
  {
    boolean Removed_Duplicates=false;
    int Threads_Count=10,Delay=5,Average_Size_For_Each_Thread=Good_Email_Address_Vector.size()/Threads_Count;

    Remove_Duplicate_From_Vector_Thread RDFVT[]=new Remove_Duplicate_From_Vector_Thread[Threads_Count];
    Remove_Duplicate_From_Vector_Thread.To_Be_Removed_Email_Address_Vector=To_Be_Removed_Email_Address_Vector;
    for (int i=0;i<Threads_Count;i++)
    {
      Vector<String> Target_Vector=new Vector<String>();
      if (i<Threads_Count-1) for (int j=i*Average_Size_For_Each_Thread;j<(i+1)*Average_Size_For_Each_Thread;j++) Target_Vector.add(Good_Email_Address_Vector.elementAt(j));
      else for (int j=i*Average_Size_For_Each_Thread;j<Good_Email_Address_Vector.size();j++) Target_Vector.add(Good_Email_Address_Vector.elementAt(j));
      RDFVT[i]=new Remove_Duplicate_From_Vector_Thread(Target_Vector,Delay);
    }

    try { for (int i=0;i<Threads_Count;i++) RDFVT[i].Remover_Thread.join(); }
    catch (Exception e) { e.printStackTrace(); }                                                   // Wait for all threads to finish

    for (int i=0;i<Threads_Count;i++) if (RDFVT[i].Changed) Removed_Duplicates=true;

    if (Removed_Duplicates)                                                                        // Collect results
    {
      Good_Email_Address_Vector.clear();
      for (int i=0;i<Threads_Count;i++) Good_Email_Address_Vector.addAll(RDFVT[i].Target_Vector);
    }

    return Removed_Duplicates;
  }

  public static void out(String message) { System.out.print(message); }
  public static void Out(String message) { System.out.println(message); }

  public static void main(String[] args)
  {
    long start=System.currentTimeMillis();

    Vector<String> Good_Email_Address_Vector=new Vector<String>(),To_Be_Removed_Email_Address_Vector=new Vector<String>();
    for (int i=0;i<1000;i++) Good_Email_Address_Vector.add(i+"");
    Out(Good_Email_Address_Vector.toString());
    for (int i=0;i<1500000;i++) To_Be_Removed_Email_Address_Vector.add(i*2+"");
    Out("=============================");

    Use_Threads_To_Remove_Duplicates(Good_Email_Address_Vector,To_Be_Removed_Email_Address_Vector);  // [ Approach 1 : Use 10 threads ] 
//    Good_Email_Address_Vector.removeAll(To_Be_Removed_Email_Address_Vector);                       // [ Approach 2 : just one thread ]
    Out(Good_Email_Address_Vector.toString());

    long end=System.currentTimeMillis();
    Out("Time taken for execution is " + (end - start));
  }
}

class Remove_Duplicate_From_Vector_Thread
{
  static Vector<String> To_Be_Removed_Email_Address_Vector;
  Vector<String> Target_Vector;
  Thread Remover_Thread;
  boolean Changed=false;

  public Remove_Duplicate_From_Vector_Thread(final Vector<String> Target_Vector,final int Delay)
  {
    this.Target_Vector=Target_Vector;

    Remover_Thread=new Thread(new Runnable()
    {
      public void run()
      {
        try
        {
          Thread.sleep(Delay);
          Changed=Target_Vector.removeAll(To_Be_Removed_Email_Address_Vector);
        }
        catch (InterruptedException e) { e.printStackTrace(); }
        finally { }
      }
    });
    Remover_Thread.start();
  }
}

在我的程序中,您可以尝试“[方法 1:使用 10 个线程]”或“[方法 2:仅一个线程]”,速度方面没有太大差异,我预计它会快几倍,为什么?

【问题讨论】:

  • 线程有成本,锁竞争有巨大的成本。
  • 初始向量有多大?
  • 你应该遵循你所写语言的约定;不要自己编。这段代码很难阅读,因为它使用了非常规的大写和下划线。
  • 我同意@erickson。单线 if / for 连击让我的眼睛受伤了。
  • 为什么每个线程都调用sleep()

标签: java multithreading


【解决方案1】:

简单的答案是您的线程都试图访问调用同步方法的单个向量。这些方法上的synchronized 修饰符确保在任何给定时间只有一个线程可以执行该对象上的任何方法。因此,计算的并行部分的重要部分涉及等待其他线程。

另一个问题是,对于O(N) 输入列表,您有一个O(N) 设置... Target_Vector 对象的填充...这是在一个线程中完成的。加上线程创建的开销。

所有这些加起来并没有多少加速。


如果您使用单个 ConcurrentHashMap 而不是拆分为多个 Target_Vector 对象的单个 Good_Email_Address_Vector 对象,您应该会获得显着的加速(使用多个线程):

  • 删除操作是O(1) 而不是O(n)
  • 减少复制,
  • 由于更好​​地处理争用,数据结构提供了更好的多线程性能,并且
  • 您无需绕圈子即可避开ConcurrentModificationException

此外,To_Be_Removed_Email_Address_Vector 对象应替换为未同步的ListList.sublist(...) 应用于创建可传递给线程的视图。


简而言之,您最好丢弃当前代码并重新开始。 使用符合 Java 编码约定的合理标识符名称,并且 将您的代码包装在 ~80 行,以便人们可以阅读它!

【讨论】:

  • Set&lt;String&gt; set = Collections.setFromMap(new ConcurrentHashMap&lt;String, Boolean&gt;()
  • 他们没有更新同一个向量;每个线程都有自己的修改向量。
  • @erikson - 但线程都使用单个向量来查找要删除的地址。
  • 是的,我的评论适用于您的原始答案,即单个集合由多个线程更新。
  • @erikson - 所以你的评论现在不再相关。对吗?
【解决方案2】:

向量同步产生争用

您已经拆分了要修改的向量,从而避免了 some 争用。但是多个线程正在访问static Vector To_Be_Removed_Email_Address_Vector,所以仍然存在很多争用(所有Vector 方法都是同步的)。

对共享的只读信息使用不同步的数据结构,这样线程之间就不会发生争用。在我的机器上,使用 ArrayList 代替 Vector 运行测试可将执行时间缩短一半。

即使没有争用,线程安全的结构也比较慢,所以当只有一个线程可以访问一个对象时不要使用它们。此外,Vector 在很大程度上已被 Java 5 淘汰。避免使用它,除非您必须与无法更改的遗留 API 进行互操作。

选择合适的数据结构

列表数据结构将为此任务提供较差的性能。由于电子邮件地址可能是唯一的,因此一组应该是合适的替代品,并且在大型组上removeAll() 会更快。使用HashSet 代替原来的Vector 将我(8 核)机器上的执行时间从超过5 秒减少到大约3 毫秒。大约一半的改进是由于使用了正确的数据工作的结构。

并发结构不合适

使用并发并发数据结构比较慢,而且没有简化代码,所以不推荐。

使用更新的并发数据结构比争夺Vector要快得多,但是这些数据结构的并发开销仍然比单线程结构高很多。例如,在我的机器上运行原始代码需要超过 5 秒,而 ConcurrentSkipListSet 需要半秒,ConcurrentHashMap 需要八分之一秒。但请记住,当每个线程都有自己的HashSet 进行更新时,总时间仅为 3 毫秒。

即使所有线程都在更新单个并发数据结构,划分工作负载所需的代码也与原始代码中用于为每个线程创建单独的Vector 的代码非常相似。从可读性和维护的角度来看,所有这些解决方案都具有同等的复杂性。

如果您遇到“坏”电子邮件地址被异步添加到集合中的情况,并且您希望“好”列表的读者自动神奇地看到这些更新,那么并发集合将是一个不错的选择。但是,在 API 的当前设计中,“好”列表的消费者显式调用阻塞过滤器方法来更新列表,并发数据结构可能是错误的选择。

【讨论】:

    【解决方案3】:

    您的所有线程都在同一个向量上工作。您对向量的访问是序列化的(即一次只有一个线程可以访问它),因此使用多个线程最多可能是相同的速度,但更可能慢得多。

    当您有独立的任务要执行时,多个线程的工作速度会更快。

    在这种情况下,最快的选择可能是创建一个新列表,其中包含您想要保留的所有元素并替换原始列表,在一个线程中。这将比使用具有多个线程的并发集合更快。


    作为比较,这是你可以用一个线程做的事情。由于集合相当小,JVM 不会在一次运行中预热,因此有多个虚拟运行未打印。

    public static void main(String... args) throws IOException, InterruptedException, ParseException {
        for (int n = -50; n < 5; n++) {
            List<String> allIds = new ArrayList<String>();
            for (int i = 0; i < 1000; i++) allIds.add(String.valueOf(i));
    
            long start = System.nanoTime();
            List<String> oddIds = new ArrayList<String>();
            for (String id : allIds) {
                if ((id.charAt(id.length()-1) % 2) != 0)
                    oddIds.add(id);
            }
            long time = System.nanoTime() - start;
            if (n >= 0)
                System.out.println("Time taken to filter " + allIds.size() + " entries was " + time / 1000 + " micro-seconds");
        }
    }
    

    打印

    Time taken to filter 1000 entries was 136 micro-seconds
    Time taken to filter 1000 entries was 141 micro-seconds
    Time taken to filter 1000 entries was 136 micro-seconds
    Time taken to filter 1000 entries was 137 micro-seconds
    Time taken to filter 1000 entries was 138 micro-seconds
    

    【讨论】:

      猜你喜欢
      • 2013-11-22
      • 2021-10-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-12-20
      • 1970-01-01
      • 1970-01-01
      • 2019-04-08
      相关资源
      最近更新 更多