目录
在《java并发编程实战》中提到同步方法在并发量大的情况下效率很低,但是为了保证程序的安全性,采用同步锁又是不得不考虑的问题,那有没有办法做到安全性和高效率同时实现呢?书中提到了可以用同步代码块取代对整个方法的同步,仅对方法中操作共享变量的代码加同步锁,这样就能使没有获取到锁的线程能执行同步代码块之前的代码,进而提高程序性能。
1 代码介绍
这里采用一个因式分解的例子,用户请求传入一个int数值,程序进行因式分解,然后将结果存储在响应头中返回。为了对程序进行优化,会对上一次因式分解的结果存储在全局变量中,若传入的待分解值等于上次分解的值,则直接返回结果。
1.1 全局变量
上一次分解的值和分解结果。
private int lastNumber;
private List<Integer> lastFactors;
1.2 因式分解代码
private List<Integer> factor(int num) {
List<Integer> factors = new ArrayList<>();
for (int i = 2; i <= num; i++) {
while (num % i == 0) {
factors.add(i);
num = num / i;
}
}
return factors;
}
1.3 细粒度同步代码(同步代码块)
仅对共享变量的操作进行了同步。
@RequestMapping("/performanceTest")
public synchronized void performanceTest(HttpServletResponse response, int number){
List<Integer> factors = null;
synchronized (this){
if (number == lastNumber){
factors = lastFactors;
}
}
if (factors == null){
factors = factor(number);
synchronized (this){
lastNumber = number;
lastFactors = factors;
}
}
response.setHeader("result",factors.toString());
}
1.4 粗粒度同步代码(同步方法)
对整个方法进行同步。
@RequestMapping("/performanceTest2")
public synchronized void performanceTest2(HttpServletResponse response, int number){
List<Integer> factors = null;
if (number == lastNumber){
factors = lastFactors;
}
if (factors == null){
factors = factor(number);
lastNumber = number;
lastFactors = factors;
}
response.setHeader("result",factors.toString());
}
2 性能测试
此次测试采用apache的Jmeter框架,使用http的post请求进行测试,测试的结果通过汇总报告来进行展示,汇总报告的相关参数介绍见:汇总报告。设置访问线程数为3000个,这3000个线程被设置在1秒内全部启动。这个过程会循环3次,意味着此次测试会在3秒内启动9000个线程。
2.1 细粒度测试(同步代码块)
9000个线程的请求和响应总共花费了10秒钟,所有请求响应时长的平均值为821毫秒,最大值3313毫秒,最小值9毫秒。每秒能处理的请求个数(吞吐量)为912.3个,异常率为11.93%。
注:有些人可能看到平均响应时间为821毫秒,觉得9000*821那肯定是大于总执行时间10秒的啊,是不是出了什么问题?其实不然,当一个线程获取到了锁,之后所有产生的线程都必须等待,而线程是并行产生的,所有线程从发出请求到获得响应的时间的总和是绝对要大于10秒的。
2.2 粗粒度测试(同步方法)
9000个线程的请求和响应总共花费了12秒钟,所有请求响应时长的平均值为2168毫秒,最大值9803毫秒,最小值8毫秒。每秒能处理的请求个数(吞吐量)为712.0个,异常率为14.70%。
3 总结
从上面可以看出,对一个方法的同步机制进行小小的改动,在1秒并发3000的情况下同步代码块相比同步方法:
- 平均响应时间:后者是前者的2.6倍。
- 吞吐量(每秒处理的请求数):前者是后者的1.2倍。