Warm tip: This article is reproduced from serverfault.com, please click

java-向executorservice添加更多线程只会使其变慢

(java - Adding more threads to executorservice only makes it slower)

发布于 2020-12-03 13:43:07

我有这段代码,我有自己的自制数组类,我想用它来测试Java中一些不同的并发工具的速度

public class LongArrayListUnsafe {
   private static final ExecutorService executor
      = Executors.newFixedThreadPool(1);
   public static void main(String[] args) {
   LongArrayList dal1 = new LongArrayList();
    int n = 100_000_000;
    Timer t = new Timer();

List<Callable<Void>> tasks = new ArrayList<>();

tasks.add(() -> {
  for (int i = 0; i <= n; i+=2){
    dal1.add(i);
  }
  return null;
});

tasks.add(() -> {
  for (int i = 0; i < n; i++){
    dal1.set(i, i + 1);
  }
  return null;});
tasks.add(() -> {
  for (int i = 0; i < n; i++) {

    dal1.get(i);
  }
  return null;});
tasks.add(() -> {
  for (int i = n; i < n * 2; i++) {

    dal1.add(i + 1);
  }
  return null;});
try {
  executor.invokeAll(tasks);
} catch (InterruptedException exn) {
  System.out.println("Interrupted: " + exn);
}
executor.shutdown();
try {
  executor.awaitTermination(1000, TimeUnit.MILLISECONDS);
} catch (Exception e){
  System.out.println("what?");
}

System.out.println("Using toString(): " + t.check() + " ms");

}
}

class LongArrayList {
 // Invariant: 0 <= size <= items.length
    private long[] items;
    private int size;

    public LongArrayList() {
       reset();
    }

    public static LongArrayList withElements(long... initialValues){
    LongArrayList list = new LongArrayList();
    for (long l : initialValues) list.add( l );
         return list;
      }


    public void reset(){
       items = new long[2];
       size = 0;
     }

     // Number of items in the double list
      public int size() {
      return size;
      }

      // Return item number i
       public long get(int i) {
          if (0 <= i && i < size)
             return items[i];
          else
             throw new IndexOutOfBoundsException(String.valueOf(i));
        }

    // Replace item number i, if any, with x
     public long set(int i, long x) {
       if (0 <= i && i < size) {
           long old = items[i];
           items[i] = x;
          return old;
       } else
        throw new IndexOutOfBoundsException(String.valueOf(i));
       }

       // Add item x to end of list
       public LongArrayList add(long x) {
          if (size == items.length) {
           long[] newItems = new long[items.length * 2];
          for (int i=0; i<items.length; i++)
              newItems[i] = items[i];
          items = newItems;
      }
      items[size] = x;
      size++;
      return this;
       }


       public String toString() {
         return Arrays.stream(items, 0,size)
        .mapToObj( Long::toString )
        .collect(Collectors.joining(", ", "[", "]"));
        }
           }

       public class Timer {
         private long start, spent = 0;
         public Timer() { play(); }
         public double check() { return (System.nanoTime()-start+spent)/1e9; }
         public void pause() { spent += System.nanoTime()-start; }
         public void play() { start = System.nanoTime(); }
         }

LongArrayList类的实现不是那么重要,它不是线程安全的。

带有executorservice的驱动程序代码在arraylist上执行一堆操作,并执行4个不同的任务,每次执行100_000_000次。

问题是,当我为线程池分配更多线程时,“ Executors.newFixedThreadPool(2);” 它只会变慢。例如,对于一个线程,典型的计时时间是1.0366974毫秒,但是如果我使用3个线程运行它,则时间将增加到5.7932714毫秒。

到底是怎么回事?为什么更多的线程这么慢?

编辑:

为了解决这个问题,我简化了驱动程序代码,其中包含四个仅添加元素的任务:

ExecutorService executor
      = Executors.newFixedThreadPool(2);
LongArrayList dal1 = new LongArrayList();
int n = 100_000_00;
Timer t = new Timer();

for (int i = 0; i < 4 ; i++){
  executor.execute(new Runnable() {
    @Override
    public void run() {
      for (int j = 0; j < n ; j++)
        dal1.add(j);
    }
  });
}


executor.shutdown();
try {
  executor.awaitTermination(1000, TimeUnit.MILLISECONDS);
} catch (Exception e){
  System.out.println("what?");
}

System.out.println("Using toString(): " + t.check() + " ms");

在这里,我分配多少线程似乎并不重要,根本没有加速,这可能仅仅是因为开销吗?

Questioner
n00bster
Viewed
0
dreamcrash 2020-12-05 19:56:16

你的代码中存在一些问题,很难解释为什么使用更多线程会增加时间。

顺便提一句

public double check() { return (System.nanoTime()-start+spent)/1e9; }

给你倒数秒而不是毫秒,因此请更改此设置:

System.out.println("Using toString(): " + t.check() + " ms");

System.out.println("Using toString(): " + t.check() + "s");

第一个问题:

LongArrayList dal1 = new LongArrayList();

dal1所有线程之间共享,并且那些线程正在更新该共享变量没有任何mutual exclusion周围,因此,导致竞争条件此外,这还可能导致cache invalidation,从而增加你的总体执行时间。

另一件事是你可能遇到负载平衡问题。你有4个并行任务,但显然是最后一个

tasks.add(() -> {
  for (int i = n; i < n * 2; i++) {

    dal1.add(i + 1);
  }
  return null;});

是最耗费计算资源的任务。即使这4个任务并行运行,也没有我提到的问题(即,共享数据之间缺乏同步),最后一个任务将决定整个执行时间。

更不用说并行不是免费提供的,它增加了开销(例如, 调度并行工作等),这可能足够高,因此一开始就不值得对代码进行并行化。在你的代码中,至少有等待任务完成的开销,还有关闭执行程序池的开销。

另一种可能也解释你为什么不花钱的可能性ArrayIndexOutOfBoundsException是前三个任务是如此之小,以至于它们是由同一线程执行的。这也再次让你的总执行时间非常依赖于最后一个任务,在上的开销 executor.shutdown();executor.awaitTermination但是,即使是这种情况,任务的执行顺序以及随后将执行的线程通常也是不确定的,因此也不是你的应用程序所依赖的。有趣的是,当我更改你的代码以立即执行任务( executor.execute)时ArrayIndexOutOfBoundsException,到处都是。