同时运行100,000个进程

时间:2015-03-20 06:29:54

标签: java concurrency jvm

我正在模拟一个银行系统,我要运行100,000个交易。每种类型的事务都实现了runnable,我可以发生各种类型的事务。

transactions是一个Runnables数组。

理想情况下,以下代码可以解决我的问题:

for (Transaction transaction : transactions) {
    new Thread(transaction).start();
}

但是,显然在尝试启动100,000个线程时会发生java.lang.OutOfMemoryError: unable to create new native thread

接下来我尝试实现ExecutorService来创建一个线程池来管理我的100,000个runnable。

ExecutorService service;
int cpus = Runtime.getRuntime().availableProcessors();
// cpus == 8 in my case
service = Executors.newFixedThreadPool(cpus);

for (Transaction transaction : transactions) {
    service.execute(transaction);
}

尝试这种方法时,需要长时间处理" hog" JVM。例如,一种类型的事务需要30-60秒才能执行。在分析应用程序时,在长事务发生时不允许其他线程运行。

In this case, thread 6 did not allow any other threads to run until its single transaction was complete

在这种情况下,线程6在其处理完成之前不允许任何其他线程运行。

所以我的问题是:如何在不遇到内存问题的情况下尽快运行100,000个事务?如果ExecutorService是答案,那么如何阻止非常长的事务占用JVM并允许其他事务同时运行?

修改

我正在强制某些类型的事务发生30-60秒,以确保我的线程程序正常工作。每个事务锁定一个帐户,有10个帐户。这是我的方法,它占用了JVM :(由run()调用)

public void makeTransaction() {
    synchronized(account) {
        long timeStarted = System.nanoTime();
        long timeToEnd = timeStarted + nanos;

        this.view = new BatchView(transactionNumber, account.getId());

        this.displayView();

        while(true) {
            if(System.nanoTime() % 1000000000 == 0) {
                System.out.println("batch | " + account.getId());
            }

            if(System.nanoTime() >= timeToEnd) {
                break;
            }
        }
    }
}

每次运行此事务时,只有一个帐户被锁定,剩下9个应该可用于处理的帐户。 为什么JVM不再处理任何线程,而是在此长事务完成之前挂起?

以下是项目缩小版的链接,用于演示问题:project

4 个答案:

答案 0 :(得分:10)

  

在分析应用程序时,在长事务发生时不允许其他线程运行。

最有可能的是,此任务使用的是单线程资源。即写入ti的方式可防止并发使用。

  

如何在不遇到内存问题的情况下尽快运行100,000个事务?

如果事务是CPU绑定的,则应该有一个与您拥有的CPU数量大小相同的池。

如果事务依赖于数据库,您应该查看对它们进行批处理以更有效地利用数据库。

  

如果ExecutorService是答案,那么如何阻止非常长的事务占用JVM并允许其他事务同时运行?

使交易更短。如果你的任务运行超过几毫秒,你应该弄清楚为什么这么长时间。我首先看一下网络/ IO必须如何使用和分析任务。大多数交易(如果您的数量很大)应该在0.01秒左右或理想情况下远远不够。

您应该特别注意如何使用共享资源。如果您的任务使用相同的资源太多,您可能会发现多线程不会更快,甚至更慢。

答案 1 :(得分:2)

您的应用程序的问题是很快所有线程都会为同一个帐户分配一个事务,然后除了一个线程之外的所有线程都必须等待。您可以在下面的屏幕截图中看到这一点,我暂停了应用程序。线程池-1-thread-3当前正在处理ID为19的Account对象的事务(此id不是您的帐户ID,而是Eclipse分配的唯一对象ID),并且所有其他线程都在等待锁定帐户对象。帐户对象是您的ID为9的帐户。

Screenshot of debugger

为什么会这样?在事务853中,一个线程启动第一个长时间运行的事务(对于帐户9)。其他线程继续处理其他事务。但是,当任何线程到达帐户9的另一个事务时,它将必须停止并等待。事务857,861和862也用于帐户9,并且每个都阻塞一个线程,所以此时我的所有线程都被阻塞(在我的四核上)。

如何解决这个问题?这取决于您的使用案例。

