如何使用多个线程处理大文本文件中的内容?

时间:2017-06-29 08:58:55

标签: java multithreading

我必须阅读一个包含文本的大文件,大约3GB(和40万行)。只是阅读它发生得非常快:

try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
  while ((line = br.readLine()) != null) {
    //Nothing here
  }
}

对于上面代码中的每个读取line,我对字符串进行一些解析并进一步处理它。(一项艰巨的任务)。我尝试做多线程

A)我已尝试BlockingQueue这样

try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
            String line;
            BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);
            int numThreads = 5;
            Consumer[] consumer = new Consumer[numThreads];
            for (int i = 0; i < consumer.length; i++) {
                consumer[i] = new Consumer(queue);
                consumer[i].start();
            }
            while ((line = br.readLine()) != null) {
                queue.put(line);
            }
            queue.put("exit");
        } catch (FileNotFoundException ex) {
            Logger.getLogger(ReadFileTest.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IOException | InterruptedException ex) {
            Logger.getLogger(ReadFileTest.class.getName()).log(Level.SEVERE, null, ex);
        }

class Consumer extends Thread {
        private final BlockingQueue<String> queue;
        Consumer(BlockingQueue q) {
            queue = q;
        }
        public void run() {
            while (true) {
                try {
                    String result = queue.take();
                    if (result.equals("exit")) {
                        queue.put("exit");
                        break;
                    }
                    System.out.println(result);
                } catch (InterruptedException ex) {
                    Logger.getLogger(ReadFileTest.class.getName()).log(Level.SEVERE, null, ex);
                }
            }

        }
    }

这种方法比普通的单线程处理花费更多时间。 我不知道为什么 - 我做错了什么?

B)我尝试过ExecutorService

 try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
            String line; 
            ExecutorService pool = Executors.newFixedThreadPool(10);         
             while ((line = br.readLine()) != null) {
                 pool.execute(getRunnable(line));
            }
             pool.shutdown();
         } catch (FileNotFoundException ex) {
            Logger.getLogger(ReadFileTest.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IOException ex) {
            Logger.getLogger(ReadFileTest.class.getName()).log(Level.SEVERE, null, ex);
        }

  private static Runnable getRunnable(String run){
        Runnable task = () -> {
            System.out.println(run);
        };
        return task;
    }

这种方法比在循环内部直接打印需要更多时间。我做错了什么?

这样做的正确方法是什么?

如何有效处理多个线程的读取line

3 个答案:

答案 0 :(得分:3)

在这里回答一个部分 - 为什么BlockingQueue选项较慢。

重要的是要理解线程不是为了“免费”而来。启动和“管理”它们总是需要某些开销

当然,当您实际使用的线程超过您的硬件可以“本机”支持时,上下文切换将被添加到账单中。

除此之外,BlockingQueue也不会自由。您会看到,为了保留 订单,ArrayBlockingQueue可能必须在某处同步。最坏的情况,这意味着锁定和等待。是的,JVM和JIT通常都很擅长这些东西;但同样,某个“百分比”会被添加到账单中。

但仅仅是为了记录,这应该不重要。来自javadoc

  

此类支持用于排序等待生产者和消费者线程的可选公平策略。默认情况下,不保证此顺序。但是,将fairness设置为true构造的队列以FIFO顺序授予线程访问权限。公平性通常会降低吞吐量,但会降低可变性并避免饥饿。

因为你没有设定“公平”

BlockingQueue queue = new ArrayBlockingQueue&lt;&gt;(100);

这不应该影响你。另一方面:我很确定你希望这些行按顺序处理,所以你真的想要去

BlockingQueue<String> queue = new ArrayBlockingQueue<>(100, true);

从而进一步减慢整个事情。

最后:我同意迄今为止的评论。对这些事情进行基准测试是一项复杂的工作;并且许多方面影响结果。最重要的问题绝对是:你的瓶颈在哪里?!是IO性能(然后更多的线程没有多大帮助!) - 或者它是否真的是整体处理时间(然后处理的“正确”线程数肯定会加快速度)。

关于“如何以正确的方式执行此操作” - 我建议在softwareengineering.SE上查看此question

答案 1 :(得分:2)

  

如何使用多个线程处理大文本文件中的内容?

如果您的计算机有足够的内存,我会执行以下操作:

  • 将整个文件读入变量(例如ArrayList) - 只使用一个线程来读取整个文件。

  • 然后启动一个ExecutorService(使用的线程池不超过计算机可以同时运行的最大内核数)

    h1 {
      color: #DC2827;
      font-size: 40px;
      font-weight: 700;
    }
    
    h3 {
      font-size: 30px;
    }
    
    h1, h2, h3 {
      font-family: 'Roboto Condensed', sans-serif;
    }
    
    .title {
      margin-top: 20px;
    }
    
    .main-img {
      width: 100%;
    }
    
    ul {
      list-style: none;
    }
    .time {
      color: #DC2827;
      font-size: 20px;
    }
    
    li, p {
      font-size: 18px;
      margin: 10px;
    }
    
    .nav-tabs {
       text-align:center;
       float:none;
       display:inline-block;
       margin-bottom: 30px;
    }
    
  • 最后,在有限数量的callables / runnables中划分读取的行,并将这些callables / runnables提交给ExecutorService(因此所有这些都可以在ExecutorService中同时执行)。

除非您对线路的处理使用I / O,否则我认为您将达到接近100%的CPU利用率,并且您的所有线程都不会处于等待状态。

你想要更快的处理吗?

垂直扩展是最简单的选择:购买更多RAM,更好的CPU(具有更多内核),使用固态硬盘

答案 2 :(得分:1)

可能是所有线程同时访问相同的共享资源,因此结果更具争议性。 有一件事你可以尝试阅读器线程将所有行放在单键中以分区方式提交,这样就不会引起争议。

public void execute(Runnable command){

    final int key= command.getKey();
     //Some code to check if it is runing
    final int index = key != Integer.MIN_VALUE ? Math.abs(key) % size : 0;
    workers[index].execute(command);
}

使用队列创建worker,这样如果你想按顺序执行某些任务,那么就运行。

private final AtomicBoolean scheduled = new AtomicBoolean(false);

private final BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>(maximumQueueSize);

public void execute(Runnable command) {
    long timeout = 0;
    TimeUnit timeUnit = TimeUnit.SECONDS;
    if (command instanceof TimeoutRunnable) {
        TimeoutRunnable timeoutRunnable = ((TimeoutRunnable) command);
        timeout = timeoutRunnable.getTimeout();
        timeUnit = timeoutRunnable.getTimeUnit();
    }

    boolean offered;
    try {
        if (timeout == 0) {
            offered = workQueue.offer(command);
        } else {
            offered = workQueue.offer(command, timeout, timeUnit);
        }
    } catch (InterruptedException e) {
        throw new RejectedExecutionException("Thread is interrupted while offering work");
    }

    if (!offered) {
        throw new RejectedExecutionException("Worker queue is full!");
    }

    schedule();
}

private void schedule() {
    //if it is already scheduled, we don't need to schedule it again.
    if (scheduled.get()) {
        return;
    }

    if (!workQueue.isEmpty() && scheduled.compareAndSet(false, true)) {
        try {
            executor.execute(this);
        } catch (RejectedExecutionException e) {
            scheduled.set(false);
            throw e;
        }
    }
}

public void run() {
    try {
        Runnable r;
        do {
            r = workQueue.poll();
            if (r != null) {
                r.run();
            }
        }
        while (r != null);
    } finally {
        scheduled.set(false);
        schedule();
    }
}

如上所述,线程池大小没有固定的规则。但是根据您的使用情况,可以使用一些建议或最佳实践。

CPU绑定任务

For CPU bound tasks, Goetz (2002, 2006) recommends

threads = number of CPUs + 1

IO绑定任务

Working out the optimal number for IO bound tasks is less obvious. During an IO bound task, a CPU will be left idle (waiting or blocking). This idle time can be better used in initiating another IO bound request.