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

Adding more threads to executorservice only makes it slower

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

I have this code, where I have my own homemade array class, that I want to use to test the speed of some different concurrency tools in 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(); }
         }

The implementation of a LongArrayList class is not so important,it's not threadsafe.

The drivercode with the executorservice performs a bunch of operations on the arraylist, and has 4 different tasks doing it, each 100_000_000 times.

The problem is that when I give the threadpool more threads "Executors.newFixedThreadPool(2);" it only becomes slower. For example, for one thread, a typical timing is 1.0366974 ms, but if I run it with 3 threads, the time ramps up to 5.7932714 ms.

What is going on? why is more threads so much slower?

EDIT:

To boil the issue down, I made this much simpler drivercode, that has four tasks that simply add elements:

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");

Here it still does not seem to matter how many threads i allocate, there is no speedup at all, could this simply be because of overhead?

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

There are some problems with your code that make it hard to reason why with more threads the time increases.

btw

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

gives you back seconds not milliseconds, so change this:

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

to

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

First problem:

LongArrayList dal1 = new LongArrayList();

dal1 is shared among all threads, and those threads are updating that shared variable without any mutual exclusion around it, consequently, leading to race conditions. Moreover, this can also lead to cache invalidation, which can increase your overall execution time.

The other thing is that you may have load balancing problems. You have 4 parallel tasks, but clearly the last one

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

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

is the most computing-intensive task. Even if the 4 tasks run in parallel, without the problems that I have mention (i.e., lack of synchronization around the shared data), the last task will dictate the overall execution time.

Not to mention that parallelism does not come for free, it adds overhead (e.g., scheduling the parallel work and so on), which might be high enough that makes it not worth to parallelize the code in the first place. In your code, there is at least the overhead of waiting for the tasks to be completed, and also the overhead of shutting down the pool of executors.

Another possibility that would also explain why you are not getting ArrayIndexOutOfBoundsException all over the place is that the first 3 tasks are so small that they are being executed by the same thread. This would also again make your overall execution time very dependent on the last task, the on the overhead of executor.shutdown(); and executor.awaitTermination. However, even if that is the case, the order of execution of tasks, and which threads will execute then, is typically non-deterministic, and consequently, is not something that your application should rely upon. Funny enough, when I changed your code to immediately execute the tasks (i.e., executor.execute) I got ArrayIndexOutOfBoundsException all over the place.