带有Deflater的Java多线程压缩

时间:2012-10-30 03:18:18

标签: java multithreading compression gzip

所以这是一个我有点碰壁的作业。在这一点上,我大多希望能有更多的眼睛,因为在重做和精炼它之后我真的不能再看到我的代码有什么问题了。

我们需要在Java中编写一个多线程压缩器,可以使用gzip -d调用正确解压缩。我们无法使用GZIPOutputStream调用。相反,我们手动生成标题和预告片,并使用Deflater压缩数据。我们从标准输入读取并写入标准输出。

基本上,我使用和Executor来维护一个线程池。我读入输入并将其写入设置大小的缓冲区。一旦缓冲区已满,我将该数据块传递给一个线程(将一个任务放入队列中)。每个线程都有自己的Deflater,并传递输入和压缩该数据所需的任何其他信息。我还使用每个块的最后32Kb作为下一个块的字典。

我已经确认我的标题和预告片是正确的。我使用GZIPOutputStream来压缩文件并使用hexdump来获取字节,这样我就可以将它与输出进行比较。我检查了不同大小的文件,标题和预告片是相同的,所以很可能问题在于压缩数据。我得到的错误是:压缩数据无效 - crc错误

我已经确认当我传入一个相对较小的输入时(因为我从未填充缓冲区只有一个线程,所以队列中只有一个任务)输出是正确的。我可以在压缩数据上调用gzip -d并返回完全相同的输入。

换句话说,问题在于当有足够的数据时,多个线程启动并运行。我在输出上使用hexdump作为大文件并将其与GZIPOutputStream的hexdump进行比较,它们非常相似(不完全相同,但即使是小文件的情况,hexdump对于压缩数据也略有不同。那种情况下,gzip -d仍然有用)。这也是我知道标题和预告片是否正确的方式。

传入代码转储

import java.lang.Runtime;
import java.lang.String;
import java.lang.Integer;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.nio.ByteBuffer;
import java.io.*;
import java.util.zip.*;

/*Warning: Do not compress files larger than 2GB please. Since this is just
  an assignment and not meant to replace an actual parallel compressor, I cut corners
  by casting longs to ints, since it's easier to convert to 4 bytes*/

public class Main {
    private static final int BLOCK_SIZE = 128*1024;
    private static final int DICT_SIZE = 32*1024;
    private static byte[] header = {(byte)0x1f, (byte)0x8b, (byte)0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};

