如何写出线程不安全的代码

什么是线程安全性

很多时候,我们的代码,在单线程的环境下是可以运行的非常完美,然而,一旦把代码放到多线程的环境下去接受蹂躏,结果常常是惨不忍睹的。

《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,它看上去只是一个操作,实际上包含了三个动作:

  1. 读取count
  2. 将count加一
  3. 将count的值到内存中

这是一个“读取-修改-写入”的操作序列,因此假设现在count是9,然后:

  1. 线程A进入service方法,读到count值是9
  2. 在A修改完count的值但是还没写入内存之前,线程B也进入service方法,并且读取了count值,这时候线程B读取到的count还是9
  3. 最后,两个线程都对值为9的count,进行了加一的操作,两次请求下来,计数器只增加了一次。

显然,这个类,在多线程的环境下,没有表现出我们预期的行为,所以称它为线程不安全

意外怀孕

这一次,我们需要写一个单例,单例很简单呀,不就是构造函数私有化么:

public class UnsafeSingleton {
    private static UnsafeSingleton instance = null;

    private UnsafeSingleton() {

    }

    public static UnsafeSingleton getInstance() {
        if (instance == null)
            instance = new UnsafeSingleton();
        return instance;
    }
}

如果只有一个线程调用我们的代码,那这个类,永远不会生出二胎。但是,放在多线程的环境下,它就可能会意外怀孕了:

  1. 线程A调用getInstance方法,这时候instance是null,进入if代码块
  2. 在线程A执行new UnsafeSingleton()之前,线程B先跨一步,执行if判断,这时候instance还是null,嗯,线程B也进去了
  3. 接下来,两个线程都会执行new UnsafeSingleton()...悲剧就这样发生了

预期中的计划生育失败,我们再一次写出了线程不安全的代码。

考题泄漏

如果说前面两种破坏方式都太过明显,很难在代码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:

  1. 线程A将事件源传入构造函数,并且执行了registerListener的代码
  2. 在线程A给listOfEvents初始化之前,线程B触发了事件源,由于线程A已经往事件源注册了监听器,因此会执行onEvent函数,也就是doSomething(e);
  3. 而此时listOfEvents还没被初始化,因此listOfEvents.add(e)报空指针异常

这一切的根源都在于,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的指令重排,后面会给大家详细讲解。

小结

这篇文章给大家解释了什么是线程安全,并且举了四个线程不安全的例子来加深大家对线程安全的理解:消失的请求数、意外怀孕、考题泄漏、半成品。这四个例子,分别对应三种常见的线程不安全情形:

  1. 读取-修改-写入: 对应上面“消失的请求数”的例子
  2. 先检查后执行:对应上面“意外怀孕”的例子
  3. 发布未完整构造的对象:对应上面“考题泄漏”和“半成品”两个例子

绝大多数的线程不安全问题,都可以归结为这三种情形。而这三种情形,其实又可以再缩减为两种:对象创建时对象创建后不仅仅是在对象创建后的业务逻辑中要考虑线程的安全性,在对象创建的过程中,也要考虑线程安全

后记

这篇文章里只是解释了为什么这些代码会有线程安全问题,并没有跟大家说如何对代码进行修改,使之成为“线程安全”,我会在后面的文章中和大家一起详细探讨。

有人可能会说,线程安全嘛,加同步锁不就可以啦,其实不然,光光同步锁,就有很多可以探究的了:

  1. 同步锁的原理是什么
  2. 锁的重入(Reentrancy)是什么
  3. 同步锁的本质?
  4. ...

更何况,解决并发问题,也绝对不是加锁这么简单,我们还需要了解:

  1. volatile关键字的含义
  2. 指令重排是什么
  3. 如何安全的发布对象
  4. 如何设计一个线程安全的类
  5. ...

再者,解决了线程安全,我们还需要考虑线程的生命周期管理、线程使用的性能问题等:

  1. 如何取消一个线程
  2. 如何关闭一个有很多线程的服务
  3. 如何设计线程池的大小
  4. ThreadPoolExecutor,Future等Java线程框架的使用
  5. 线程被中断了如何处理
  6. 线程池资源不够了,有什么处理策略
  7. 死锁的N种情形
  8. ...

乃至我们学习Java并发编程最最初始的问题:

  1. 我们为什么要学习并发编程
  2. 并发和异步的关系

这些,都是我新的一年里要和大家一起分享的,分享的内容主要基于《Java并发编程实践》里提到的知识,我买了中文版和英文版。这是一本很难啃的书,我会一如既往的用通俗易懂的语言来和大家分享我的学习心得。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,293评论 18 399
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,565评论 25 707
  • 在西安落地到坐上机场大巴的这段时间,其实是失望的,填空灰蒙蒙,嗓子的发痒证明不是因为天气,树木都是枯败的样子,没有...
    白隐阅读 586评论 0 51
  • China launched its second space laboratory, the Tiangong ...
    莫徯阅读 493评论 0 0
  • 人的生命就像是大海 时而平静 时而澎湃 生活在同一片蓝天下 生命有阳光明媚 活的也许会多一...
    彩虹人生阅读 413评论 0 5