编写优质的并发代码是一件难度极高的事情。Java语言从第一版本开始内置了对多线程的支持,这一点在当年是非常了不起的,但是当我们对并发编程有了更深刻的认识和更多的实践后,实现并发编程就有了更多的方案和更好的选择。本文是对并发编程的一点总结和思考,同时也分享了Java 5以后的版本中如何编写并发代码的一点点经验。
为什么需要并发
并发其实是一种解耦合的策略,它帮助我们把做什么(目标)和什么时候做(时机)分开。这样做可以明显改进应用程序的吞吐量(获得更多的CPU调度时间)和结构(程序有多个部分在协同工作)。做过Java Web开发的人都知道,Java Web中的Servlet程序在Servlet容器的支持下采用单实例多线程的工作模式,Servlet容器帮助你处理了并发请求的问题。
误解和正解
最常见的对并发编程的误解有以下这些:
A. 并发总能改进性能。(真相:并发在CPU有很多空闲时间时能明显改进程序的性能,但当线程数量较多的时候,线程间频繁的调度切换反而会让系统的性能下降)
B. 编写并发程序无需修改原有的设计。(真相:目的与时机的解耦往往会对系统结构产生巨大的影响)
C. 在使用Web或EJB容器时不用关注并发问题。(真相:只有了解了容器在做什么,才能更好的使用容器)
下面的这些说法是对并发编程比较客观的认识:
A. 编写并发程序会在代码上增加额外的开销。
B. 正确的并发是非常复杂的,即使对于很简单的问题。
C. 并发中的缺陷因为不易重现也不容易被发现。
D. 并发往往需要对设计策略从根本上进行修改
并发编程的原则和技巧
- 单一职责原则:分离并发相关代码和其他代码(并发相关代码有自己的开发、修改和调优生命周期)。
- 限制数据作用域:两个线程修改共享对象的同一字段时可能会相互干扰,导致不可预期的行为,解决方案之一是构造临界区,但是必须限制临界区的数量。
- 使用数据副本:数据副本是避免共享数据的好方法,复制出来的对象只是以只读的方式对待。Java 5的java.util.concurrent包中增加一个名为CopyOnWriteArrayList的类,它是List接口的子类型,所以你可以认为它是ArrayList的线程安全的版本,它使用了写时复制的方式创建数据副本进行操作来避免对共享数据并发访问而引发的问题。
- 线程应尽可能独立:让线程存在于自己的世界中,不与其他线程共享数据。有过Java Web开发经验的人都知道,Servlet就是以单实例多线程的方式工作,和每个请求相关的数据都是通过Servlet子类的service方法(或者是doGet或doPost方法)的参数传入的。只要Servlet中的代码只使用局部变量,Servlet就不会导致同步问题。Spring MVC的控制器也是这么做的,从请求中获得的对象都是以方法的参数传入而不是作为类的成员,很明显Struts2的做法就正好相反,因此Struts 2中作为控制器的Action类都是每个请求对应一个实例。
测试并发代码
对并发代码的测试也是非常棘手的事情,棘手到无需说明大家也很清楚的程度,所以这里我们只是探讨一下如何解决这个棘手的问题。我们建议大家编写一些能够发现问题的测试并经常性的在不同的配置和不同的负载下运行这些测试。不要忽略掉任何一次失败的测试,线程代码中的缺陷可能在上万次测试中仅仅出现一次。具体来说有这么几个注意事项:
- 不要将系统的失效归结于偶发事件,就像拉不出屎的时候不能怪地球没有引力。
- 先让非并发代码工作起来,不要试图同时找到并发和非并发代码中的缺陷。
- 编写可以在不同配置环境下运行的线程代码。
- 编写容易调整的线程代码,这样可以调整线程使性能达到最优。
- 让线程的数量多于CPU或CPU核心的数量,这样CPU调度切换过程中潜在的问题才会暴露出来。
- 让并发代码在不同的平台上运行。
- 通过自动化或者硬编码的方式向并发代码中加入一些辅助测试的代码。