Ad

Can Not Figure Out An Exception

I am writing simple code to asynchronously write logs to file, but find it difficult to figure out one issue.

I get java.util.NoSuchElementException in logNodes.removeFirst(). How can this happen if I check if the list is empty?

This issue mostly occurs if I log very frequently.

If anyone can explain to me why is this happening, it would be very appreciated.

My code:

private static class FileLogger extends Thread {
    private File logFile;
    private PrintWriter logWriter;
    private final LinkedList<LogNode> logNodes = new LinkedList<>();

    public FileLogger(Context context) {
        String dateString = (String) DateFormat.format("yyyy-MM-dd_HH:mm:ss", new Date());
        File logsDir = new File(context.getCacheDir(), "logs");

        if (logsDir.exists()) {
            for (File file : logsDir.listFiles()) {
                file.delete();
            }
        }

        try {
            logFile = new File(logsDir, dateString + ".log");
            if (!logFile.exists()) {
                logFile.getParentFile().mkdirs();
                logFile.createNewFile();
            }

            logWriter = new PrintWriter(new FileOutputStream(logFile));
            start();
        } catch (IOException ignored) {
        }
    }

    public void log(Date date, String tag, String msg) {
        if (isAlive()) {
            logNodes.addLast(new LogNode(date, tag, msg));
            synchronized (this) {
                this.notify();
            }
        }
    }

    @Override
    public void run() {
        while (true) {
            if (logNodes.isEmpty()) {
                try {
                    synchronized (this) {
                        this.wait();
                    }
                } catch (InterruptedException e) {
                    logWriter.flush();
                    logWriter.close();
                    return;
                }
            } else {
                LogNode node = logNodes.removeFirst();
                SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US);
                logWriter.println(String.format(
                        "%s %s.%s", dateFormat.format(node.date), node.tag, node.msg
                ));
                logWriter.flush();
            }
        }
    }

    private class LogNode {
        final Date date;
        final String tag;
        final String msg;

        public LogNode(Date date, String tag, String msg) {
            this.date = date;
            this.tag = tag;
            this.msg = msg;
        }
    }
}
Ad

Answer

Reason

You did not synchronize mutiple log threads.

Suppose you have thread1 and thread2:

  1. thread1 has written node1 into the queue.
  2. FileLogger noticed node1 when it call isEmpty, while thread2 did not notice it.
  3. thread2 think this list is empty, and let the list's first and last node be node2, which means node1 has be overwritten.
  4. Since you did not has any other synchronization, node2 might not be noticed by FileLogger, NoSuchElementException will be thrown.

Solution

Instead implementing a blocking queue yourself, try use BlockigQueue provided by java.util.concurrent, let it do the synchronization for you.

private static class FileLogger extends Thread {
    private File logFile;
    private PrintWriter logWriter;
    private final BlockingQueue<LogNode> logNodes = new LinkedBlockingQueue<>();

    public FileLogger(Context context) {
        String dateString = (String) DateFormat.format("yyyy-MM-dd_HH:mm:ss", new Date());
        File logsDir = new File(context.getCacheDir(), "logs");

        if (logsDir.exists()) {
            for (File file : logsDir.listFiles()) {
                file.delete();
            }
        }

        try {
            logFile = new File(logsDir, dateString + ".log");
            if (!logFile.exists()) {
                logFile.getParentFile().mkdirs();
                logFile.createNewFile();
            }

            logWriter = new PrintWriter(new FileOutputStream(logFile));
            start();
        } catch (IOException ignored) {
        }
    }

    public void log(Date date, String tag, String msg) {
        if (isAlive()) {
            logNodes.add(new LogNode(date, tag, msg));
        }
    }

    @Override
    public void run() {
        while (true) {
            try {
                LogNode node = logNodes.take();
                SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US);
                logWriter.println(String.format(
                        "%s %s.%s", dateFormat.format(node.date), node.tag, node.msg
                ));
                logWriter.flush();
            } catch (InterruptedException e) {
                logWriter.flush();
                logWriter.close();
                return;
            }
        }
    }
}
Ad
source: stackoverflow.com
Ad