    public static void main(String[] args){

    class workerThread implements Callable<byte[]> {
        private boolean lastBlock;
        private boolean dictAvailable;
        private byte[] input;
        private byte[] dictionary;
        private int lastSize;       

        private byte[] output = new byte[BLOCK_SIZE];
        private int compressedLength;
        private ByteArrayOutputStream bOut = new ByteArrayOutputStream();
        Deflater compress = new Deflater (Deflater.DEFAULT_COMPRESSION, true);

        workerThread(boolean last, byte[] blockIn, byte[] dict, boolean useDictionary, int lastBSize){
            this.lastBlock = last;
            this.input = blockIn;
            this.dictionary = dict;
            this.dictAvailable = useDictionary;
            this.lastSize = lastBSize;
        }

        public byte[] call() {
            //System.out.println("running thread ");
            if (lastBlock) {
                // System.out.println("Last block!");
                compress.setInput(input,0,lastSize);
                if(dictAvailable) {
                    compress.setDictionary(dictionary);
                }
                compress.finish();
                compressedLength = compress.deflate(output,0,BLOCK_SIZE,Deflater.SYNC_FLUSH);
            }
            else {
                //System.out.println("Not last block!");
                compress.setInput(input,0,BLOCK_SIZE);
                if(dictAvailable) {
                    compress.setDictionary(dictionary);
                }
                compressedLength = compress.deflate(output,0,BLOCK_SIZE,Deflater.SYNC_FLUSH);
            }
            byte[] finalOut = Arrays.copyOfRange(output,0,compressedLength);
            return finalOut;
        }
    }

    getProcessors p = new getProcessors();
    boolean useDict = true;
    int numProcs = p.getNumProcs();
    boolean customProcs = false;
    boolean foundProcs = false;
    boolean foundDict = false;

    /*Checking if arguments are correct*/
    ........
    /*Correct arguments, proceeding*/

    BufferedInputStream inBytes = new BufferedInputStream(System.in);
    byte[] buff = new byte[BLOCK_SIZE];
    byte[] dict = new byte[DICT_SIZE];
    int bytesRead = 0;
    int offset = 0;
    int uncompressedLength = 0;
    int lastBlockSize = 0;
    boolean isLastBlock = false;
    boolean firstBlockDone = false;

    /*Using an executor with a fixed thread pool size in order to manage threads
    as well as obtain future results to maintain synchronization*/
    ExecutorService exec = Executors.newFixedThreadPool(numProcs);
    CRC32 checksum = new CRC32();
    checksum.reset();
    List<Future<byte[]>> results = new ArrayList<Future<byte[]>>();

    //byte[] temp;

    System.out.write(header,0,header.length);
    try{
        bytesRead = inBytes.read(buff,0, BLOCK_SIZE);
        while (bytesRead != -1)  {
            uncompressedLength += bytesRead;
            checksum.update(buff,offset,bytesRead);
            offset += bytesRead;

            if (offset == BLOCK_SIZE) {
                offset = 0;
                if(!firstBlockDone){ 
                firstBlockDone = true;
                results.add(exec.submit(new workerThread(isLastBlock,buff,dict,false,lastBlockSize)));
                }
                 else {
                     results.add(exec.submit(new workerThread(isLastBlock,buff,dict,useDict,lastBlockSize)));
                 }

                 if (useDict) {
                     System.arraycopy(buff, BLOCK_SIZE-DICT_SIZE, dict, 0, DICT_SIZE);
                 }
            }

             /*Implementation warning! Because of the way bytes are read in, this program will fail if
             the file being zipped is exactly a multiple of 128*1024*/
             if((bytesRead=inBytes.read(buff,offset,BLOCK_SIZE-offset)) == -1) {
                 isLastBlock = true;
                 lastBlockSize = offset;
                 results.add(exec.submit(new workerThread(isLastBlock,buff,dict,useDict,lastBlockSize)));
             }
        }    
        try {
            for(Future<byte[]> result: results) {
            //System.out.println("Got result!");
            System.out.write(result.get(),0,result.get().length);
            //temp = result.get();
            }
        }
        catch (InterruptedException ex) {
            ex.printStackTrace();
            System.err.println("Interrupted thread!");
        } 
        catch (ExecutionException ex) {
            ex.printStackTrace();
            System.err.println("Interrupted thread!");
        }
        finally{ 
            exec.shutdownNow();
        }

    /*Converting CRC sum and total length to bytes for trailer*/
    byte[] trailer = new byte[8];
    getTrailer trail = new getTrailer(checksum.getValue(),uncompressedLength);
    trail.writeTrailer(trailer,0);
    System.out.write(trailer);

    }
    catch (IOException ioe) {
        ioe.printStackTrace();
        System.out.println("IO error.");
        System.exit(-1);    
    }
    catch (Throwable e) {
        System.out.println("Unexpected exception or error.");
        System.exit(-1);
    }
    }
}
啊,哎呀,格式化代码块格式有点被抛弃了。

正如你所看到的,我一直在阅读输入,直到buff满了。原因是因为这不是一个文件,所以第一次调用read有可能没有读取足够的字节来填充数组(留下一堆我不想弄乱任何东西的空值)。一旦它已满,我将它交给Executor,这样线程就会执行任务。我实现了Callable而不是Runnable,这样我就可以将输出作为字节数组返回,因为我需要未来的接口。 exec.get()方法允许我保持线程同步。我已经用一个任意的情况测试了它(打印出数字1 - 100以确保,并且它们确实按顺序打印)。

有一个缺陷,这个程序不适用于BLOCK_SIZE倍数的文件,但这甚至都不是我现在的问题。当输入足够小以至于我只运行一个线程时,该程序有效。

对于除最后一个块之外的每个块,我使用SYNC_FLUSH选项调用deflate。这样我就可以在字节边界上结束了。我正常压缩的最后一个块并调用完成。

很抱歉很长的帖子。我真的需要除了我自己以外的更多意见,因为我似乎无法找到错误。如果有人想要编译并运行它以便自己查看,那么这里有其他类(只是为了获得进程的数量并生成预告片。这两种方法都可以。)

import java.io.*;

