java并发-记一次统计变量值偏差问题

Posted anhaix0120

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java并发-记一次统计变量值偏差问题相关的知识,希望对你有一定的参考价值。

  1 问题描述
  
  在一个项目中,需要对发送的请求结果进行统计,开发同事定义了两个全局共享变量CommonUtil.ReqFailNum和ReqNum,分别记录请求失败数和发送的请求数。并在每次发送请求之前都假定该请求会处理失败,先对其累加,直到成功收到200的返回码后,重新修正失败数量。
  
  最后当应用处理请求处于较频繁的阶段时,出现了ReqFailNum最后减为负数的情况,一次正常请求完成时,CommonUtil.ReqFailNum ++;和CommonUtil.ReqFailNum --应该是成对出现的,这个统计值不应该为负数的。
  
  发送请求的代码如下:

private static boolean XMLPost(String content, String sendUrl) throws Exception{
    boolean bn = false;
    
    if ( null != content ) {
            //初始假设请求发送失败,等待正常返回200后再将失败记录数--
            CommonUtil.ReqFailNum ++;
            
            URL url =null;
            URLConnection con = null;
            OutputStreamWriter out = null;
            try {
                url = new URL(sendUrl);
                con = url.openConnection();
            }catch (MalformedURLException e1) {
                throw new ConnException("MalformedURLException");
            } catch (IOException e) {
                throw new ConnException("IOException");
            }
            con.setConnectTimeout(2000);
            con.setReadTimeout(2000);
            con.setDoOutput(true);
            con.setRequestProperty("Connection", "keep-alive");
            con.setRequestProperty("Pragma:", "no-cache");
            con.setRequestProperty("Cache-Control", "no-cache");
            con.setRequestProperty("Content-Type", "text/xml");
            
            try {
                out = new OutputStreamWriter(con.getOutputStream(), "UTF-8");
                out.write(content);
                out.flush();
                out.close();
            } catch (UnsupportedEncodingException e) {
                throw new ConnException("UnsupportedEncodingException");
            } catch (IOException e) {
                String exceptionStr = CommonUtil.stackTraceStr(e);
                throw new ConnException("IOException."+exceptionStr);
            }finally{
                try {
                    if(out != null){
                        out.close();
                    }
                } catch (IOException e) {
                    
                    throw new ConnException("IOException...");
                }
            }
            
            String headline = con.getHeaderField(0);
            if (headline != null && headline.indexOf("200") > -1) {
                CommonUtil.ReqFailNum --;
                CommonUtil.ReqNum ++;
                bn = true;
                logger.info("sendUrl:: return 200 ok" );
            }
    }
    return bn;
}

  2 错误原因分析
  
  统计变量在并发环境下,开发人员却忽视了其安全问题。由于该方法在Action中调用,客户端的每个请求,都会调用该方法。而Web服务器处理客户端的请求时,对每个请求都创建了一个线程去处理。这段对统计变量操作的代码,曝露在多线程环境下,却没有任何同步处理,很容易导致统计数据的不一致问题。
  
  在这个应用中,ReqFailNum++这个操作实际上应该是一个原子操作,它包含了对内存的三个动作“读-修改-写”,并且结果状态依赖于之前的状态。上述代码,在没有同步的情况下,当两个线程同时执行这行代码时,可能读到的是同一个值,同时+1 ,最终应该是两次累计操作,结果只累加了一次,由于丢失了一次递增操作,最终的统计值就偏差了1。
  
  由于++代码是方法最初的几行,线程同时执行++操作的概率较大,而CommonUtil.ReqFailNum --;是在请求成功处理完成后执行的,这段时间涉及到网络请求,处理时间不确定性较大,所以- -操作同时执行的概率也较低。最终ReqFailNum++丢失的次数会多于ReqFailNum--丢失的次数,从而导致这个共享变量ReqFailNum的值成了负数。
  
  3 解决办法
  
  1)使用锁,将ReqFailNum++或--的操作放在同步代码块中
  
  2)由于是简单的统计变量,可以利用原子变量的特性,使用AtomicInteger或AtomicLong
  
  结论:Web项目中,共享变量的线程安全性容易被忽视,加上数据不一致问题的出现具有偶发、不可预测等因素(本来想截个图的,但是应用目前并发量小,没有出现数据不一致的现象,这也是并发问题隐蔽而不易被发现的原因),为了防患于未然,在项目伊始就应该分析并发因素,让开发人员关注可变状态的线程安全性问题,是非常必要的。

以上是关于java并发-记一次统计变量值偏差问题的主要内容,如果未能解决你的问题,请参考以下文章

如何为每个片段保存一个变量值以重用它? [关闭]

记一次PATH环境变量设置不生效的问题

从活动中更改片段的变量值

Java并发编程(05):悲观锁和乐观锁机制

记一次不太聪明的微服务优化方案

记一次用Jmeter测试并发的步骤