Java全局变量在并行时引起的问题

  最近刚完成了一个云服务平台的开发工作,系统采用Spring架构,其中测试使用的是TestNG,可以利用注解的方式,开启多线程,并且开启多个测试任务。其中遇见许多问题,闲暇之余记录下来以避免在后续项目中再犯同样的错误。
  这是项目中的一段单元测试代码,使用了MockMvc与TestNG相结合。
  好处:

  • 项目不用启动服务器就可以对SpringMVC进行测试。
  • 可以任意的开启线程与多个任务。
    @WebAppConfiguration(value = "src/main/webapp")
    @ContextConfiguration(locations = {"classpath:spring-*xml"})
    public class DeviceManageTest extends AbstractTestNGSpringContextTests {

        @Autowired
        private WebApplicationContext wac;
        private MockMvc mockMvc;

        // 全局的打开设备的句柄
        String deviceHandle = null;

        // 开启10个线程,总共执行100次的执行任务
        @Test(threadPoolSize = 10, invocationCount = 100)//priority 可以控制执行的顺序
        public void test_hash() throws Exception {

            mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();

            //openDevice
            String responseStr =     
            mockMvc.perform(get("/xxxxxx").contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk())
                    //.andDo(print())
                    .andReturn().getResponse().getContentAsString();//将相应的数据转化为字符串
            RESTResult restResult = parseObject(responseStr, RESTResult.class);

            ...

            System.out.println(Thread.currentThread().getId() + "打开xxxx");//打印线程号
            // 去出来所打开设备的句柄
            deviceHandle = (String) getKeyValue(restResult, "phDeviceHandle");
            Assert.notNull(deviceHandle, "xxxx不能为Null");

        }

  以上这段代码在访问量不构成并发时不会出现什么问题。 但当一个请求还未完成,另一个请求已经开始执行的情况下就会出现问题(并发): 第二个请求执行执行test_hash()方法会将第一个请求的deviceHandle 变量设置为null或它本身的值,这样数据就被篡改了。

  这样写的目的是因为deviceHandle变量需要在多个方法中使用,而且变量很多,但又不想通过方法参数的方式来传递,故使用成员变量。

  先看看为什么会出现这种情况。 由于系统采用springmvc框架,springmvc核心控制器DispatcherServlet 默认为每个controller生成单一实例来处理所有用户请求,所以在这个单一实例的controller中,它的XXXService也是一个实例处理所有请求, 这样XXXService的成员变量就被所有请求共享。这样就会出现并发请求时变量内容被篡改的问题。

那么出现这种问题如何解决呢?
  第一种方式: 既然是全局变量惹的祸,那就将全局变量都编程局部变量,通过方法参数来传递。
  第二种方式: jdk提供了java.lang.ThreadLocal,它为多线程并发提供了新思路。 (当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本)
  那么在什么地方使用ThreadLocal呢? 什么变量是请求公用的就将该变量托付给ThreadLocal来管理其线程副本, 所以我们在xxxService中使用它。
  XXXService.java代码:

    @WebAppConfiguration(value = "src/main/webapp")
    @ContextConfiguration(locations = {"classpath:spring-*xml"})
    public class DeviceManageTest extends AbstractTestNGSpringContextTests {


        @Autowired
        private WebApplicationContext wac;
        private MockMvc mockMvc;

        // 全局设备的句柄
        private ThreadLocal deviceHandle = new ThreadLocal();

        @Test(threadPoolSize = 10, invocationCount = 100)//priority 可以控制执行的顺序
        public void test_deviceManage() throws Exception {

            mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();

            //xxxxxx
            String responseStr = mockMvc.perform(get("/xxxxxx").contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk())
                    //.andDo(print())
                    .andReturn().getResponse().getContentAsString();//将相应的数据转化为字符串
            RESTResult restResult = parseObject(responseStr, RESTResult.class);
            if (restResult.getResultCode() != 0) {
                System.err.println("xxxxxx出错-->" + restResult.getResultCode());
                throw new RuntimeException("xxxxxx出错-->" + restResult.getResultCode());
            }
            System.out.println(Thread.currentThread().getId() + "xxxxxxx成功");
            // 去出来所xxxxxx的句柄
            String deviceHandleStr= (String) getKeyValue(restResult, "phDeviceHandle");
            this.deviceHandle.set(deviceHandleStr);
            Assert.notNull(deviceHandle, "设备句柄不能为Null");

            System.out.println("xxxxxx的句柄为-->" + deviceHandle);

            /**********************************打印线程号******************************************/
            System.out.println("线程号:" + Thread.currentThread().getId());
            System.out.println(".........");
            /**********************************End打印线程号********×******************************/

  此类并发篡改数据的问题,可以在开发工具中设置断点调试的方式来模拟并发。即第一次请求运行到断点时,查看deviceHandle 内容,并且不让程序继续往下运行,同时再发起一个请求,查看deviceHandle 内容。 如内容是第一次请求的内容,并且让第一个请求跑完后,第二个请求到断线处的deviceHandle 正确时,可以确定不会出现并发问题。

  如果有spring配置文件那就直接加上scope=”prototype”就行啦

你可能感兴趣的:(Java)