很多时候,我们的代码,在单线程的环境下是可以运行的非常完美,然而,一旦把代码放到多线程的环境下去接受蹂躏,结果常常是惨不忍睹的。
《Java 并发编程实践》中,给出了线程安全性的解释:
A class is thread-safe when it continues to behave correctly when accessed from multiple threads.
当一个类,不断被多个线程调用,仍能表现出正确的行为时,那它就是线程安全的。
这里的关键在于对 "正确的行为" 的理解,什么意思呢?多写几个线程不安全的代码你就明白了。
假设我们需要给 Servlet 增加一个统计请求数的功能,于是我们使用了一个 long 变量作为计数器,并在每次请求时都给这个计数器加一(本文的所有代码,可到 Github 下载):
- public class UnsafeCountingServlet extends GenericServlet implements Servlet {
- private long count = 0;
- public long getCount() {
- return count;
- }
- public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException,
- IOException {++count;
- // To something else...
- }
- }
在单线程的环境下,这份代码绝对正确,然而,当有多个线程同时访问时,问题就暴露了。
关键就在于 ++count,它看上去只是一个操作,实际上包含了三个动作:
这是一个 "读取 - 修改 - 写入" 的操作序列,因此假设现在 count 是 9,然后:
显然,这个类,在多线程的环境下,没有表现出我们预期的行为,所以称它为线程不安全。
这一次,我们需要写一个单例,单例很简单呀,不就是构造函数私有化么:
- public class UnsafeSingleton {
- private UnsafeSingleton instance = null;
- public UnsafeSingleton getInstance() {
- if (instance == null)
- instance = new UnsafeSingleton();
- return instance;
- }
- }
如果只有一个线程调用我们的代码,那这个类,永远不会生出二胎。但是,放在多线程的环境下,它就可能会意外怀孕了:
预期中的计划生育失败,我们再一次写出了线程不安全的代码。
如果说前面两种破坏方式都太过明显,很难在代码 review 中逃过法眼的话,接下来这种方式,就显得非常高级了。
- public class ThisEscape {
- private final List < Event > listOfEvents;
- public ThisEscape(EventSource source) {
- source.registerListener(new EventListener() {
- public void onEvent(Event e) {
- doSomething(e);
- }
- });
- listOfEvents = new ArrayList < Event > ();
- }
- void doSomething(Event e) {
- listOfEvents.add(e);
- }
- interface EventSource {
- void registerListener(EventListener e);
- }
- interface EventListener {
- void onEvent(Event e);
- }
- interface Event {}
- }
这个类的构造函数接收了一个事件源,在构造函数中,会给事件源添加一个监听器。咋看之下,你也许不会发现这段代码有什么问题,其实这里面暗藏着 NullPointerException:
这一切的根源都在于,ThisEscape 的构造函数,在 ThisEscape 还没实例化完成之前,就把 this 对象泄漏出去,使得外部可以调用实例对象的方法,这就像还没开考,就把考题给公布出去了,因此称之为,考题泄漏。
《Java 并发编程实践》将这种误把对象发布出去的行为,称为对象逸出(Escape)。
对象逸出是指不想发布对象,却不小心发布了。还有一种是,想发布对象,却在对象还没制造好之前,就给了对方使用半成品的机会:
- public class StuffIntoPublic {
- public Holder holder;
- public void initialize() {
- holder = new Holder(42);
- }
- }
- public class Holder {
- private int n;
- public Holder(int n) {
- this.n = n;
- }
- public void assertSanity() {
- if (n != n) throw new AssertionError("This statement is false.");
- }
- }
很难想象,什么情况下 n != n 会成立,并抛出异常。大家可以先参考 StackOverflow 里的解释,主要是涉及到 Java 的指令重排,后面会给大家详细讲解。
这篇文章给大家解释了什么是线程安全,并且举了四个线程不安全的例子来加深大家对线程安全的理解:消失的请求数、意外怀孕、考题泄漏、半成品。这四个例子,分别对应三种常见的线程不安全情形:
绝大多数的线程不安全问题,都可以归结为这三种情形。而这三种情形,其实又可以再缩减为两种:对象创建时和对象创建后。不仅仅是在对象创建后的业务逻辑中要考虑线程的安全性,在对象创建的过程中,也要考虑线程安全。
这篇文章里只是解释了为什么这些代码会有线程安全问题,并没有跟大家说如何对代码进行修改,使之成为 "线程安全",我会在后面的文章中和大家一起详细探讨。
有人可能会说,线程安全嘛,加同步锁不就可以啦,其实不然,光光同步锁,就有很多可以探究的了:
更何况,解决并发问题,也绝对不是加锁这么简单,我们还需要了解:
再者,解决了线程安全,我们还需要考虑线程的生命周期管理、线程使用的性能问题等:
乃至我们学习 Java 并发编程最最初始的问题:
这些,都是我新的一年里要和大家一起分享的,分享的内容主要基于《Java 并发编程实践》里提到的知识,我买了中文版和英文版。这是一本很难啃的书,我会一如既往的用通俗易懂的语言来和大家分享我的学习心得。
来源: http://www.jianshu.com/p/f3d62bc16469