如何测试一个方法是否是线程安全的?(通过之后的研究发现第三方jar包 GroboUtil5可以更好的完成此任务, 参考:使用GroboUtils多线程并发请求测试springmvc controller)
准备一个方法
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
/**
-
Created by Administrator on 2017/6/16.
*/
public class SignatureUtil {/**
- 签名生成方法
- @param parameters
- @param key
- @return
*/
public static String getSignature(SortedMap<Object, Object> parameters, String key) {
StringBuffer sb = new StringBuffer();
Set<Map.Entry<Object, Object>> es = parameters.entrySet();
Iterator<Map.Entry<Object, Object>> it = es.iterator();
while (it.hasNext()) {
Map.Entry<Object, Object> entry = it.next();
String k = (String) entry.getKey();
Object v = entry.getValue();
if (null != v && “”.equals(v)) {
sb.append(k + “=” + v + “&”);
}
}
sb.append(“key=” + key);
String sign = MD5Util.MD5ForString(sb.toString()).toUpperCase();
//String sign = MD5Util_static.MD5ForString(sb.toString()).toUpperCase();
return sign;
}
}
当MD5Util.MD5ForString方法为如下时:
import sun.misc.BASE64Encoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
-
Created by Administrator on 2017/6/16.
*/
public class MD5Util {protected static MessageDigest messagedigest = null;
static {
try {
messagedigest = MessageDigest.getInstance(“MD5”);
} catch (NoSuchAlgorithmException nsaex) {
nsaex.printStackTrace();
}
}public static String MD5ForString(String str) {
try {
BASE64Encoder base64en = new BASE64Encoder();
//加密后的字符串
String value = base64en.encode(messagedigest.digest(str.getBytes(“utf-8”)));
return value;
} catch (Exception var4) {
var4.printStackTrace();
return null;
}
}
}
测试类:
@Test public void testGetSignature() throws Exception { //在线程可以通过await()之前必须调用countDown()的次数 //具有计数1的 CountdownLatch 可以用作”启动大门”,来立即启动一组线程; final CountDownLatch begin = new CountDownLatch(1); //为0时开始执行 final ExecutorService exec = Executors.newFixedThreadPool(100); for (int i = 0; i < 100; i++) { final int NO = i + 1; Runnable runnable = new Runnable() { public void run() { try { //如果当前计数为零,则此方法立即返回。 //如果当前计数大于零,则当前线程将被禁用以进行线程调度,并处于休眠状态,直至发生两件事情之一: //或调用countDown()方法,计数达到零; //或一些其他线程中断当前线程。
//等待直到 CountDownLatch减到1 begin.await(); SortedMap<Object, Object> sortedMap = new TreeMap<Object,Object>(); sortedMap.put("appid", "5412916052207510000052159"); sortedMap.put("mch_id", "dhfkjadh"); sortedMap.put("nonce_str", "fasjkldfh"); String sign = SignatureUtil.getSignature(sortedMap, "fhldhfkajhdfkjajsdh"); System.out.println("sign" + String.valueOf(NO) + ": " + sign); } catch (InterruptedException e) { e.printStackTrace(); } } }; exec.submit(runnable); } System.out.println("开始执行"); //减少锁存器的计数,如果计数达到零,释放所有等待的线程。 // begin减一,开始并发执行 begin.countDown(); //此方法不等待先前提交的任务完成执行 //exec.shutdown(); //为了保证先前提交的任务完成执行 使用此方法 exec.awaitTermination(4000, TimeUnit.MILLISECONDS); }
测试结果:
生成的方法签名存在不同
当给MD5Util.MD5ForString方法加上synchronized后测试结果正确,方法签名相同。原因在于
消息摘要的类 MessageDigest 为static,也就是每次方法调用都使用的是同一个对象,而这个类是非线程安全的(参考)
The MessageDigest classes are NOT thread safe. If they’re going to be used by different threads, just create a new one, instead of trying to reuse them.
于是修改MD5Util.MD5ForString方法
import sun.misc.BASE64Encoder;
import java.security.MessageDigest;
public class MD5Util {
public static String MD5ForString(String str) {
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
BASE64Encoder base64en = new BASE64Encoder();
//加密后的字符串
String value = base64en.encode(md5.digest(str.getBytes("utf-8")));
return value;
} catch (Exception var4) {
var4.printStackTrace();
return null;
}
}
public static String MD5ForString(String str, int times) {
for (int j = 0; j < times; j++) {
str = MD5ForString(str);
}
return str;
}
}
测试结果正确,方法签名相同。
总结:
这个问题有一些解决方法,最容易想到的就是我们常用的 synchronized 同步块,在资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized是很合适的,当同步非常激烈的时候,synchronized的性能则会急剧下降。采用API建议的方法,在 MD5Util 中每个使用
MessageDigest 的方法里都创建一个新的对象解决可能更合适
参考:Java模拟并发请求