💥 Java 中使用 MessageDigest 的坑:线程不安全导致 MD5 值不一致
在日常开发中,MD5 作为一种常见的哈希算法,经常用于加密、签名或唯一标识生成等场景。很多人会封装一个工具类来复用 MD5 逻辑,但如果没注意到 MessageDigest 不是线程安全的,就很容易在多线程环境下踩坑。
本文就以一个实际案例出发,说明这个问题产生的原因、如何验证、以及最简洁安全的修复方案。
🧪 问题复现:封装 MD5Util 工具类
我们封装了一个工具类,用于对字符串进行多次 MD5 加密:
java
public class MD5Util {
protected static char hexDigits[] = "0123456789abcdef".toCharArray();
protected static MessageDigest digest = null;
static {
try {
digest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
public static String getMD5StringTimes(String str, int count) {
if (str == null) return null;
count = count <= 0 ? 1 : count;
String temp = str;
for (int i = 0; i < count; i++) {
temp = getMD5String(temp.getBytes());
}
return temp;
}
private static String getMD5String(byte[] bytes) {
digest.update(bytes); // 非线程安全
return byteArrayToHexString(digest.digest());
}
private static String byteArrayToHexString(byte[] b) {
StringBuilder sb = new StringBuilder();
for (byte value : b) {
sb.append(String.format("%02x", value));
}
return sb.toString();
}
}⚠️ 问题说明
MessageDigest 实例被声明为静态变量 digest,在多个线程中共享。
但是!MessageDigest 是非线程安全的,多线程并发调用时,update() 和 digest() 会互相干扰,导致输出错误或不一致。
✅ 验证线程安全问题
我们写一段代码来验证:
java
import java.util.concurrent.ConcurrentSkipListSet;
public class MD5Test {
public static void main(String[] args) throws InterruptedException {
final String input = "HelloWorld";
final int count = 10;
final int threads = 100;
ConcurrentSkipListSet<String> resultSet = new ConcurrentSkipListSet<>();
Thread[] ts = new Thread[threads];
for (int i = 0; i < threads; i++) {
ts[i] = new Thread(() -> {
String md5 = MD5Util.getMD5StringTimes(input, count);
resultSet.add(md5);
});
}
for (Thread t : ts) t.start();
for (Thread t : ts) t.join();
System.out.println("结果个数:" + resultSet.size());
resultSet.forEach(System.out::println);
}
}若 MD5 算法线程安全,所有输出应该一致,结果数量应为 1。
实际运行发现:结果数量常常 > 1,说明多线程冲突确实存在。
🛠 最简单的修复方式
我们不改动整体结构,仅将 MessageDigest 改为 局部变量(每次调用新建实例)即可:
java
private static String getMD5String(byte[] bytes) {
try {
MessageDigest digest = MessageDigest.getInstance("MD5"); // 局部变量
digest.update(bytes);
return byteArrayToHexString(digest.digest());
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}这样即可保证线程安全,且修改量极小。
✅ 最终版本 MD5Util(线程安全)
java
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class MD5Util {
protected static char[] hexDigits = "0123456789abcdef".toCharArray();
public static String getMD5StringTimes(String str, int count) {
if (str == null) return null;
count = count <= 0 ? 1 : count;
String temp = str;
for (int i = 0; i < count; i++) {
temp = getMD5String(temp.getBytes());
}
return temp;
}
private static String getMD5String(byte[] bytes) {
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(bytes);
return byteArrayToHexString(digest.digest());
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
private static String byteArrayToHexString(byte[] b) {
StringBuilder sb = new StringBuilder();
for (byte value : b) {
sb.append(String.format("%02x", value));
}
return sb.toString();
}
}📌 总结
| 问题 | 原因 |
|---|---|
| 多线程时 MD5 值不一致 | 静态共享 MessageDigest 实例,非线程安全 |
| 最简修复方案 | 将 MessageDigest 改为局部变量,每次新建实例 |
👀 延伸阅读
如果你对性能要求更高(避免频繁创建对象),可使用
ThreadLocal<MessageDigest>进行线程隔离。也可以用 Apache Commons Codec 提供的
DigestUtils.md5Hex()简化实现。
希望这篇文章能帮你避坑、写出更健壮的加密工具类。欢迎点赞、收藏、关注我!
