Android 多线程之线程安全问题

什么是线程安全问题

线程安全问题不是说线程不安全,而是多个线程之间交错操作有可能导致数据异常。就比如两个线程同时对一个数据进行操作,不能保证最后得到是数据是正确的,这就出现了线程安全问题。

什么是Java内存模型

在这之前,先了解下Java内存模型是什么,这能帮助我们更好的理解线程的安全性问题。


Java内存模型

其实线程每次对数据操作,这些数据都是当前线程工作内存中的共享变量副本,并不是直接在主内存操作。每条线程都有自己的工作内存。

如果线程对变量的操作没有刷回主内存的话,仅仅改变了自己的工作内存的变量副本,那么对其他线程来说是不可见的,不知道这个变量发生变化。而如果一个变量没有读取内存中新值,而是使用旧的值去做后续操作的话,会得到一个错误的结果,这里体现出线程安全问题 -- 可见性

什么是Java线程调度

在任意时刻,CPU 只能执行一条机器指令,每个线程只有获取到 CPU 的使用权后,才可以执行指令。也就是在任意时刻,只有一个线程占用 CPU,处于运行的状态。

多线程并发运行实际上是指多个线程轮流获取 CPU 使用权,分别执行各自的任务。线程的调度由 JVM 负责,线程的调度是按照特定的机制为多个线程分配 CPU 的使用权。线程调度模型分为两类:分时调度模型和抢占式调度模型。

1、分时调度模型:让所有线程轮流获取 CPU 使用权,并且平均分配每个线程占用 CPU 的时间片。
2、抢占式调度模型:JVM 采用的是抢占式调度模型,也就是先让优先级高的线程占用 CPU,如果线程的优先级都一样,那就随机选择一个线程,并让该线程占用 CPU。也就是如果我们同时启动多个线程,并不能保证它们能轮流获取到均等的时间片。如果我们的程序想干预线程的调度过程,最简单的办法就是给每个线程设定一个优先级。

什么是数据依赖性

如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖。数据依赖分下列三种类型:
1、写后读(a = 1;b = a;),写一个变量之后,再读这个位置。
2、写后写 (a = 1;a = 2;),写一个变量之后,再写这个变量。
3、读后写(a = b;b = 1;),读一个变量之后,再写这个变量。

上面三种情况,只要重排序两个操作的执行顺序,程序的执行结果将会被改变。所以,编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。也就是说:在单线程环境下,指令执行的最终效果应当与其在顺序执行下的效果一致,否则这种优化便会失去意义。

如何保证线程安全

1、原子性
2、可见性
3、有序性
要实现线程安全就要保证上面说到的原子性、可见性和有序性。

原子性

在讲原子性之前先来看看这个例子:

a++,对于共享变量a的操作,实际上会执行三个步骤,
1.读取变量a的值
2.a的值+1
3.将值赋予变量a

这三个操作中任何一个操作过程中,a的值被人篡改,那么都会出现我们不希望出现的结果。在多线程中,a的值可能被其他线程修改,导致线程不安全。为了保证线程安全,必须把这三个步骤当成不可分割的一个整体操作,在其他线程看来,该操作只有未开始和结束的两种状态,不知道中间发生什么。这就体现了原子性。

在单线程环境下我们可以认为整个步骤都是原子性操作,但是在多线程环境下则不同,Java只保证了基本数据类型的变量和赋值操作才是原子性的。

注:基本数据类型的变量和赋值操作类似 i = 0 的操作

可见性

刚才讲到Java内存模型时,有提及到可见性。可见性是指一个线程对共享变量的更新,对于其他读取该变量的线程是否可见。怎么让修改完的数据可见呢?举个例子

① 将工作内存1中的共享变量的改变更新到主内存中
② 将主内存中最新的共享变量的变化更新到工作内存2中

Java提供了volatile关键字来保证可见性。通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

有序性

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。举个例子

class ReorderExample {
    int a = 0;
    boolean flag = false;
 
    public void writer() {
        a = 1;          // 1
        flag = true;    // 2
    }
 
    public void reader() {
        if (flag) {          // 3
            int i = a * a; // 4
        }
    }
}

思考一下,flag变量是个标记,用来标识变量a是否已被写入。这里假设有两个线程A和B,A首先执行writer()方法,随后B线程接着执行reader()方法。线程B在执行操作4时,能否看到线程A在操作1对共享变量a的写入?

答案是:不一定能看到。

由于操作1和操作2没有数据依赖关系,编译器和处理器可以对这两个操作重排序;同样,操作3和操作4没有数据依赖关系,编译器和处理器也可以对这两个操作重排序。让我们先来看看,当操作1和操作2重排序时,可能会产生什么效果?

执行顺序是:2 -> 3 -> 4 -> 1 (这是完全存在并且合理的一种顺序,如果你不能理解,请先了解CPU是如何对多个线程进行时间分配的)

操作3和操作4重排序后,因为操作3和操作4存在控制依赖关系。当代码中存在控制依赖性时,会影响指令序列执行的并行度。为此,编译器和处理器会采用猜测(Speculation)执行来克服控制相关性对并行度的影响。以处理器的猜测执行为例,执行线程B的处理器可以提前读取并计算a*a,然后把计算结果临时保存到一个名为重排序缓冲(reorder buffer ROB)的硬件缓存中。当接下来操作3的条件判断为真时,就把该计算结果写入变量i中。我们可以看出,猜测执行实质上对操作3和4做了重排序。重排序在这里破坏了多线程程序的语义。

在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。

happens-before原则(先行发生原则)

程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。

锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作。

volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。

传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。

线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作。

线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。

线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。

对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始。

总结

要实现线程安全就要保证上面说到的原子性、可见性和有序性。

注:提及到的volatile、synchronized和Lock后续写一篇关于锁的文章Android多线程之锁

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

推荐阅读更多精彩内容