? ? ? ? 最近在学习Java并发编程方面的知识,我的学习的路线是:先学习了JVM,然后再学的JUC。学习了JVM后,我开始看《Java并发编程实战》作者Brian Goetz ,童云兰翻译的那本,在看书的过程中,书中给出了一些重点突出的辅助示例代码(但是这些代码不是完整的,本来想按照书中提供的完整代码地址 http://www.javaconcurrencyinpractice.com去下载完整的代码示例,奈何梯子到了,打不开网站)于是就自己来实现书中的一些代码示例。
? ? ? ? 其中JVM的学习,推荐的数据《深入理解Java虚拟机》(周志明,第三版),推荐的课程是尚硅谷的JVM课程(尚硅谷的课程讲解的比较细致,全面但是学习周期较长),传智播客-黑马的JVM教程(黑马的课程讲解的都是干货,没有那么多前情提要的背景知识铺垫,建议有2年以上工作经验,并且之前对JVM多少有些了解的同学进行学习)。
在没有同步的情况下统计已处理请求数量的Servlet组件(大白话就是:客户端并发请求服务端这同一个Servlet组件,在这个Servlet组件里面需要有一个计数器统计其被请求的次数,也即有多少个线程访问过这个Servlet)
import lombok.extern.slf4j.Slf4j;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @DESCRIPTION:
* @USER: shg
* @DATE: 2024/1/13 8:40
*/
@Slf4j
public class UnsafeCountingFactorizer extends HttpServlet {
// 线程共享变量
private long count = 0;
public long getCount() {
return count;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
++count;
// log.info("线程:{},count的值:{}", Thread.currentThread().getName(),getCount());
encodeIntoResponse(resp, factors);
}
private BigInteger extractFromRequest(HttpServletRequest req) {
return new BigInteger(req.getParameter("key"));
}
private BigInteger[] factor(BigInteger number) {
List<BigInteger> factors = new ArrayList<>();
BigInteger divisor = BigInteger.valueOf(2);
while (number.compareTo(BigInteger.ONE) > 0) {
if (number.mod(divisor).equals(BigInteger.ZERO)) {
number = number.divide(divisor);
factors.add(divisor);
} else {
divisor = divisor.add(BigInteger.ONE);
}
}
return factors.toArray(new BigInteger[0]);
}
private void encodeIntoResponse(HttpServletResponse resp, BigInteger[] factors) throws IOException {
resp.setContentType("text/html;charset=utf-8");
PrintWriter writer = resp.getWriter();
writer.println("<html><body>");
writer.println("<h1>" + Arrays.asList(factors) +"count的值:"+getCount()+ "</h1>");
writer.println("</body></html>");
}
}
? ? ? ? 上述这个?UnsafeCountingFactorizer 并非线程安全的,那么怎么证明这个线程是不安全的呢,接下来我们可以通过 Jmeter压测工具,使用10个线程,运行20秒来看看结果:
?
????????从统计结果可以看到10个线程并发访问?UnsafeCountingFactorizer 这个Servlet组件,一共发起了1411554个请求,那么计数器的统计数量应该就是 1411554,下面我们看看最后一次发起请求计数器返回的结果:如上图,可以看到最后一次请求返回的计数器值为1401554,少统计了1万次。
? ? ? ? 改进上面的代码,使用线程安全的原子变量类AtomicLong作为计数器来统计请求次数,diamond如下:
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
/**
* @DESCRIPTION:
* @USER: shg
* @DATE: 2024/1/13 9:40
*/
public class CountingFactorizer extends HttpServlet {
private final AtomicLong count = new AtomicLong();
public long getCount(){
return count.get();
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
count.incrementAndGet();
// log.info("线程:{},count的值:{}", Thread.currentThread().getName(),getCount());
encodeIntoResponse(resp, factors);
}
private BigInteger extractFromRequest(HttpServletRequest req) {
return new BigInteger(req.getParameter("key"));
}
private BigInteger[] factor(BigInteger number) {
List<BigInteger> factors = new ArrayList<>();
BigInteger divisor = BigInteger.valueOf(2);
while (number.compareTo(BigInteger.ONE) > 0) {
if (number.mod(divisor).equals(BigInteger.ZERO)) {
number = number.divide(divisor);
factors.add(divisor);
} else {
divisor = divisor.add(BigInteger.ONE);
}
}
return factors.toArray(new BigInteger[0]);
}
private void encodeIntoResponse(HttpServletResponse resp, BigInteger[] factors) throws IOException {
resp.setContentType("text/html;charset=utf-8");
PrintWriter writer = resp.getWriter();
writer.println("<html><body>");
writer.println("<h1>" + Arrays.asList(factors) +"count的值:"+getCount()+ "</h1>");
writer.println("</body></html>");
}
}
?仍然使用Jmeter进行压测,如下:
????????从统计结果可以看到10个线程并发访问?CountingFactorizer 这个Servlet组件,一共发起了1472639个请求,那么计数器的统计数量应该就是 1472639,下面我们看看最后一次发起请求计数器返回的结果:如下图,可以看到最后一次请求返回的计数器值为1472639,是正确的结果。
? ? ? ? (摘自书中的原话)在基于Web的服务中,命中计数器值的少量偏差或许是可以接受的,在某些情况下也确实如此。但如果该计数器被用来生产数值序列或者唯一的对象标识符,那么在多次调用中返回相同的值将导致严重的数据完整性问题。这种由于不恰当的执行时许而出现的不正确的结果是一种非常重要的情况,它有一个正式的名字:竞态条件(Race Condition)。