在Java多线程应用场景中,多个线程同时调用日志记录方法时,如果没有做线程安全处理,可能会出现日志内容交叉拼接、日志文件写入异常、日志数据丢失等问题,因此实现线程安全的日志记录是保障程序稳定运行的重要一环。下面介绍几种常见的实现方案。

方案一:使用synchronized同步锁
最基础的线程安全实现方式是对日志记录方法添加同步锁,保证同一时间只有一个线程可以执行日志写入操作,避免并发冲突。这种方案实现简单,但在高并发场景下会因为锁竞争导致性能下降。
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class SynchronizedLogger {
private final String logFilePath;
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public SynchronizedLogger(String logFilePath) {
this.logFilePath = logFilePath;
}
// 使用synchronized关键字保证方法线程安全
public synchronized void log(String message) {
String timestamp = dateFormat.format(new Date());
String logContent = "[" + timestamp + "] " + message + "n";
try (FileWriter writer = new FileWriter(logFilePath, true)) {
writer.write(logContent);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SynchronizedLogger logger = new SynchronizedLogger("app.log");
// 模拟多线程写入日志
for (int i = 0; i < 5; i++) {
final int threadId = i;
new Thread(() -> {
for (int j = 0; j < 3; j++) {
logger.log("线程" + threadId + "的第" + j + "条日志");
}
}).start();
}
}
}
方案二:使用ThreadLocal实现线程隔离日志
ThreadLocal可以为每个线程提供独立的日志缓冲区,每个线程的日志先写入自己的缓冲区,最后再统一刷入文件,避免多个线程直接竞争文件写入资源。这种方案适合需要按线程维度收集日志的场景,但需要注意缓冲区的内存占用和刷写时机。
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class ThreadLocalLogger {
private final String logFilePath;
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 每个线程独立的日志缓冲区
private final ThreadLocal<List<String>> threadLocalLogs = ThreadLocal.withInitial(ArrayList::new);
public ThreadLocalLogger(String logFilePath) {
this.logFilePath = logFilePath;
}
public void log(String message) {
String timestamp = dateFormat.format(new Date());
String logContent = "[" + timestamp + "] " + message;
// 先写入当前线程的缓冲区
threadLocalLogs.get().add(logContent);
}
// 刷写当前线程的日志到文件
public void flush() {
List<String> logs = threadLocalLogs.get();
if (logs.isEmpty()) {
return;
}
try (FileWriter writer = new FileWriter(logFilePath, true)) {
for (String log : logs) {
writer.write(log + "n");
}
} catch (IOException e) {
e.printStackTrace();
}
// 清空缓冲区,避免重复写入
logs.clear();
// 移除ThreadLocal中的值,防止内存泄漏
threadLocalLogs.remove();
}
public static void main(String[] args) throws InterruptedException {
ThreadLocalLogger logger = new ThreadLocalLogger("app_threadlocal.log");
// 模拟多线程写入日志
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 3; i++) {
final int threadId = i;
Thread thread = new Thread(() -> {
for (int j = 0; j < 2; j++) {
logger.log("线程" + threadId + "的第" + j + "条日志");
}
// 线程执行完成后刷写日志
logger.flush();
});
threads.add(thread);
thread.start();
}
// 等待所有线程执行完成
for (Thread thread : threads) {
thread.join();
}
}
}
方案三:使用阻塞队列实现异步日志
通过阻塞队列作为日志的中转缓冲区,日志生产线程只需要把日志内容放入队列,由单独的消费者线程负责从队列中取出日志并写入文件,既保证了线程安全,又避免了日志写入操作阻塞业务线程,是高并发场景下的常用方案。
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class QueueLogger {
private final String logFilePath;
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 阻塞队列作为日志缓冲区,容量为1000
private final BlockingQueue<String> logQueue = new ArrayBlockingQueue<>(1000);
private volatile boolean isRunning = true;
public QueueLogger(String logFilePath) {
this.logFilePath = logFilePath;
// 启动日志消费线程
startConsumer();
}
// 日志记录方法,放入队列即可,非阻塞(队列满时会阻塞,也可以根据需求调整为丢弃策略)
public void log(String message) {
String timestamp = dateFormat.format(new Date());
String logContent = "[" + timestamp + "] " + message;
try {
logQueue.put(logContent);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
// 启动消费者线程,不断从队列取日志写入文件
private void startConsumer() {
new Thread(() -> {
try (FileWriter writer = new FileWriter(logFilePath, true)) {
while (isRunning || !logQueue.isEmpty()) {
String log = logQueue.take();
writer.write(log + "n");
// 批量刷写,减少IO次数
writer.flush();
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}, "log-consumer-thread").start();
}
// 停止日志服务,等待剩余日志写入完成
public void shutdown() {
isRunning = false;
}
public static void main(String[] args) throws InterruptedException {
QueueLogger logger = new QueueLogger("app_queue.log");
// 模拟多线程写入日志
for (int i = 0; i < 5; i++) {
final int threadId = i;
new Thread(() -> {
for (int j = 0; j < 3; j++) {
logger.log("线程" + threadId + "的第" + j + "条日志");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}).start();
}
// 等待一段时间,让日志消费完成
Thread.sleep(1000);
logger.shutdown();
}
}
方案四:使用成熟日志框架
实际项目中更推荐使用已经实现好线程安全的成熟日志框架,比如Logback、Log4j2等,这些框架内部已经做好了线程安全处理,支持异步日志、日志分级、滚动归档等各种功能,不需要开发者自己重复实现底层逻辑。
以Logback为例,只需要引入对应的依赖,配置好日志文件,就可以直接在多线程环境中使用,无需担心线程安全问题:
<!-- pom.xml中引入Logback依赖 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.14</version>
</dependency>
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class FrameworkLoggerDemo {
// 获取Logger实例,每个类对应独立的Logger,框架内部保证线程安全
private static final Logger logger = LoggerFactory.getLogger(FrameworkLoggerDemo.class);
public static void main(String[] args) {
// 模拟多线程写入日志
for (int i = 0; i < 5; i++) {
final int threadId = i;
new Thread(() -> {
for (int j = 0; j < 3; j++) {
logger.info("线程{}的第{}条日志", threadId, j);
}
}).start();
}
}
}
不同方案对比
可以通过下表了解不同方案的适用场景和优缺点:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| synchronized同步锁 | 实现简单,无额外依赖 | 高并发下性能差,锁竞争严重 | 低并发、简单的单机应用 |
| ThreadLocal隔离 | 按线程隔离,无锁竞争 | 需要手动管理缓冲区刷写,可能有内存泄漏风险 | 需要按线程维度收集日志的场景 |
| 阻塞队列异步 | 不阻塞业务线程,性能较好 | 实现复杂度较高,需要处理队列满、消费者异常等情况 | 高并发、对性能要求较高的场景 |
| 成熟日志框架 | 功能完善,线程安全,无需重复开发 | 需要引入额外依赖,需要学习框架配置 | 绝大多数实际生产项目 |
总结
实现线程安全的日志记录有多种方案,简单场景可以使用同步锁快速实现,需要按线程隔离日志可以考虑ThreadLocal方案,高并发场景适合用阻塞队列实现异步日志,而实际生产项目更推荐使用Logback、Log4j2等成熟的日志框架,既保证线程安全,又能获得丰富的日志管理功能。开发者可以根据项目的具体需求选择合适的实现方式。