【问题标题】:Getting familiar with Threads in Java: Why does this program's runtime increase with increasing number of threads熟悉Java中的线程:为什么这个程序的运行时间随着线程数量的增加而增加
【发布时间】:2014-04-27 08:43:00
【问题描述】:

情况

我正在尝试熟悉 Java 中的线程。出于这个原因,我修改了我在一本书中找到的程序列表。它的作用非常简单:

  1. 它创建一个包含 100.000.000 个元素的 boolean[]-array。
  2. 它使用NUMBER_OF_SERVERS 线程使用truefalse 随机填充该数组的元素。
  3. 最后,它使用NUMBER_OF_SERVERS 线程扫描该数组并计算有多少条目设置为true

更多详情,请查看本文底部的代码。

问题

当我用不同数量的线程运行代码并测量运行时,我得到一个非常奇怪的结果;或者至少是我不理解的行为:当我使用更多线程时,BuildService-Thread 会消耗更多运行时。仅在一个线程中构建整个数组(基于随机truedistribution)大约需要 10 秒。接下来,当我使用四个线程时,我预计运行时间会减少。但是,我得到了大约 17 秒的时间消耗。

我的 ScanService 按预期工作:时间消耗随着线程的增加而减少。

详情请看下表:

但是,如果更改我的代码中的一行并将if ((int) ((Math.random() * 2d)) == 0)-statement(用于随机true-distribution)替换为if (i % 2 == 0)(因此,每隔一个项目将是真的)我会得到我期望的行为:

问题

所以,我的问题是:

  1. 为什么在使用 Math.random() 函数时,更多线程会导致更长的运行时间?
  2. 反之亦然,为什么当只使用一个线程使用完全相同的函数时,运行时间会减少?
  3. 当涉及到处理线程时,可以从这种行为中得出哪些“一般规则”?

背景信息

代码在 Intel core i3 上运行。

代码

public class AsynchService
{
    private static final int ARRAY_SIZE = 100000000; //100.000.000
    private static final int NUMBER_OF_SERVERS = 16;
    private static final int HOW_MANY = ARRAY_SIZE / NUMBER_OF_SERVERS;

    //build array asynch
    public static boolean[] buildArrayAsynch()
    {
        //build array with NUMBER_OF_SERVERS-Threads
        boolean[] array = new boolean[ARRAY_SIZE];
        Thread[] buildServerThread = new Thread[NUMBER_OF_SERVERS];

        long startTime = System.currentTimeMillis();

        for (int i = 0; i < NUMBER_OF_SERVERS; i++)
        {
            int start = i * HOW_MANY;
            int end = (i != NUMBER_OF_SERVERS - 1) ? (i + 1) * HOW_MANY - 1 : ARRAY_SIZE - 1;

            buildServerThread[i] = new BuildService(array, i, start, end);
        }

        //synchronize and wait for result
        int expectedResult = 0;

        for (int i = 0; i < NUMBER_OF_SERVERS; i++)
        {
            try
            {
                buildServerThread[i].join();
            }
            catch (InterruptedException ex) {}

            expectedResult += ((BuildService) buildServerThread[i]).getExpectedResult();
        }

        System.out.println("\nNumber of \"true\"s ==> Expected result: " + expectedResult);
        System.out.println("Build duration: " + (System.currentTimeMillis() - startTime) + " ms\n");

        return array;
    }

    //scan array asynch
    public static int scanArrayAsynch(boolean[] array)
    {
        //create services and server-threads
        Thread[] serverThread = new Thread[NUMBER_OF_SERVERS];

        long startTime = System.currentTimeMillis();

        for (int i = 0; i < NUMBER_OF_SERVERS; i++)
        {
            int start = i * HOW_MANY;
            int end = (i != NUMBER_OF_SERVERS - 1) ? (i + 1) * HOW_MANY - 1 : ARRAY_SIZE - 1;

            serverThread[i] = new ScanService(array, i, start, end);
        }

        //synchronize with servers, wait for server end
        int result = 0;

        for (int i = 0; i < NUMBER_OF_SERVERS; i++)
        {
            try
            {
                serverThread[i].join();
            }
            catch (InterruptedException ex) {}

            result += ((ScanService) serverThread[i]).getResult();
        }

        System.out.println("Search duration: " + (System.currentTimeMillis() - startTime) + " ms");
        return result;
    }

    public static void main(String[] args)
    {
        //build array
        boolean[] array = buildArrayAsynch();

        //scan array
        int result = scanArrayAsynch(array);

        //display result
        System.out.println("\nResult: " + result);

    }
}

class BuildService extends Thread
{
    private boolean[] array;
    private int start;
    private int end;
    private int expectedResult = 0;

    public BuildService(boolean[] array, int serviceId, int start, int end)
    {
        this.array = array;
        this.start = start;
        this.end = end;

        this.setName("BuildService " + serviceId);

        this.start();
    }

    public int getExpectedResult()
    {
        return expectedResult;
    }

    public void run()
    {
        if (start < 0 || end >= array.length) throw new IndexOutOfBoundsException();

        System.out.println(getName() + ": StartIndex = " + start + "; EndIndex = " + end);

        long startTime = System.currentTimeMillis();

        for (int i = start; i <= end; i++)
        {
            //if (i % 2 == 0)
            if ((int) ((Math.random() * 2d)) == 0)
            {
                array[i] = true;
                expectedResult++;
            }
            else
            {
                array[i] = false;
            }
        }

        System.out.println(getName() + " finished! \"true\" elements: " + expectedResult + "; duration = " + (System.currentTimeMillis() - startTime) + "ms");
    }
}