public class getTrailer {
    private long crc;
    private int total;
    public getTrailer (long crcVal, int totalIn) {
        this.crc = crcVal;
        this.total = totalIn;
    } 
    public void writeTrailer(byte[] buf, int offset) throws IOException {
        writeInt((int)crc, buf, offset); // CRC-32 of uncompr. data
        writeInt(total, buf, offset + 4); // Number of uncompr. bytes
    }

    /* Writes integer in Intel byte order to a byte array, starting at a
    * given offset
    */

    public void writeInt(int i, byte[] buf, int offset) throws IOException {
        writeShort(i & 0xffff, buf, offset);
        writeShort((i >> 16) & 0xffff, buf, offset + 2);
    }

   /*
    * Writes short integer in Intel byte order to a byte array, starting
    * at a given offset
    */

    public void writeShort(int s, byte[] buf, int offset) throws IOException {
        buf[offset] = (byte)(s & 0xff);
        buf[offset + 1] = (byte)((s >> 8) & 0xff);
    }
}

预告片功能实际上是从JAva的文档中复制粘贴的

public class getProcessors {
    private Runtime runner = Runtime.getRuntime();
    private int nProcs = runner.availableProcessors();

    int getNumProcs() {
        return nProcs;
    }
}

我意识到这是多久,但我真的需要别人的意见。如果有人看到他们认为可能导致问题的任何事情,请告诉我。我不需要有人为我编写程序(我想我差不多了)但我只是......看不出有什么不对。

2 个答案:

答案 0 :(得分:0)

所以,我的第一个猜测是你以错误的字节顺序写CRC。这似乎是您第一次一次写入4个字节。

答案 1 :(得分:0)

如果你是为一个类做这个,并且你所呈现的内容看起来与接受的类似,那么我希望这个类完全是关于结构化的,程序化的编程,因为你所呈现的是什么以及面向对象的解决方案是什么样的喜欢相隔数英里。

您的评论,

“exec.get()方法允许我保持线程同步。我已经测试了任意情况(打印数字1 - 100以确保,并且它们确实按顺序打印)。”

与人们对多线程解决方案的期望完全相反。多线程解决方案将以完全不可预测的顺序输出数字1-100。它按顺序出现意味着您已经同步了多线程的所有好处。在继续进行之前等待缓冲区填满会立即引起我的注意。

根据责任将解决方案分解为类。您正在对操作(即getProcessors,getTrailer)建模您的类,这是错误的。不要根据活动或状态对类进行建模。大多数时候,只是谈论你要做的事情会产生正确的类(即我们有一些输入数据,压缩器,解压缩器,某种工作队列,预告片等。如果你需要操纵一个处理程序列表,然后有一个包装(有一个,而不是一个)List的Processors类。每个类在整个解决方案中都有一个特定的职责,每个类只在其自身上运行(没有公共访问者)。当每个类时能够在独立测试中执行其功能,然后您就可以在多线程解决方案中使用它们的实例了。

如果您创建一个由您认为在解决方案中的类组成的域模型,然后通过向适当的类添加方法开始对功能建模,模型本身将开始通知您交互应该如何编码。提示:构造函数可以接受低级结构作为参数,其他方法不应该。

最重要的是,不要线性思考 - 你有一个Main()方法从上到下执行处理 - BUZZZZZ。回复不正确。解决方案应该是一组类之间相互作用的症状,每个类提供整体解决方案的独特和独立部分。

最好的多线程解决方案不需要同步 - 线程是谨慎的,并且能够以尽可能高的速度运行。实现这一目标的一种简单方法是确保每个线程使用自己所涉及的任何类的实例 - 不要使用共享内存。如果在输出端需要同步,则线程应将其结果转储到一个类中,该类将在输出之前执行排序作为最后一步。

最后,你确定你是正确的吗?我认为我更希望在不同的数据源上启动多个实例而不是一个数据源上的多个线程。我们知道每个源都需要从头到尾完全处理 - 问题就变成我们希望尽可能快地处理每个源,或者我们是否希望能够同时处理多个源,这样我们就不在乎如何每个人都可以长。

在单个线程上执行单个源的处理允许我同时处理多个源,并且在解决方案方面非常简单 - 有迹象表明这是执行多线程的好地方。

对单个源执行多线程处理会增加相当大的复杂性,并且由于数据的性质(必须按输入顺序),多线程并不是一个好的解决方案。