C#自定义日志类实现问题:多线程日志队列与调用确认
嘿,我明白你想自己撸一个日志类的想法——不想依赖第三方库,还得搞定多线程多类调用下的日志入队和展示,确实得把线程安全这块抠细了。从你给的代码片段和描述来看,核心问题大概率出在多线程下的队列访问安全和LogWork方法的调用逻辑冗余上,咱们一步步捋清楚:
首先,补全你可能的代码逻辑(猜你是写到锁那里断了)
你提到的private static reado...应该是ReentrantLock吧?很多人实现自定义日志类时会用锁来保证单例和队列安全,但如果锁的用法不对,或者队列本身不是线程安全的,就会出现需要重复确认LogWork调用的问题。
核心问题分析
- 单例的线程不安全:如果你的
mInstance初始化没做线程安全处理,多线程下可能会创建多个Logger实例,导致日志队列分散,LogWork调用混乱。 - 普通Queue的并发风险:如果用的是
LinkedList这类非线程安全的Queue,多线程同时入队时会出现元素丢失、队列结构损坏的情况,这时候你可能会反复检查LogWork是否正确触发。 - LogWork调用逻辑不合理:如果每次入队日志都手动调用LogWork,可能会导致重复执行,或者在多线程下触发多次,你不得不反复确认是否调用正确。
解决方案:线程安全的日志类实现
下面是一个修正后的完整实现,解决上述问题:
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; // 定义日志级别枚举 enum LogLevel { VERBOSE, DEBUG, INFO, WARN, ERROR } // 日志条目类,封装日志的所有信息 class LogEntry { private LogLevel level; private String message; private String className; private long timestamp; public LogEntry(LogLevel level, String message, String className) { this.level = level; this.message = message; this.className = className; this.timestamp = System.currentTimeMillis(); } // getter方法 public LogLevel getLevel() { return level; } public String getMessage() { return message; } public String getClassName() { return className; } public long getTimestamp() { return timestamp; } } public class Logger { // 枚举单例,天生线程安全,避免双重检查锁定的坑 private enum SingletonHolder { INSTANCE; private final Logger logger = new Logger(); } private final LogLevel mLogLevel; // 用ConcurrentLinkedQueue,天生线程安全,不需要额外加锁处理入队出队 private final ConcurrentLinkedQueue<LogEntry> mLogQueue; // 单线程线程池,保证日志顺序输出,同时处理后台消费 private final ExecutorService mLogExecutor; private Logger() { // 默认日志级别设为INFO,可根据需求调整 mLogLevel = LogLevel.INFO; mLogQueue = new ConcurrentLinkedQueue<>(); // 创建后台守护线程,随应用退出自动终止 mLogExecutor = Executors.newSingleThreadExecutor(r -> { Thread t = new Thread(r, "Logger-Worker-Thread"); t.setDaemon(true); return t; }); // 启动日志消费逻辑 startLogConsumer(); } public static Logger getInstance() { return SingletonHolder.INSTANCE.logger; } // 对外暴露的日志入队API public void log(LogLevel level, String message, String className) { if (level.ordinal() < mLogLevel.ordinal()) { return; // 低于当前日志级别,直接忽略 } LogEntry entry = new LogEntry(level, message, className); mLogQueue.offer(entry); // 这里不需要手动调用LogWork,后台线程会自动处理 } // 快捷日志方法,简化调用 public void d(String message, String className) { log(LogLevel.DEBUG, message, className); } public void i(String message, String className) { log(LogLevel.INFO, message, className); } public void e(String message, String className) { log(LogLevel.ERROR, message, className); } // 后台日志消费逻辑(替代你的LogWork) private void startLogConsumer() { mLogExecutor.submit(() -> { while (!Thread.currentThread().isInterrupted()) { LogEntry entry = mLogQueue.poll(); if (entry != null) { // 这里是你的日志展示逻辑,比如打印到控制台、更新UI等 processLogEntry(entry); } else { try { // 队列为空时休眠,避免空轮询浪费CPU Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } } }); } // 实际处理日志的方法 private void processLogEntry(LogEntry entry) { // 示例:打印到控制台,你可以改成展示给用户的UI逻辑 String logStr = String.format("[%d] [%s] [%s]: %s", entry.getTimestamp(), entry.getLevel().name(), entry.getClassName(), entry.getMessage()); System.out.println(logStr); // 如果是Android应用,可通过Handler.post更新UI;如果是桌面应用,可更新日志面板 } // 应用退出时调用,关闭线程池 public void shutdown() { mLogExecutor.shutdownNow(); } }
关键改进点说明
- 线程安全的单例:用枚举实现单例,比双重检查锁定更可靠,完全避免多线程下的单例实例重复创建问题。
- ConcurrentLinkedQueue:这个队列是线程安全的,
offer()和poll()方法都是原子操作,不需要额外加锁,解决了多线程入队的安全问题。 - 后台消费线程:把LogWork(也就是
processLogEntry)的调用交给后台单线程处理,不需要每次入队都手动触发,从根本上解决了“需要两次确认LogWork是否正确调用”的问题——后台线程会自动轮询队列,有日志就处理,没有就休眠。 - 日志级别过滤:提前过滤低于当前级别的日志,减少不必要的入队操作。
使用示例
在你的其他类中,直接调用即可,多线程下也完全安全:
// 在主线程的类中 Logger.getInstance().i("用户登录成功", MainActivity.class.getSimpleName()); // 在子线程中 new Thread(() -> { Logger.getInstance().d("子线程执行任务", "BackgroundTask"); }).start();
这样改造后,你就不用再反复确认LogWork的调用了——后台线程会稳定地处理所有入队的日志,而且多线程下的日志顺序也能保证(因为用了单线程线程池)。
内容的提问来源于stack exchange,提问作者YogoWafel
相关产品推荐
相关产品推荐

