Java加密服务提供者:使用池还是不使用池?

11

解决方案

  • MessageDigest => 根据需要创建新的实例
  • KeyFactory => 使用单个共享实例
  • SecureRandom => 使用 StackObjectPool
  • Cipher => 使用 StackObjectPool

问题

在编写安全框架时,我经常面临一个困境:"使用池还是不使用池"

基本上,这个问题可以分为两个“组”:

  1. 第一组:使用 SecureRandom,因为调用 nextBytes(...) 是同步的,可能会成为 WebApp/多线程应用程序的瓶颈。

  2. 第二组:加密服务提供者,例如 MessageDigestSignatureCipherKeyFactory 等(因为 getInstance() 的成本较高?)

你有什么看法?

你在这类问题上的习惯是什么?

编辑 09/07/2013

我最终花时间亲自测试了 @Qwerky 的 Share 类,发现结果相当...出乎意料。

该类缺少我最关心的内容:像 GenericObjectPoolStackObjectPool 这样的池。

因此,我重新设计了这个类来测试所有4种备选方案:

  • 使用同步的单个共享实例gist
  • 在每个循环内部创建新实例(我不关心您可以将摘要创建从循环外提取的情况)gist
  • GenericObjectPool:gist
  • StackObjectPool:gist

由于使用池时1M次循环需要太长时间,我将循环次数降低到100000。

我还在每个循环的末尾添加了Thread.yield()以使负载更好地分配。

结果(累积运行时间):

  • MessageDigest
    • 新实例:420秒
    • 单个实例:550秒
    • StackObjectPool:800秒
    • GenericObjectPool:1900秒
  • KeyFactory
    • 新实例:400秒
    • 单个实例:350秒
    • StackObjectPool:2900秒
    • GenericObjectPool:3500秒
  • SecureRandom
    • StackObjectPool:1600秒
    • 新实例:2300秒
    • GenericObjectPool:2300秒
    • 单个实例:2800秒
  • Cipher
    • StackObjectPool:2800秒
    • GenericObjectPool:3500秒
    • 单个实例:5100秒
    • 新实例:8000秒

结论

对于MessageDigest和KeyFactory,池是性能杀手,甚至比具有同步瓶颈的单个实例更糟糕,而在SecureRandom和Cipher方面,它们确实非常有用。


重用取决于昂贵创建和同步访问的开销之间的权衡。只有您可以选择,这取决于您的使用配置文件。请注意,两个组之间没有区别,如果在线程之间共享实例,则必须同步访问MessageDigest等。 - Qwerky
当然,@Qwerky任何选项都需要线程/使用块之间的同步。问题在于你如何处理这些类在你的应用程序中? - Cerber
1
这太棒了---我希望像这样的信息对于各种课程更容易获取。实际上,我希望它能成为标准Javadoc的一部分。 - Robert Tupelo-Schneck
2个回答

6
如果您让100个线程访问一个共享的 MessageDigest 并让它们各自计算一百万个哈希值,那么在我的机器上,第一个线程完成需要70,160毫秒,而最后一个线程完成需要98,748毫秒。
如果这些线程每次都创建一个新的 MessageDigest 实例,那么第一个线程完成需要43,392毫秒,而最后一个线程需要58,691毫秒。 编辑:
实际上,在这个例子里,只有两个线程时创建新实例的例子会更快。
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Share {

  final byte[] bytes = new byte[100];
  final MessageDigest sharedDigest;
  final ExecutorService pool;
  int threads = 100;

  Share() throws NoSuchAlgorithmException {
    sharedDigest = MessageDigest.getInstance("MD5");
    pool = Executors.newFixedThreadPool(threads);
  }

  void go() {

    for (int i=0; i<threads; i++) {
      pool.execute(new Runnable() {
        public void run() {
          long start = System.currentTimeMillis();
          for (int i=0; i<1000000; i++) {
            /*
            synchronized (sharedDigest) {
              sharedDigest.reset();
              sharedDigest.update(bytes);
              sharedDigest.digest();
            }*/
            try {
              MessageDigest digest = MessageDigest.getInstance("MD5");
              digest.reset();
              digest.update(bytes);
              digest.digest();
            } catch (Exception ex) {
              ex.printStackTrace();
            }
          }
          long end = System.currentTimeMillis();
          System.out.println(end-start);
          pool.shutdown();
        }
      });
    }

  }

  public static void main(String[] args) throws Exception {
    Share share = new Share();
    share.go();
  }

}

-1

这个测试似乎有利于缓存

long t0 = System.currentTimeMillis();
byte[] bytes = new byte[100];
MessageDigest md = MessageDigest.getInstance("MD5");
for(int i = 0; i < 1000000; i++) {
    //MessageDigest md = MessageDigest.getInstance("MD5");
    md.reset();
    md.update(bytes);
    md.digest();
}
System.out.println(System.currentTimeMillis() - t0);

当 md 在循环外部时,它打印出 579,当在循环内部时,打印出 953。


只显示了 MessageDigest 的创建开销。在单线程情况下,重用总是更快的。 - Qwerky

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接