如果在您的实际程序中,只要有另一个帐户X的交易运行,就可以保证给定帐户X没有传入的交易,您无需更改任何内容。

如果您的帐户数量与线程数量相比非常大,则问题变得更加不可能,因此您可能决定使用它。

如果您的帐户数量相对较少(假设可能少于一百个左右),您应该(正如彼得所说)每个帐户都有一个(无休止地运行)的帖子,每个帐户都有自己的交易队列。这可能会更有效率,因为线程不需要"战斗"通过共享队列。

另一种解决方案是实施某种形式的工作窃取"。这意味着每当一个线程被阻塞时,它就会寻找其他一些工作要做。要实现这一点,首先需要能够检查线程是否可以获取给定帐户的锁定。在Java中使用synchronized这是不可能的,因此您需要ReentrantLock.tryLock()之类的内容。您还需要能够从每个线程直接访问事务队列,所以我想你不能在这里使用ExecutorService但需要自己实现事务处理(使用LinkedBlockingQueue)。

现在每个线程都会在循环中轮询队列中的事务。首先,它尝试使用tryLock()获取相应帐户的锁定。如果失败,请将事务添加到(特定于线程)列表,从队列中获取下一个事务,然后尝试执行此事务,直到找到可以处理的事务。事务完成后,首先查看列表中的当前可能处理事务,然后再从全局队列中提取另一个事务。代码可以大致如下:

public BlockingQueue<Transaction> queue = ...; // the global queue for all threads

public void run() {
   LinkedList<Transaction> myTransactions = new LinkedList<>();
   while (true) {
     Transaction t = queue.take();
     while (!t.getLock().tryLock()) {
        myTransactions.add(t);
     }
     try {
       // here we hold the lock for t
       t.makeTransaction();
     } finally {
       t.getLock().unlock();
     }

     Iterator<Transaction> iter = myTransactions.iterator();
     while (iter.hasNext()) {
       t = iter.next();
       if (t.getLock().tryLock()) {
         try {
           t.makeTransaction();
         } finally {
           t.getLock().unlock();
         }
         iter.remove();
       }
     }
   }
 }

请注意,至少还有以下问题需要解决:

  • 当一个帖子在queue.take()中挂起时,它不会检查其列表中的事务是否可用。因此,如果有queue为空的时间段(例如,在处理结束时),则可能会在列表中停留未处理的事务。
  • 如果某些线程持有大量的锁,剩下的线程可能会占用他们现在无法处理的大量事务,因此他们只会填充其本地列表,从而耗尽全局队列。释放锁时,许多事务可能已从全局队列中删除,从而在线程可以执行的工作之间产生不平衡(某些线程可能处于空闲状态,而其他线程仍在处理其长时间积压的事务)。

一个更简单的替代方案可能是put()进入队列的事务(最后),如果你不能获取它们的锁,但这会使它们以非常任意的顺序执行(这可能发生在上面解决方案,也许不是那么极端)。

编辑: 更好的解决方案可能是将队列附加到每个帐户而不是特定于线程的列表。然后,只要发现此帐户被阻止,线程就会将事务添加到相应帐户的队列中。当一个线程完成账户X的交易时,如果在那里添加了任何交易,它应首先查看账户X的队列,然后再查看全局列表。

答案 2 :(得分:1)

根据您的硬件计算可以并行处理事务的工作线程数非常重要。可用于调整线程池大小的公式很少

对于CPU绑定应用程序

  

N * U或(N + 1)* U

对于IO绑定应用程序

  

N * U *(1 + W / C)

,其中 N - 处理器数量 U - 目标CPU利用率 W - 等待时间 C - 计算时间

例如,如果您的应用程序使用50%CPU并且您拥有8个内核。然后为CPU绑定应用程序实现高效的多线程

  

8 *(0.5)= 4

如果您有4个线程,那么所有核心都将得到有效处理。一些支持hyperthreading

的公猪会发生这种变化

答案 3 :(得分:-1)

如果您使用笔记本电脑甚至是16核桌面,那么在单独的线程中执行100,000次调用很难。您将需要一个网格或一组服务器来优化执行此操作。

但是,您仍然可以通过执行callback中的任何事务操作来扩展它。您的吞吐量可能会增加。