一、案例分析

今天这篇文章我们将通过一个车辆管理系统的案例讲解实现不可变类的思路。在这个案例中,需要对车辆的信息进行跟踪,其中车辆的位置信息的代码如下图(图1):
34140400_1622348613.png
图1

图中是一个位置信息类,包含代表坐标的变量X和Y,和用来对车辆位置信息进行更新的方法setXY,接下来我们看下实现车辆信息追踪代码(图2):
87416300_1622348613.png
图2
当车辆的位置信息发生变更的时候,我们可以调用updateLocation方法来更新车辆的位置,另外也可以通过调用getLocation方法来获取车辆的信息。

但Location类的setXY方法不是一个线程安全的方法,我们可以参考下图(图3)做一下具体分析:
41871900_1622348614.png
图3
如图3所示,一开始某辆车的位置信息为x=1.0 y=1.0,接着线程1调用updateLocation方法来更新位置信息为x = 2.0,y = 2.0 ,这时线程1只来得及更新了x的值,y的值还没有更新,好巧不巧,线程2也来读取车辆的位置信息,此时它得到的结果是 x =2.0,y = 1.0。 这可是这个车根本不曾到达过的“诗和远方”。

为了确保车辆信息的更新具备线程安全的特性,我们可以将位置信息类改造为不可变类,如果车辆的位置信息发生变化,咱们通过替换整个Location对象来实现,而不是通过setXY方法来实现。

二、如何实现一个不可变类?

那么怎么将一个类改造为不可变类呢?所谓的不可变类是指一个对象一经创建就不再改变。
在我们车辆管理系统中来说就是Location类一旦创建就不能变了,不能改变X的值,也不能改变Y的值。
说到这里就有点意思了,如果Location类中的X的值不能变,Y的值也不能变,那么我们是不是可以使用Java关键字final来修饰这连个子弹,通过Java语言特性来保证这两个子弹的不可变,如图4
93316400_1622348614.png
图4
接着还是说X和Y的值不能改变,这个时候setXY方法的存在是不是不太合理?所以需要将setXY方法也去掉。

三、继续思考:如果当前类被子类继承还是一个不可变类吗?

接着我们再思考一个问题:假设我有一个子类继承了Location,然后重写了它的getX方法怎么办?如图5:
35258300_1622348615.png
图5
如图5所示,假设有人继承Location类,然后重写getX方法。比如说我本来一个Location对象的X值为1的,但是这个子类确返回了 1 + 1 = 2。这很显然不符合不可变对象的行为,因为它的子类可以改变它的方法行为。 为了杜绝这种情况,我们需要将Location类设计为不可继承的,通过final修饰符修饰即可。

那么最终版本的不可变的Location如图6:
74338700_1622348615.png
图6
接着,如果车辆位置发生变化的时候,通过替换整个Location来表示,这样就能避免前面的问题了。
23776400_1622348616.png
图7
如图7中,如果车辆位置发生了变化,可以通过替换整个Location从而避免线程安全问题。

四、回头看看:如何将一个类改造成不可变类

最后我们总结一下实现不可变类的一些思路:

  1. 使用final关键字修饰所有成员变量,避免其被修改,也可以保证多线程环境下被final关键字修饰的变量所引用的对象的初始化安全,即final修饰的字段在其他线程可见时,必定是初始化完成的。
  2. 使用private修饰所有成员变量,可以防止子类及其他地方通过引用直接修改变量值。
  3. 禁止提供修改内部状态的公开接口(比如前面例子中的setXY方法)
  4. 禁止不可变类被外部继承,防止子类改变其定义的方法的行为
  5. 如果类中存在数组或集合,在提供外部访问之前需要做防御性复制。

JMeter 测试
1.直接测试,直接return data;。未加防御

  1. package com.example.demo.model;
  2. import org.springframework.stereotype.Component;
  3. import java.util.ArrayList;
  4. import java.util.List;
  5. /**
  6. * @version 1.0
  7. * @Description
  8. * @Date 2021/8/3 10:15
  9. * @Author wangyun
  10. */
  11. @Component
  12. public final class DefensiveReplicaDemo {
  13. private final List<Integer> data = new ArrayList<>();
  14. public DefensiveReplicaDemo(){
  15. data.add(1);
  16. data.add(2);
  17. data.add(3);
  18. }
  19. public List<Integer> getData(){
  20. return data; #重点看这里
  21. }
  22. }

