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项目中,共享变量的线程安全性容易被忽视,加上数据不一致问题的出现具有偶发、不可预测等因素(本来想截个图的,但是应用目前并发量小,没有出现数据不一致的现象,这也是并发问题隐蔽而不易被发现的原因),为了防患于未然,在项目伊始就应该分析并发因素,让开发人员关注可变状态的线程安全性问题,是非常必要的。

时间: 2024-11-08 07:54:01

java并发-记一次统计变量值偏差问题的相关文章

Java反射-修改private final成员变量值,你知道多少?

大家都知道使用java反射可以在运行时动态改变对象的行为,甚至是private final的成员变量,但并不是所有情况下,都可以修改成员变量.今天就举几个小例子说明.  基本数据类型 String类型 Integer类型 总结 首先看下对基本类型的修改: /** * @author Cool-Coding 2018/5/15 */ public class ReflectionUsage {private final int age=18; public int getAge(){ return

Java随笔:使用异或操作交换变量值的风险

在面试中,经常会问到“如何不用中间变量交换两个变量值”. 看看下面这个代码输出是什么: int x = 1984; int y = 2001; x^=y^=x^=y; System.out.println("x="+x+";y="+y); 看上去应该很完美的:x=2001;y=1984 实际输出是:x=0;y=1984 问题出在哪里?是的,就是JVM的编译器. 看看实际的汇编: Code: 0: sipush 1984 3: istore_1 4: sipush 2

JAVA读取字符串中某个变量值,并进行简单比较

开发中有时会遇到需要截取一个字符串中某个变量后面带的值,并对这个值做简单运算的情况,比如“tom, age 23, come fron us”, 这里想判断年龄是否大于30岁,实现如下: 1 String inputstr = "tom, age 23, come fron us"; 2 String matchString = "age 变量1"; // 需要截取的变量正则表达式 3 String ruleString = "变量1 > 30&qu

java并发编程(五)正确使用volatile

转载请注明出处:     volatile用处说明     在JDK1.2之前,Java的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的.而随着JVM的成熟和优化,现在在多线程环境下volatile关键字的使用变得非常重要. 在当前的Java内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写.这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致. 要解决这个问题,就需要

Java并发编程之CAS二源码追根溯源

在上一篇文章中,我们知道了什么是CAS以及CAS的执行流程,在本篇文章中,我们将跟着源码一步一步的查看CAS最底层实现原理. 本篇是<凯哥(凯哥Java:kagejava)并发编程学习>系列之<CAS系列>教程的第二篇:从源码追根溯源查看CAS最底层是怎么实现的. 本文主要内容:CAS追根溯源,彻底找到CAS的根在哪里. 一:查看AtomicInteger.compareAndSet源码 通过上一篇文章学习,我们知道了AtomicInteger.compareAndSet方法不加锁

Java并发编程 Volatile关键字解析

volatile关键字的两层语义 一旦一个共享变量(类的成员变量.类的静态成员变量)被volatile修饰之后,那么就具备了两层语义: 1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的. 2)禁止进行指令重排序. 根据volatile的语义,我们可以看到,volatile主要针对的是并发三要素(原子性,可见性和有序性)中的后两者有实际优化作用. 可见性: 线程本身并不直接与主内存进行数据的交互,而是通过线程的工作内存来完成相应的操作.

【死磕Java并发】----深入分析CAS

CAS,Compare And Swap,即比较并交换.Doug lea大神在同步组件中大量使用CAS技术鬼斧神工地实现了Java多线程的并发操作.整个AQS同步组件.Atomic原子类操作等等都是以CAS实现的,甚至ConcurrentHashMap在1.8的版本中也调整为了CAS+Synchronized.可以说CAS是整个JUC的基石. CAS分析 在CAS中有三个参数:内存值V.旧的预期值A.要更新的值B,当且仅当内存值V的值等于旧的预期值A时才会将内存值V的值修改为B,否则什么都不干.

JAVA并发总结-基础篇

多线程 1. java中有几种方法可以实现一个线程? 继承Thread类,实现Runnable接口创建一个线程的唯一方法是实例化java.lang.Thread类(或其子类),并调用其start()方法 2. 如何停止一个正在运行的线程? 调用ThreadInstanceA.inerrupt()方法,这样当A线程在Thread的sleep,join方法,或者Object的wait方法的时候会直接抛出InerruptedException,捕捉后便可退出. public void shutdown

关于Java并发编程的总结和思考

编写优质的并发代码是一件难度极高的事情.Java语言从第一版本开始内置了对多线程的支持,这一点在当年是非常了不起的,但是当我们对并发编程有了更深刻的认识和更多的实践后,实现并发编程就有了更多的方案和更好的选择.本文是对并发编程的一点总结和思考,同时也分享了Java5以后的版本中如何编写并发代码的一点点经验. 为什么需要并发 ??并发其实是一种解耦合的策略,它帮助我们把做什么(目标)和什么时候做(时机)分开.这样做可以明显改进应用程序的吞吐量(获得更多的CPU调度时间)和结构(程序有多个部分在协同