class ScanService extends Thread
{
    private boolean[] array;
    private int serviceId;
    private int start;
    private int end;
    private int result = 0;

    public ScanService(boolean[] array, int serviceId, int start, int end)
    {
        this.array = array;
        this.serviceId = serviceId;
        this.start = start;
        this.end = end;

        this.start();
    }

    public int getResult()
    {
        return result;
    }

    public void run()
    {
        if (start < 0 || end >= array.length) throw new IndexOutOfBoundsException();

        System.out.println("Server " + serviceId + ": StartIndex = " + start + "; EndIndex = " + end);

        for (int i = start; i <= end; i++)
        {
            if (array[i]) result++;
        }
    }
}

【问题讨论】:

    标签: java multithreading performance random


    【解决方案1】:

    魔鬼在细节中。 Math.random()documentation有答案:

    此方法已正确同步,以允许多个线程正确使用。但是,如果许多线程需要以很高的速度生成伪随机数,则可能会减少每个线程对拥有自己的伪随机数生成器的争用。

    要解决此问题,请尝试为您的 BuildService 类的每个实例创建一个 java.util.Random 实例,并使用它来代替 Math.random()。使用java.util.Random 的一个好处是您不必做不必要的双整数运算,而可以简单地使用nextBoolean() 方法。

    【讨论】:

    • 感谢您的好评!这帮助很大!我将在下面发布评估和我编辑的代码。但是,现在有一个后续问题:为什么只有一个线程的代码比具有多个线程的代码更快,都使用一种同步方法?当多个线程必须等待一个同步方法解锁时,您不希望代码几乎以恒定的速度运行吗?
    • 从绘制的数据中可以看出,当线程数增加时,BuildService 和 ScanService 都会收敛于常量。 BuildService 的偏移量是线程同步对程序施加的开销。这里要吸取的教训是,有时,线程只有在各个线程大部分独立工作并且很少使用共享资源时才真正有益。在这种情况下,每个线程在一个紧密而长的循环的每次迭代中都使用一个共享资源,因此考虑到所有因素,结果是完全合理的。
    • 感谢您的详细解释。如果可以的话,我会给你第二个“答案复选标记”!
    • 一个就够了!我本可以在实际答案中添加额外的信息。对于那些有兴趣深入研究 cmets 的人,我会将其作为“奖励”作为评论留下,muahaha!
    【解决方案2】:

    您可能在Math.random() 中看到了线程争用的影响。 Java 7 具有 ThreadLocalRandom 特性来避免这个问题。

    【讨论】:

      【解决方案3】:

      您的代码似乎使用了 16 个线程,每个线程都使用此处的 Join 方法

      serverThread[i].join();

      这似乎没有充分利用线程的潜力。

      当使用连接时,您实际上是在说线程要等到另一个线程完成,而不是并行运行线程。

      您可能希望使用 start 方法而不是 join 方法。

      尝试运行更改后的代码并在时间线上发布您的分析。

      祝你学习顺利

      【讨论】:

      • 阅读所有代码。 BuildServiceScanService 类扩展 Thread 并在创建时自行启动。这些创建的线程是加入辅助循环的线程,当他想等待所有线程都完成时,这是必要的。
      • 同意。您能否也评论一下这些担忧。对 Math.random() 上的非同步方法调用有什么影响,因为根据他的代码,多个线程永远不会并行访问它们,因为它们正在使用连接相互等待。
      • 再一次,工作线程不会互相等待。主线程等待所有并行运行的工作线程。在Thread 实例上调用join() 时,调用者等待被调用者。主线程(调用者)在工作线程(被调用者)上调用join(),因此对Math.random() 的调用是并行的。发生争用(因此速度变慢)是因为该方法实际上是同步的。
      • 安德烈亚斯是对的,我认为。 join() 方法用于使主线程等待服务线程完成。因此,您的陈述“每个线程都使用 Join 方法”似乎是不正确的。它是主线程调用它正在等待的所有线程对象上的方法。
      【解决方案4】:

      考虑到 Andreas Troelsen 的回答,我想出了下面显示的代码,导致以下运行时。

      与之前发生的情况相比,这个解决方案现在更好地满足了我的期望!

      import java.util.Random;
      
      class BuildService extends Thread
      {
          private boolean[] array;
          private int start;
          private int end;
          private int expectedResult = 0;
          private Random random = new Random();
      
          public BuildService(boolean[] array, int serviceId, int start, int end)
          {
              this.array = array;
              this.start = start;
              this.end = end;
      
              this.setName("BuildService " + serviceId);
      
              this.start();
          }
      
          public int getExpectedResult()
          {
              return expectedResult;
          }
      
          public void run()
          {
              if (start < 0 || end >= array.length) throw new IndexOutOfBoundsException();
      
              System.out.println(getName() + ": StartIndex = " + start + "; EndIndex = " + end);
      
              long startTime = System.currentTimeMillis();
      
              for (int i = start; i <= end; i++)
              {
                  array[i] = random.nextBoolean();
                  if (array[i]) expectedResult++;
              }
      
              System.out.println(getName() + " finished! \"true\" elements: " + expectedResult + "; duration = " + (System.currentTimeMillis() - startTime) + "ms");
          }
      }
      

      【讨论】:

        猜你喜欢
        • 2012-06-20
        • 2016-07-04
        • 1970-01-01
        • 2021-05-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-09-17
        • 2016-12-19
        相关资源
        最近更新 更多