10亿条long型数据外部排序的文件分割实践及优化过程(JAVA)

 一、题目

        生成10亿个long随机数正整数,把它写入一个文件里。然后实现一个函数 fetch(int k,int n)。(fetch函数的输出结果是这10亿个正整数中从小到大中第k个开始(不包含第k个)往后取n个数。)
        给定内存为1G(可为2G)。


二、题目分析


    (1)首先生成10亿个long随机正整数,可考虑使用ThreadLocalRandom和多线程生成随机数。由于全部数据内存占用10几G,需要分批写入文件。(一个数据一行,行末为\n)
    (2)fetch函数的实现:
    1.先对随机数进行外部排序。由于随机数文件较大,无法一次性读取全部数据进行排序,所以必须对随机数文件进行分割成多个完成数据排序的小文件,然后通过多路归并实现外部排序。
    2.然后实现fetch函数,输出结果
    因此本文主要针对外部排序的文件分割部分进行说明,至于多路归并和fetch函数的实现本文暂不开展。


 三、程序设计

        本文主要针对大文件切割的程序设计进行分析。 (为了方便描述,后文将大文件切割分为read、parse、sort、write四个工序来描述)  
        设计思路:通过BufferedReader的readLine()方法读取每一行数据为String(read),通过parseLong()将String转换为long(parse),存放在一个long[]数组里。当装满long[]时通过Araay.sort()排序(sort),将排序好的long[]按行输出(write)。(long[]大小自行设定)通过多次循环操作实现大文件分割。为了加快效率,我用一个线程执行read、parse,另一个线程执行sort、write,两线程间用BlockingQueue交流数据。

由于内存开销很大,而且由于过大的内存开销,很容易就堆满了,且毫无效率可言,所以必须优化。优化思路是通过duox多线程进行read,一个线程处理parse、sort、write如图:

           read部分:使用readLine()10亿个数据要读10亿次,因此考虑采用RandomAccessFile和多线程结合进行读取,根据偏移量进行分次读取,每次读取32M(这个量是比较快而且不容易出现堆满的)。读取的字节数据存放在byte[]数据里,这时会出现一个新问题:每次读取的数据的末尾不一定是以“\n”结束,那么必定有个随机数被分割了!
        由于采用多线程进行IO读操作,因此为了解决随机数被分割问题费了点心思。
        主要思路:每完成一次read(b, 0, length)之后,往后继续read()一个byte,直到遇到第一个[10](即'\n');同时还要判断每次read起始部分是否为完整的一个随机数,从byte[0]开始判断直到遇见第一个[10](即'\n')。注意临界条件:第一组数据和最后一组数据的处理。这样才能在parse的时候数据时完整的。由于代码不小心删了,这里就提供一个思路。
        
         write部分:将long数据用BufferWriter按行写出为字符,这样的效率比较低,且占用内存较多。后来考虑到分割的文件是临时文件,fetch函数使用完之后就删除了。所以考虑使用DataOutputStream包装BufferOutputStream输出为一个个8字节的long,这样减少了一半以上的文件大小,且能提高输出效率。(ps:这算是一个不错的想法)
         历经千辛万苦,跑了20几分钟才分割完数据,还是太慢了。
         反思:方案一有许多不足之处。
         采用多线程进行I/O操作并不一定会提高效率,有时反而会影响效率。因为一个磁盘一个时间段内只能进行一个I/O操作,如果通过多线程进行I/O操作,可能造成每次I/O是磁头寻道的偏移量较大,也就是寻道时间长,反而增加了I/O时间。
         其次parse部分。将byte转换为String,每次新的一个String都会占用常量池。为了避免使用String,需直接将byte[]数据转换为long。于是乎想到了迭代计算,同时参考parseLong()的源码,进行优化。


     优化:


         如果想要提高效率,多线程的使用时必须的,那么如何使用多线程很关键。后经高人点播:既然大文件分割分为read、parse、sort、write四个部分,而且电脑是四核(二核四线程),那么一个部分用一个线程进行操作,形成一条流水线,流水线上的数据通过BlockingQueue来传递,这样可以提高CPU的利用率。(这个流水线模式是确定的,因此后文按照不同工序的优化过程来描述)


         read部分: 这里将每次读取的数据通过BlockingQueue直接传递给parse线程(后续parse部分给出解决随机数分割的问题的方法)。为了减少写出文件的数量,我尽可能的将spiltSize设置大(实际上这个方式并没有充分利用流水线模式)。由于每次read时间较长,后置的线程会先处于阻塞状态。

        由于缺乏对磁盘IO的理解,我局限的认为一次性读取的数据越大(取32M),减少I/O次数而提高效率,同时我又想保证每次写出的数据也越多越好,这样也可以减少后续归并的路数。因此将每次读取文件大小尽可能调大。
        通过对磁盘IO的了解:影响磁盘的关键因数是磁盘服务时间,即磁盘完成一个I/O请求所花费的时间,它由寻道时间、旋转延迟和数据传输时间三部分构成。其中寻道时间、旋转延迟是占主要的,数据传输时间可以忽略。由于磁盘上每个扇区512byte,而操作系统的文件系统不是一个扇区一个扇区的来读数据,所以有了block(块)的概念,它是一个块一个块的读取的,块(block)是基本的数据传输单元(一般的操作系统block size

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值