10个线程,循环一次
image.png
发现数据乱了。。。

  1. 修改getData()方法,

代码如下:

  1. package com.example.demo.model;
  2. import org.springframework.stereotype.Component;
  3. import org.springframework.util.CollectionUtils;
  4. import java.util.ArrayList;
  5. import java.util.Collections;
  6. import java.util.List;
  7. /**
  8. * @version 1.0
  9. * @Description
  10. * @Date 2021/8/3 10:15
  11. * @Author wangyun
  12. */
  13. @Component
  14. public final class DefensiveReplicaDemo {
  15. private final List<Integer> data = new ArrayList<>();
  16. public DefensiveReplicaDemo(){
  17. data.add(1);
  18. data.add(2);
  19. data.add(3);
  20. }
  21. public List<Integer> getData(){
  22. return Collections.unmodifiableList(new ArrayList<>(data)); #重点看这里
  23. }
  24. }

这样程序运行之后,如果外部要给我们数组中添加内容,程序会报错。内容如下:

  1. 2021-08-03 10:27:34.807 ERROR 37604 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.UnsupportedOperationException] with root cause
  2. java.lang.UnsupportedOperationException: null
  3. at java.util.Collections$UnmodifiableCollection.add(Collections.java:1057) ~[na:1.8.0_271]
  4. at com.example.demo.controller.Test02.lockMethod1(Test02.java:32) ~[classes/:na]
  5. at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_271]
  6. at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_271]
  7. at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_271]
  8. at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_271]
  9. at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:197) ~[spring-web-5.3.6.jar:5.3.6]
  10. at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:141) ~[spring-web-5.3.6.jar:5.3.6]
  11. at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106) ~[spring-webmvc-5.3.6.jar:5.3.6]
  12. at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:894) ~[spring-webmvc-5.3.6.jar:5.3.6]
  13. at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) ~[spring-webmvc-5.3.6.jar:5.3.6]
  14. at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.3.6.jar:5.3.6]
  15. at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1060) ~[spring-webmvc-5.3.6.jar:5.3.6]
  16. at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:962) ~[spring-webmvc-5.3.6.jar:5.3.6]
  17. at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.6.jar:5.3.6]
  18. at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.3.6.jar:5.3.6]
  19. at javax.servlet.http.HttpServlet.service(HttpServlet.java:626) ~[tomcat-embed-core-9.0.45.jar:4.0.FR]
  20. at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.3.6.jar:5.3.6]
  21. at javax.servlet.http.HttpServlet.service(HttpServlet.java:733) ~[tomcat-embed-core-9.0.45.jar:4.0.FR]
  22. at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
  23. at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
  24. at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.45.jar:9.0.45]
  25. at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
  26. at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
  27. at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.6.jar:5.3.6]
  28. at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.6.jar:5.3.6]
  29. at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
  30. at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
  31. at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.3.6.jar:5.3.6]
  32. at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.6.jar:5.3.6]
  33. at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
  34. at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
  35. at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.3.6.jar:5.3.6]
  36. at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.6.jar:5.3.6]
  37. at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
  38. at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
  39. at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
  40. at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) [tomcat-embed-core-9.0.45.jar:9.0.45]
  41. at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) [tomcat-embed-core-9.0.45.jar:9.0.45]
  42. at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143) [tomcat-embed-core-9.0.45.jar:9.0.45]
  43. at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) [tomcat-embed-core-9.0.45.jar:9.0.45]
  44. at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) [tomcat-embed-core-9.0.45.jar:9.0.45]
  45. at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) [tomcat-embed-core-9.0.45.jar:9.0.45]
  46. at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374) [tomcat-embed-core-9.0.45.jar:9.0.45]
  47. at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) [tomcat-embed-core-9.0.45.jar:9.0.45]
  48. at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893) [tomcat-embed-core-9.0.45.jar:9.0.45]
  49. at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1707) [tomcat-embed-core-9.0.45.jar:9.0.45]
  50. at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.45.jar:9.0.45]
  51. at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_271]
  52. at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_271]
  53. at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.45.jar:9.0.45]
  54. at java.lang.Thread.run(Thread.java:748) [na:1.8.0_271]
  55. 2021-08-03 10:27:34.866 ERROR 37604 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.UnsupportedOperationException] with root cause

报错类为:Collections
image.png