Skip to content

💥 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() 简化实现。


希望这篇文章能帮你避坑、写出更健壮的加密工具类。欢迎点赞、收藏、关注我!