使用#threads进行多线程字符串处理

时间:2015-04-14 20:09:13

标签: java string parsing executorservice

我正在处理多线程项目,我们必须将文件中的一些文本解析为魔术对象,对对象进行一些处理,然后聚合输出。旧版本的代码在一个线程中解析文本,并使用Java ExecutorService在线程池中进行对象处理。我们没有获得我们想要的性能提升,结果发现解析需要比我们想象的相对于每个对象的处理时间更长的时间,所以我尝试将解析移动到工作线程中。

这应该有效,但实际发生的是每个对象的时间随着池中线程数的增加而爆炸。它比线性更差,但不如指数那么糟糕。

我已将其缩小为一个小例子(无论如何在我的机器上)显示行为。这个例子甚至没有创建魔术对象;它正在进行字符串操作。我可以看到没有线程间的依赖关系;我知道split()并不是非常有效但我无法想象为什么它会在多线程环境中占据一席之地。我错过了什么吗?

我在24核机器上运行Java 7。线条很长,每条约1MB。 features中有数十项,edges中有100k +项。

示例输入:

1    1    156    24    230    1350    id(foo):id(bar):w(house,pos):w(house,neg)    1->2:1@1.0    16->121:2@1.0,3@0.5

使用16个工作线程运行的示例命令行:

$ java -Xmx10G Foo 16 myfile.txt

示例代码:

public class Foo implements Runnable {
String line;
int id;
public Foo(String line, int id) {
    this.line = line;
    this.id = id;
}
public void run() {
    System.out.println(System.currentTimeMillis()+" Job start "+this.id);
    // line format: tab delimited                                                                
    // x[4]
    // graph[2]
    // features[m]      <-- ':' delimited                                              
    // edges[n]
    String[] x = this.line.split("\t",5);
    String[] graph = x[4].split("\t",4);
    String[] features = graph[2].split(":");
    String[] edges = graph[3].split("\t");
    for (String e : edges) {
        String[] ee = e.split(":",2);
        ee[0].split("->",2);
        for (String f : ee[1].split(",")) {
            f.split("@",2);
        }
    }                                                                    
    System.out.println(System.currentTimeMillis()+" Job done "+this.id);
}
public static void main(String[] args) throws IOException,InterruptedException {
    System.err.println("Reading from "+args[1]+" in "+args[0]+" threads...");
    LineNumberReader reader = new LineNumberReader(new FileReader(args[1]));
    ExecutorService pool = Executors.newFixedThreadPool(Integer.parseInt(args[0]));
    for(String line; (line=reader.readLine()) != null;) {
        pool.submit(new Foo(line, reader.getLineNumber()));
    }
    pool.shutdown();
    pool.awaitTermination(7,TimeUnit.DAYS);
}
}

更新

  • 首先将整个文件读入内存无效。更具体地说,我读了整个文件,将每一行添加到ArrayList<String>。然后我遍历列表以创建池的作业。这使得吃掉子串的假设不太可能,不是吗?
  • 编译要由所有工作线程使用的分隔符模式的一个副本无效。 :(

解决:

我已经将解析代码转换为使用基于indexOf()的自定义拆分例程,如下所示:

private String[] split(String string, char delim) {
    if (string.length() == 0) return new String[0];
    int nitems=1;
    for (int i=0; i<string.length(); i++) {
        if (string.charAt(i) == delim) nitems++;
    }
    String[] items = new String[nitems];
    int last=0;
    for (int next=last,i=0; i<items.length && next!=-1; last=next+1,i++) {
        next=string.indexOf(delim,last);
        items[i]=next<0?string.substring(last):string.substring(last,next);
    }
    return items;       
}

奇怪的是,当线程数量增加时,会爆炸,我不知道为什么。这是一个功能性的解决方法,所以我会忍受它......

2 个答案:

答案 0 :(得分:1)

在Java 7中,String.split()在内部使用String.subString(),用于&#34;优化&#34;原因不会创建真正的新Strings,而是指向原始String的空split()个外壳。

所以,当你StringString.split()变成小块时,原始的(可能是巨大的)仍然在记忆中,最终可能会吃掉你所有的堆。我看到你解析大文件,这可能是一个风险(在Java 8中已经改变了)。

鉴于您的格式众所周知,我建议您手动解析每一行&#34;#34;相反,使用{{1}}(正则表达式无论如何都非常糟糕),并为子部件创建真正的新版本。

答案 1 :(得分:1)

String.split实际上是使用正则表达式进行拆分,请参阅:http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/java/lang/String.java#String.split%28java.lang.String%2Cint%29 这意味着您每次迭代都要编译一大堆正则表达式。

最好为整行编译一次模式,然后将其应用到每一行,因为它会读取它们进行解析。或者,编写自己的解析器,可以查找字符符,而不是正则表达式。